paperback 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/tests.yml +46 -0
  3. data/.rubocop-disables.yml +26 -9
  4. data/CHANGELOG.md +7 -0
  5. data/README.md +4 -1
  6. data/lib/paperback/cli.rb +17 -0
  7. data/lib/paperback/document.rb +74 -11
  8. data/lib/paperback/preparer.rb +76 -21
  9. data/lib/paperback/version.rb +2 -1
  10. data/lib/paperback.rb +12 -0
  11. data/paperback.gemspec +10 -7
  12. data/sorbet/config +3 -0
  13. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  14. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  15. data/sorbet/rbi/gems/chunky_png@1.4.0.rbi +4498 -0
  16. data/sorbet/rbi/gems/coderay@1.1.3.rbi +3426 -0
  17. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
  18. data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
  19. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  20. data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
  21. data/sorbet/rbi/gems/parser@3.2.0.0.rbi +6963 -0
  22. data/sorbet/rbi/gems/pdf-core@0.4.0.rbi +1682 -0
  23. data/sorbet/rbi/gems/prawn@1.3.0.rbi +5567 -0
  24. data/sorbet/rbi/gems/pry@0.14.1.rbi +9990 -0
  25. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +408 -0
  26. data/sorbet/rbi/gems/rake@13.0.6.rbi +3023 -0
  27. data/sorbet/rbi/gems/rbi@0.0.16.rbi +3008 -0
  28. data/sorbet/rbi/gems/regexp_parser@2.6.1.rbi +3481 -0
  29. data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
  30. data/sorbet/rbi/gems/rqrcode@0.10.1.rbi +617 -0
  31. data/sorbet/rbi/gems/rspec-core@3.12.0.rbi +10791 -0
  32. data/sorbet/rbi/gems/rspec-expectations@3.12.1.rbi +8106 -0
  33. data/sorbet/rbi/gems/rspec-mocks@3.12.1.rbi +5305 -0
  34. data/sorbet/rbi/gems/rspec-support@3.12.0.rbi +1617 -0
  35. data/sorbet/rbi/gems/rspec@3.12.0.rbi +88 -0
  36. data/sorbet/rbi/gems/rubocop-ast@1.24.1.rbi +6617 -0
  37. data/sorbet/rbi/gems/rubocop@0.93.1.rbi +40848 -0
  38. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1234 -0
  39. data/sorbet/rbi/gems/sixword@0.4.0.rbi +536 -0
  40. data/sorbet/rbi/gems/spoom@1.1.15.rbi +2383 -0
  41. data/sorbet/rbi/gems/subprocess@1.5.6.rbi +391 -0
  42. data/sorbet/rbi/gems/tapioca@0.10.5.rbi +3207 -0
  43. data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
  44. data/sorbet/rbi/gems/ttfunk@1.4.0.rbi +1951 -0
  45. data/sorbet/rbi/gems/unicode-display_width@1.8.0.rbi +40 -0
  46. data/sorbet/rbi/gems/unparser@0.6.7.rbi +4524 -0
  47. data/sorbet/rbi/gems/webrick@1.7.0.rbi +2555 -0
  48. data/sorbet/rbi/gems/yard-sorbet@0.8.0.rbi +441 -0
  49. data/sorbet/rbi/gems/yard@0.9.28.rbi +17816 -0
  50. data/sorbet/tapioca/config.yml +13 -0
  51. data/sorbet/tapioca/require.rb +4 -0
  52. data/spec/functional/paperback/cli_spec.rb +54 -25
  53. data/spec/spec_helper.rb +1 -0
  54. data/spec/unit/paperback_spec.rb +1 -0
  55. metadata +89 -13
  56. data/sample/aes.key +0 -1
  57. data/sample/aes.pdf +0 -14413
  58. data/sample/aes.pdf.passphrase.txt +0 -1
  59. data/sample/rsa2048.pdf +0 -106803
  60. data/sample/rsa2048.pdf.passphrase.txt +0 -1
  61. data/sample/rsa2048.pem +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 80af4f42712942cb193a1d3875f0de751a7b3803
4
- data.tar.gz: 5f23830de9c39209eeb6e7a72c8c39e40db2a921
2
+ SHA256:
3
+ metadata.gz: fe9b3003bc8749cbd4c5bd312b2b1d0b7546f9268e0f1c41e94c71f12a62df2c
4
+ data.tar.gz: d9630db98c97621804f79a951a140d2d9faa5080bc853c7e2492920e58dec07f
5
5
  SHA512:
6
- metadata.gz: 61dd66d5b7c0e2db779b5124b1b7d20553303841fc1c7dbcfe04443f7b1e8b96af4dcad4af5d47e701dbd5e708106e699c90a08ca3c600ff4d6d27fd307bde41
7
- data.tar.gz: fec463faf717472399de4884229a3223b9f64b61136f5f72dfdf327331544d20c3dde37307d5bf33703640850d7267015f17cc00ff1ada99bfafb280b03935c6
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
@@ -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
- PreferredDelimiters:
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/TrailingCommaInLiteral:
91
- # SupportedStyles:
92
- # - comma
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)
@@ -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
- attr_reader :pdf, :debug
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
- unless qr_code.is_a?(RQRCode::QRCode)
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
 
@@ -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(passphrase_file, File::CREAT|File::EXCL|File::WRONLY,
88
- 0400) do |f|
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 "SHA256(passphrase)[0...16]: " + opts.fetch(:passphrase_sha)
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 = [*'a'..'z', *'A'..'Z', *'0'..'9'].freeze
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
- stdout: Subprocess::PIPE) do |p|
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
- stdout: Subprocess::PIPE) do |p|
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
- stdout: Subprocess::PIPE) do |p|
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
@@ -1,5 +1,6 @@
1
+ # typed: strong
1
2
  # frozen_string_literal: true
2
3
  module Paperback
3
4
  # version string
4
- VERSION = '0.0.4'
5
+ VERSION = '0.0.5'
5
6
  end
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