paperback 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/tests.yml +46 -0
- data/.rubocop-disables.yml +26 -9
- data/CHANGELOG.md +7 -0
- data/README.md +4 -1
- data/lib/paperback/cli.rb +17 -0
- data/lib/paperback/document.rb +74 -11
- data/lib/paperback/preparer.rb +76 -21
- data/lib/paperback/version.rb +2 -1
- data/lib/paperback.rb +12 -0
- data/paperback.gemspec +10 -7
- data/sorbet/config +3 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
- data/sorbet/rbi/gems/chunky_png@1.4.0.rbi +4498 -0
- data/sorbet/rbi/gems/coderay@1.1.3.rbi +3426 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
- data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
- data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
- data/sorbet/rbi/gems/parser@3.2.0.0.rbi +6963 -0
- data/sorbet/rbi/gems/pdf-core@0.4.0.rbi +1682 -0
- data/sorbet/rbi/gems/prawn@1.3.0.rbi +5567 -0
- data/sorbet/rbi/gems/pry@0.14.1.rbi +9990 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +408 -0
- data/sorbet/rbi/gems/rake@13.0.6.rbi +3023 -0
- data/sorbet/rbi/gems/rbi@0.0.16.rbi +3008 -0
- data/sorbet/rbi/gems/regexp_parser@2.6.1.rbi +3481 -0
- data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
- data/sorbet/rbi/gems/rqrcode@0.10.1.rbi +617 -0
- data/sorbet/rbi/gems/rspec-core@3.12.0.rbi +10791 -0
- data/sorbet/rbi/gems/rspec-expectations@3.12.1.rbi +8106 -0
- data/sorbet/rbi/gems/rspec-mocks@3.12.1.rbi +5305 -0
- data/sorbet/rbi/gems/rspec-support@3.12.0.rbi +1617 -0
- data/sorbet/rbi/gems/rspec@3.12.0.rbi +88 -0
- data/sorbet/rbi/gems/rubocop-ast@1.24.1.rbi +6617 -0
- data/sorbet/rbi/gems/rubocop@0.93.1.rbi +40848 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1234 -0
- data/sorbet/rbi/gems/sixword@0.4.0.rbi +536 -0
- data/sorbet/rbi/gems/spoom@1.1.15.rbi +2383 -0
- data/sorbet/rbi/gems/subprocess@1.5.6.rbi +391 -0
- data/sorbet/rbi/gems/tapioca@0.10.5.rbi +3207 -0
- data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
- data/sorbet/rbi/gems/ttfunk@1.4.0.rbi +1951 -0
- data/sorbet/rbi/gems/unicode-display_width@1.8.0.rbi +40 -0
- data/sorbet/rbi/gems/unparser@0.6.7.rbi +4524 -0
- data/sorbet/rbi/gems/webrick@1.7.0.rbi +2555 -0
- data/sorbet/rbi/gems/yard-sorbet@0.8.0.rbi +441 -0
- data/sorbet/rbi/gems/yard@0.9.28.rbi +17816 -0
- data/sorbet/tapioca/config.yml +13 -0
- data/sorbet/tapioca/require.rb +4 -0
- data/spec/functional/paperback/cli_spec.rb +54 -25
- data/spec/spec_helper.rb +1 -0
- data/spec/unit/paperback_spec.rb +1 -0
- metadata +89 -13
- data/sample/aes.key +0 -1
- data/sample/aes.pdf +0 -14413
- data/sample/aes.pdf.passphrase.txt +0 -1
- data/sample/rsa2048.pdf +0 -106803
- data/sample/rsa2048.pdf.passphrase.txt +0 -1
- data/sample/rsa2048.pem +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fe9b3003bc8749cbd4c5bd312b2b1d0b7546f9268e0f1c41e94c71f12a62df2c
|
4
|
+
data.tar.gz: d9630db98c97621804f79a951a140d2d9faa5080bc853c7e2492920e58dec07f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 15f5f07874c345f8ed9c0565a7704bf46be836c9d398ba8ec0f39fac6501cd4152762ed257cba92ea3e2f0e1abe223a9aec2a3318e13bc620507ec809033ba30
|
7
|
+
data.tar.gz: e6bbb5beb3ea669e663031ffe8d25088d2657f264fdd7569048f7f3d90264c0954f838025ae4c020c9b28c5971a8ca53669377edf5a731cfff8bcacfcb8fd4eb
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
2
|
+
# They are provided by a third-party and are governed by
|
3
|
+
# separate terms of service, privacy policy, and support
|
4
|
+
# documentation.
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
7
|
+
|
8
|
+
name: Ruby
|
9
|
+
|
10
|
+
on:
|
11
|
+
- push
|
12
|
+
- pull_request
|
13
|
+
|
14
|
+
permissions:
|
15
|
+
contents: read
|
16
|
+
|
17
|
+
jobs:
|
18
|
+
test:
|
19
|
+
|
20
|
+
runs-on: ubuntu-latest
|
21
|
+
strategy:
|
22
|
+
matrix:
|
23
|
+
ruby-version: ['2.7', '3.0', '3.1', '3.2']
|
24
|
+
|
25
|
+
steps:
|
26
|
+
- uses: actions/checkout@v3
|
27
|
+
|
28
|
+
- name: Install apt dependencies for tests
|
29
|
+
run: |
|
30
|
+
sudo apt-get update -y
|
31
|
+
sudo apt-get install poppler-utils
|
32
|
+
|
33
|
+
- name: Set up Ruby
|
34
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
35
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
36
|
+
# uses: ruby/setup-ruby@v1
|
37
|
+
uses: ruby/setup-ruby@319066216501fbd5e2d568f14b7d68c19fb67a5d # v1.33.1 / 2023-01-06
|
38
|
+
with:
|
39
|
+
ruby-version: ${{ matrix.ruby-version }}
|
40
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
41
|
+
|
42
|
+
- name: Run tests
|
43
|
+
run: bundle exec rake test
|
44
|
+
|
45
|
+
- name: Sorbet Typecheck
|
46
|
+
run: bundle exec srb tc
|
data/.rubocop-disables.yml
CHANGED
@@ -21,6 +21,9 @@ Metrics/MethodLength:
|
|
21
21
|
Metrics/ModuleLength:
|
22
22
|
Enabled: false
|
23
23
|
|
24
|
+
Metrics/ParameterLists:
|
25
|
+
Enabled: false
|
26
|
+
|
24
27
|
# Configuration parameters: Exclude.
|
25
28
|
#
|
26
29
|
# We could re-enable this if it understood that the top level module should
|
@@ -36,6 +39,11 @@ Style/Documentation:
|
|
36
39
|
# Disagree with these style points
|
37
40
|
# ********************************
|
38
41
|
|
42
|
+
Layout/EmptyLineBetweenDefs:
|
43
|
+
Enabled: false
|
44
|
+
Layout/EmptyLineAfterGuardClause:
|
45
|
+
Enabled: false
|
46
|
+
|
39
47
|
Style/DotPosition:
|
40
48
|
Enabled: false
|
41
49
|
Style/DoubleNegation:
|
@@ -57,12 +65,16 @@ Style/IfUnlessModifier:
|
|
57
65
|
Style/WhileUntilModifier:
|
58
66
|
Enabled: false
|
59
67
|
|
68
|
+
# Don't favor wacky unless methods
|
69
|
+
Style/NegatedIf:
|
70
|
+
Enabled: false
|
71
|
+
Style/InverseMethods:
|
72
|
+
Enabled: false
|
73
|
+
|
60
74
|
Style/InfiniteLoop:
|
61
75
|
Enabled: false
|
62
76
|
Style/PercentLiteralDelimiters:
|
63
|
-
|
64
|
-
'%w': '{}'
|
65
|
-
'%W': '{}'
|
77
|
+
Enabled: false
|
66
78
|
Style/RaiseArgs:
|
67
79
|
EnforcedStyle: compact
|
68
80
|
Style/RedundantReturn:
|
@@ -84,14 +96,13 @@ Style/SpaceInsideBlockBraces:
|
|
84
96
|
Style/SpaceInsideHashLiteralBraces:
|
85
97
|
Enabled: false
|
86
98
|
|
87
|
-
# Offense count: 27
|
88
|
-
# Cop supports --auto-correct.
|
89
99
|
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
|
90
|
-
Style/
|
91
|
-
|
92
|
-
|
93
|
-
# - no_comma
|
100
|
+
Style/TrailingCommaInArrayLiteral:
|
101
|
+
EnforcedStyleForMultiline: comma
|
102
|
+
Style/TrailingCommaInHashLiteral:
|
94
103
|
EnforcedStyleForMultiline: comma
|
104
|
+
Style/TrailingCommaInArguments:
|
105
|
+
Enabled: false
|
95
106
|
|
96
107
|
# Don't favor %w for arrays of words.
|
97
108
|
Style/WordArray:
|
@@ -101,6 +112,9 @@ Style/WordArray:
|
|
101
112
|
Style/SymbolArray:
|
102
113
|
Enabled: false
|
103
114
|
|
115
|
+
Style/NumericLiteralPrefix:
|
116
|
+
Enabled: false
|
117
|
+
|
104
118
|
# Definitely do NOT assign variables within a conditional. Christ.
|
105
119
|
Style/ConditionalAssignment:
|
106
120
|
Enabled: false
|
@@ -128,3 +142,6 @@ Style/GlobalVars:
|
|
128
142
|
Style/SpecialGlobalVars:
|
129
143
|
Exclude:
|
130
144
|
- 'paperback.gemspec'
|
145
|
+
|
146
|
+
Naming/HeredocDelimiterNaming:
|
147
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,13 @@ This project adheres to [Semantic Versioning](http://semver.org).
|
|
4
4
|
|
5
5
|
## [Unreleased]
|
6
6
|
|
7
|
+
## [0.0.5] -- 2023-01-17
|
8
|
+
|
9
|
+
- Upgrade to modern versions of ruby
|
10
|
+
- Upgrade a few dependencies, fix type checking
|
11
|
+
- Fix tests to work on newer versions of pdftotext
|
12
|
+
- Run tests on Github Actions
|
13
|
+
|
7
14
|
## [0.0.4] -- 2019-02-26
|
8
15
|
|
9
16
|
- Add some basic end-to-end tests.
|
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
# Paperback
|
1
|
+
# Paperback
|
2
2
|
|
3
3
|
[](https://rubygems.org/gems/paperback)
|
4
4
|
[](http://www.rubydoc.info/github/ab/paperback/master)
|
5
|
+
[](https://github.com/ab/paperback/actions/workflows/tests.yml)
|
5
6
|
|
6
7
|
*Paperback* is a library that facilitates the creation of paper offline backups
|
7
8
|
of small amounts of important data, such as encryption keys.
|
@@ -42,6 +43,8 @@ paperback data.key out.pdf
|
|
42
43
|
|
43
44
|
See [sample directory](./sample)
|
44
45
|
|
46
|
+

|
47
|
+

|
45
48
|
|
46
49
|
### More complex patterns
|
47
50
|
|
data/lib/paperback/cli.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
# typed: strict
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
module Paperback
|
4
5
|
module CLI
|
6
|
+
extend T::Sig
|
7
|
+
|
5
8
|
# Top level CLI interface for Paperback. This is the one stop shop for
|
6
9
|
# calling paperback.
|
7
10
|
#
|
@@ -18,6 +21,20 @@ module Paperback
|
|
18
21
|
# [Paperback::Preparer#render]
|
19
22
|
# @param [Boolean] include_base64 Whether to include a Base64 copy of the
|
20
23
|
# input
|
24
|
+
sig do
|
25
|
+
params(
|
26
|
+
input: String,
|
27
|
+
output: String,
|
28
|
+
encrypt: T::Boolean,
|
29
|
+
qr_base64: T::Boolean,
|
30
|
+
qr_level: Symbol,
|
31
|
+
comment: T.nilable(String),
|
32
|
+
passphrase_file: T.nilable(String),
|
33
|
+
extra_draw_opts: T::Hash[T.untyped, T.untyped],
|
34
|
+
include_base64: T::Boolean,
|
35
|
+
)
|
36
|
+
.void
|
37
|
+
end
|
21
38
|
def self.create_backup(input:, output:, encrypt: true, qr_base64: true,
|
22
39
|
qr_level: :l, comment: nil, passphrase_file: nil,
|
23
40
|
extra_draw_opts: {}, include_base64: true)
|
data/lib/paperback/document.rb
CHANGED
@@ -1,26 +1,43 @@
|
|
1
|
+
# typed: strict
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'prawn'
|
4
5
|
|
5
6
|
# Main class for creating and rendering PDFs
|
6
7
|
module Paperback; class Document
|
7
|
-
|
8
|
+
extend T::Sig
|
8
9
|
|
10
|
+
sig {returns(Prawn::Document)}
|
11
|
+
attr_reader :pdf
|
12
|
+
|
13
|
+
sig {returns(T::Boolean)}
|
14
|
+
attr_reader :debug
|
15
|
+
|
16
|
+
sig {params(debug: T::Boolean).void}
|
9
17
|
def initialize(debug: false)
|
10
18
|
log.debug('Document#initialize')
|
11
|
-
@debug = debug
|
12
|
-
@pdf = Prawn::Document.new
|
19
|
+
@debug = T.let(debug, T::Boolean)
|
20
|
+
@pdf = T.let(Prawn::Document.new, Prawn::Document)
|
21
|
+
@log = T.let(nil, T.nilable(Logger))
|
13
22
|
end
|
14
23
|
|
24
|
+
sig {returns(Logger)}
|
15
25
|
def log
|
16
26
|
@log ||= Paperback.class_log(self.class)
|
17
27
|
end
|
18
28
|
|
29
|
+
sig do
|
30
|
+
params(
|
31
|
+
output_file: String,
|
32
|
+
draw_opts: T::Hash[Symbol, T.untyped],
|
33
|
+
)
|
34
|
+
.void
|
35
|
+
end
|
19
36
|
def render(output_file:, draw_opts:)
|
20
37
|
log.info('Rendering PDF')
|
21
38
|
|
22
39
|
# Create all the PDF content
|
23
|
-
draw_paperback(**draw_opts)
|
40
|
+
draw_paperback(**T.unsafe(draw_opts))
|
24
41
|
|
25
42
|
# Render to output file
|
26
43
|
log.info("Writing PDF to #{output_file.inspect}")
|
@@ -42,13 +59,25 @@ module Paperback; class Document
|
|
42
59
|
# content (possibly encrypted) encoded using Base64.
|
43
60
|
# @param [Integer, nil] base64_bytes The length of the original content
|
44
61
|
# before encoding to base64. This is used for the informational header.
|
62
|
+
sig do
|
63
|
+
params(
|
64
|
+
qr_code: RQRCode::QRCode,
|
65
|
+
sixword_lines: T::Array[String],
|
66
|
+
sixword_bytes: Integer,
|
67
|
+
labels: T::Hash[String, T.untyped],
|
68
|
+
passphrase_sha: T.nilable(String),
|
69
|
+
passphrase_len: T.nilable(Integer),
|
70
|
+
sixword_font_size: T.nilable(Float),
|
71
|
+
base64_content: T.nilable(String),
|
72
|
+
base64_bytes: T.nilable(Integer),
|
73
|
+
)
|
74
|
+
.void
|
75
|
+
end
|
45
76
|
def draw_paperback(qr_code:, sixword_lines:, sixword_bytes:, labels:,
|
46
77
|
passphrase_sha: nil, passphrase_len: nil,
|
47
78
|
sixword_font_size: nil, base64_content: nil,
|
48
79
|
base64_bytes: nil)
|
49
|
-
|
50
|
-
raise ArgumentError.new('qr_code must be RQRCode::QRCode')
|
51
|
-
end
|
80
|
+
T.assert_type!(qr_code, RQRCode::QRCode)
|
52
81
|
|
53
82
|
# Header & QR code page
|
54
83
|
pdf.font('Times-Roman')
|
@@ -71,11 +100,11 @@ module Paperback; class Document
|
|
71
100
|
|
72
101
|
draw_sixword(lines: sixword_lines, sixword_bytes: sixword_bytes,
|
73
102
|
font_size: sixword_font_size,
|
74
|
-
is_encrypted: passphrase_len)
|
103
|
+
is_encrypted: !!passphrase_len)
|
75
104
|
|
76
105
|
if base64_content
|
77
|
-
draw_base64(b64_content: base64_content, b64_bytes: base64_bytes,
|
78
|
-
is_encrypted: passphrase_len)
|
106
|
+
draw_base64(b64_content: base64_content, b64_bytes: T.must(base64_bytes),
|
107
|
+
is_encrypted: !!passphrase_len)
|
79
108
|
end
|
80
109
|
|
81
110
|
pdf.number_pages('<page> of <total>', align: :right,
|
@@ -83,16 +112,27 @@ module Paperback; class Document
|
|
83
112
|
end
|
84
113
|
|
85
114
|
# If in debug mode, draw axes on the page to assist with layout
|
115
|
+
sig {void}
|
86
116
|
def debug_draw_axes
|
87
117
|
return unless debug
|
88
118
|
pdf.float { pdf.stroke_axis }
|
89
119
|
end
|
90
120
|
|
91
121
|
# Move cursor down by one line
|
122
|
+
sig {void}
|
92
123
|
def add_newline
|
93
124
|
pdf.move_down(pdf.font_size)
|
94
125
|
end
|
95
126
|
|
127
|
+
sig do
|
128
|
+
params(
|
129
|
+
labels: T::Hash[String, T.untyped],
|
130
|
+
passphrase_sha: T.nilable(String),
|
131
|
+
passphrase_len: T.nilable(Integer),
|
132
|
+
repo_url: String,
|
133
|
+
)
|
134
|
+
.void
|
135
|
+
end
|
96
136
|
def draw_header(labels:, passphrase_sha:, passphrase_len:,
|
97
137
|
repo_url: 'https://github.com/ab/paperback')
|
98
138
|
|
@@ -103,7 +143,7 @@ module Paperback; class Document
|
|
103
143
|
pdf.text(intro, inline_format: true)
|
104
144
|
add_newline
|
105
145
|
|
106
|
-
label_pad = labels.keys.map(&:length).max + 1
|
146
|
+
label_pad = T.must(labels.keys.map(&:length).max) + 1
|
107
147
|
|
108
148
|
unless passphrase_sha && passphrase_len
|
109
149
|
labels['Encrypted'] = 'no'
|
@@ -140,6 +180,16 @@ module Paperback; class Document
|
|
140
180
|
# @param [Integer] columns The number of text columns on the page
|
141
181
|
# @param [Integer] hunks_per_row The number of 6-word sentences per line
|
142
182
|
# @param [Integer] sixword_bytes Bytesize of the sixword encoded data
|
183
|
+
sig do
|
184
|
+
params(
|
185
|
+
lines: T::Array[String],
|
186
|
+
sixword_bytes: Integer,
|
187
|
+
columns: Integer,
|
188
|
+
hunks_per_row: Integer,
|
189
|
+
font_size: T.nilable(Float),
|
190
|
+
is_encrypted: T::Boolean,
|
191
|
+
).void
|
192
|
+
end
|
143
193
|
def draw_sixword(lines:, sixword_bytes:, columns: 3, hunks_per_row: 1,
|
144
194
|
font_size: nil, is_encrypted: true)
|
145
195
|
font_size ||= 11
|
@@ -171,6 +221,11 @@ module Paperback; class Document
|
|
171
221
|
end
|
172
222
|
end
|
173
223
|
|
224
|
+
sig do
|
225
|
+
params(
|
226
|
+
qr_modules: T::Array[T::Array[T::Boolean]],
|
227
|
+
).void
|
228
|
+
end
|
174
229
|
def draw_qr_code(qr_modules:)
|
175
230
|
qr_height = pdf.cursor # entire rest of page
|
176
231
|
qr_width = pdf.bounds.width # entire page width
|
@@ -203,6 +258,14 @@ module Paperback; class Document
|
|
203
258
|
end
|
204
259
|
|
205
260
|
# @param [String] b64_content
|
261
|
+
sig do
|
262
|
+
params(
|
263
|
+
b64_content: String,
|
264
|
+
b64_bytes: Integer,
|
265
|
+
font_size: T.nilable(Float),
|
266
|
+
is_encrypted: T::Boolean,
|
267
|
+
).void
|
268
|
+
end
|
206
269
|
def draw_base64(b64_content:, b64_bytes:, font_size: nil, is_encrypted: true)
|
207
270
|
font_size ||= 11
|
208
271
|
|
data/lib/paperback/preparer.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# typed: strict
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'base64'
|
@@ -12,39 +13,67 @@ module Paperback
|
|
12
13
|
# Class wrapping functions to prepare data for paperback storage, including
|
13
14
|
# QR code and sixword encoding.
|
14
15
|
class Preparer
|
16
|
+
extend T::Sig
|
17
|
+
|
18
|
+
sig {returns(String)}
|
15
19
|
attr_reader :data
|
20
|
+
|
21
|
+
sig {returns(T::Hash[String, T.untyped])}
|
16
22
|
attr_reader :labels
|
23
|
+
|
24
|
+
sig {returns(T::Boolean)}
|
17
25
|
attr_reader :qr_base64
|
26
|
+
|
27
|
+
sig {returns(T::Boolean)}
|
18
28
|
attr_reader :encrypt
|
29
|
+
|
30
|
+
sig {returns(T.nilable(String))}
|
19
31
|
attr_reader :passphrase_file
|
20
32
|
|
33
|
+
sig do
|
34
|
+
params(
|
35
|
+
filename: String,
|
36
|
+
encrypt: T::Boolean,
|
37
|
+
qr_base64: T::Boolean,
|
38
|
+
qr_level: T.nilable(Symbol),
|
39
|
+
comment: T.nilable(String),
|
40
|
+
passphrase_file: T.nilable(String),
|
41
|
+
include_base64: T::Boolean,
|
42
|
+
).void
|
43
|
+
end
|
21
44
|
def initialize(filename:, encrypt: true, qr_base64: false, qr_level: nil,
|
22
45
|
comment: nil, passphrase_file: nil, include_base64: true)
|
23
46
|
|
24
47
|
log.debug('Preparer#initialize')
|
25
48
|
|
49
|
+
# lazy initializers, all explicitly set to nil
|
50
|
+
@log = T.let(nil, T.nilable(Logger))
|
51
|
+
@qr_code = T.let(nil, T.nilable(RQRCode::QRCode))
|
52
|
+
@sixword_lines = T.let(nil, T.nilable(T::Array[String]))
|
53
|
+
@passphrase = T.let(nil, T.nilable(String))
|
54
|
+
|
26
55
|
log.info("Reading #{filename.inspect}")
|
27
56
|
plain_data = File.read(filename)
|
28
57
|
|
29
58
|
log.debug("Read #{plain_data.bytesize} bytes")
|
30
59
|
|
31
|
-
@encrypt = encrypt
|
60
|
+
@encrypt = T.let(encrypt, T::Boolean)
|
32
61
|
|
33
62
|
if encrypt
|
34
63
|
@data = self.class.gpg_encrypt(filename: filename, password: passphrase)
|
35
64
|
else
|
36
|
-
@data = plain_data
|
65
|
+
@data = T.let(plain_data, String)
|
37
66
|
end
|
38
|
-
@sha256 = Digest::SHA256.hexdigest(plain_data)
|
67
|
+
@sha256 = T.let(Digest::SHA256.hexdigest(plain_data), String)
|
39
68
|
|
40
|
-
@qr_base64 = qr_base64
|
41
|
-
@qr_level = qr_level
|
69
|
+
@qr_base64 = T.let(qr_base64, T::Boolean)
|
70
|
+
@qr_level = T.let(qr_level, T.nilable(Symbol))
|
42
71
|
|
43
|
-
@passphrase_file = passphrase_file
|
72
|
+
@passphrase_file = T.let(passphrase_file, T.nilable(String))
|
44
73
|
|
45
|
-
@include_base64 = !!include_base64
|
74
|
+
@include_base64 = T.let(!!include_base64, T::Boolean)
|
46
75
|
|
47
|
-
@labels = {}
|
76
|
+
@labels = T.let({}, T::Hash[String, T.untyped])
|
48
77
|
@labels['Filename'] = filename
|
49
78
|
@labels['Backed up'] = Time.now.to_s
|
50
79
|
|
@@ -55,16 +84,21 @@ module Paperback
|
|
55
84
|
|
56
85
|
@labels['SHA256'] = Digest::SHA256.hexdigest(plain_data)
|
57
86
|
|
58
|
-
@document = Paperback::Document.new
|
87
|
+
@document = T.let(Paperback::Document.new, Paperback::Document)
|
59
88
|
end
|
60
89
|
|
90
|
+
@log = T.let(nil, T.nilable(Logger))
|
91
|
+
|
92
|
+
sig {returns(Logger)}
|
61
93
|
def log
|
62
94
|
@log ||= Paperback.class_log(self.class)
|
63
95
|
end
|
96
|
+
sig {returns(Logger)}
|
64
97
|
def self.log
|
65
98
|
@log ||= Paperback.class_log(self)
|
66
99
|
end
|
67
100
|
|
101
|
+
sig {params(output_filename: String, extra_draw_opts: T::Hash[T.untyped, T.untyped]).void}
|
68
102
|
def render(output_filename:, extra_draw_opts: {})
|
69
103
|
log.debug('Preparer#render')
|
70
104
|
|
@@ -84,8 +118,11 @@ module Paperback
|
|
84
118
|
opts[:passphrase_sha] = self.class.truncated_sha256(passphrase)
|
85
119
|
opts[:passphrase_len] = passphrase.length
|
86
120
|
if passphrase_file
|
87
|
-
File.open(
|
88
|
-
|
121
|
+
File.open(
|
122
|
+
T.must(passphrase_file),
|
123
|
+
File::CREAT | File::EXCL | File::WRONLY,
|
124
|
+
0o400
|
125
|
+
) do |f|
|
89
126
|
f.write(passphrase)
|
90
127
|
end
|
91
128
|
log.info("Wrote passphrase to #{passphrase_file.inspect}")
|
@@ -99,7 +136,7 @@ module Paperback
|
|
99
136
|
log.info('Render complete')
|
100
137
|
|
101
138
|
if encrypt
|
102
|
-
puts
|
139
|
+
puts 'SHA256(passphrase)[0...16]: ' + opts.fetch(:passphrase_sha)
|
103
140
|
if !passphrase_file
|
104
141
|
puts "Passphrase: #{passphrase}"
|
105
142
|
end
|
@@ -108,13 +145,20 @@ module Paperback
|
|
108
145
|
end
|
109
146
|
end
|
110
147
|
|
148
|
+
sig {returns(String)}
|
111
149
|
def passphrase
|
112
150
|
raise "Can't have passphrase without encrypt" unless encrypt
|
113
151
|
@passphrase ||= self.class.random_passphrase
|
114
152
|
end
|
115
153
|
|
116
|
-
PassChars =
|
154
|
+
PassChars = T.let(
|
155
|
+
[*'a'..'z', *'A'..'Z', *'0'..'9'].freeze, T::Array[String]
|
156
|
+
)
|
117
157
|
|
158
|
+
sig do
|
159
|
+
params(entropy_bits: Integer, char_set: T::Array[String])
|
160
|
+
.returns(String)
|
161
|
+
end
|
118
162
|
def self.random_passphrase(entropy_bits: 256, char_set: PassChars)
|
119
163
|
chars_needed = (entropy_bits / Math.log2(char_set.length)).ceil
|
120
164
|
(0...chars_needed).map {
|
@@ -123,35 +167,40 @@ module Paperback
|
|
123
167
|
end
|
124
168
|
|
125
169
|
# Compute a truncated SHA256 digest
|
170
|
+
sig {params(content: String).returns(String)}
|
126
171
|
def self.truncated_sha256(content)
|
127
172
|
Digest::SHA256.hexdigest(content)[0...16]
|
128
173
|
end
|
129
174
|
|
175
|
+
sig {params(filename: String, password: String).returns(String)}
|
130
176
|
def self.gpg_encrypt(filename:, password:)
|
131
177
|
cmd = %w[
|
132
178
|
gpg -c -o - --batch --cipher-algo aes256 --passphrase-fd 0 --
|
133
179
|
] + [filename]
|
134
|
-
out = nil
|
180
|
+
out = T.let(nil, T.nilable(String))
|
135
181
|
|
136
182
|
log.debug('+ ' + cmd.join(' '))
|
137
183
|
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
|
138
|
-
|
184
|
+
stdout: Subprocess::PIPE) do |p|
|
139
185
|
out, _err = p.communicate(password)
|
140
186
|
end
|
141
187
|
|
142
|
-
out
|
188
|
+
T.must(out)
|
143
189
|
end
|
144
190
|
|
191
|
+
sig {params(data: String, strip_comments: T::Boolean).returns(String)}
|
145
192
|
def self.gpg_ascii_enarmor(data, strip_comments: true)
|
146
193
|
cmd = %w[gpg --batch --enarmor]
|
147
|
-
out = nil
|
194
|
+
out = T.let(nil, T.nilable(String))
|
148
195
|
|
149
196
|
log.debug('+ ' + cmd.join(' '))
|
150
197
|
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
|
151
|
-
|
198
|
+
stdout: Subprocess::PIPE) do |p|
|
152
199
|
out, _err = p.communicate(data)
|
153
200
|
end
|
154
201
|
|
202
|
+
out = T.must(out)
|
203
|
+
|
155
204
|
if strip_comments
|
156
205
|
out = out.each_line.select { |l| !l.start_with?('Comment: ') }.join
|
157
206
|
end
|
@@ -159,32 +208,36 @@ module Paperback
|
|
159
208
|
out
|
160
209
|
end
|
161
210
|
|
211
|
+
sig {params(data: String).returns(String)}
|
162
212
|
def self.gpg_ascii_dearmor(data)
|
163
213
|
cmd = %w[gpg --batch --dearmor]
|
164
|
-
out = nil
|
214
|
+
out = T.let(nil, T.nilable(String))
|
165
215
|
|
166
216
|
log.debug('+ ' + cmd.join(' '))
|
167
217
|
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
|
168
|
-
|
218
|
+
stdout: Subprocess::PIPE) do |p|
|
169
219
|
out, _err = p.communicate(data)
|
170
220
|
end
|
171
221
|
|
172
|
-
out
|
222
|
+
T.must(out)
|
173
223
|
end
|
174
224
|
|
175
225
|
# Whether to add the Base64 encoding to the generated document.
|
176
226
|
#
|
177
227
|
# @return [Boolean]
|
228
|
+
sig {returns(T::Boolean)}
|
178
229
|
def include_base64?
|
179
230
|
!!@include_base64
|
180
231
|
end
|
181
232
|
|
182
233
|
private
|
183
234
|
|
235
|
+
sig {returns(RQRCode::QRCode)}
|
184
236
|
def qr_code
|
185
237
|
@qr_code ||= qr_code!
|
186
238
|
end
|
187
239
|
|
240
|
+
sig {returns(RQRCode::QRCode)}
|
188
241
|
def qr_code!
|
189
242
|
log.info('Generating QR code')
|
190
243
|
|
@@ -211,12 +264,14 @@ module Paperback
|
|
211
264
|
RQRCode::QRCode.new(input, level: @qr_level)
|
212
265
|
end
|
213
266
|
|
267
|
+
sig {returns(T::Array[String])}
|
214
268
|
def sixword_lines
|
215
269
|
log.info('Encoding with Sixword')
|
216
270
|
@sixword_lines ||=
|
217
271
|
Sixword.pad_encode_to_sentences(data).map(&:downcase)
|
218
272
|
end
|
219
273
|
|
274
|
+
sig {returns(String)}
|
220
275
|
def base64_content
|
221
276
|
log.debug('Encoding with Base64')
|
222
277
|
if encrypt
|
data/lib/paperback/version.rb
CHANGED
data/lib/paperback.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
+
# typed: strict
|
1
2
|
require 'logger'
|
3
|
+
require 'sorbet-runtime'
|
2
4
|
|
3
5
|
# Paperback is a library for creating paper backups of sensitive data.
|
4
6
|
module Paperback
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
@log = T.let(nil, T.nilable(Logger))
|
10
|
+
|
11
|
+
sig {returns(Logger)}
|
5
12
|
def self.log
|
6
13
|
return @log if @log
|
7
14
|
@log = Logger.new(STDERR)
|
@@ -10,6 +17,7 @@ module Paperback
|
|
10
17
|
@log
|
11
18
|
end
|
12
19
|
|
20
|
+
sig {params(klass: Class, stream: IO).returns(Logger)}
|
13
21
|
def self.class_log(klass, stream=STDERR)
|
14
22
|
log = Logger.new(stream)
|
15
23
|
log.progname = klass.name
|
@@ -17,10 +25,14 @@ module Paperback
|
|
17
25
|
log
|
18
26
|
end
|
19
27
|
|
28
|
+
@log_level = T.let(nil, T.nilable(Integer))
|
29
|
+
|
30
|
+
sig {returns(Integer)}
|
20
31
|
def self.log_level
|
21
32
|
@log_level ||= Logger::INFO
|
22
33
|
end
|
23
34
|
|
35
|
+
sig {params(val: Integer).returns(Integer)}
|
24
36
|
def self.log_level=(val)
|
25
37
|
@log_level = val
|
26
38
|
end
|