paperback 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/paperback.svg)](https://rubygems.org/gems/paperback)
|
4
4
|
[![Inline Docs](http://inch-ci.org/github/ab/paperback.svg?branch=master)](http://www.rubydoc.info/github/ab/paperback/master)
|
5
|
+
[![Test status](https://github.com/ab/paperback/actions/workflows/tests.yml/badge.svg)](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
|
+
![sample page one](./sample/sample.pg1.png)
|
47
|
+
![sample page two](./sample/sample.pg2.png)
|
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
|