paperback 0.0.4 → 0.0.6

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.
Files changed (61) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/tests.yml +43 -0
  3. data/.rubocop-disables.yml +26 -9
  4. data/CHANGELOG.md +13 -0
  5. data/README.md +4 -1
  6. data/lib/paperback/cli.rb +17 -0
  7. data/lib/paperback/document.rb +78 -11
  8. data/lib/paperback/preparer.rb +77 -22
  9. data/lib/paperback/version.rb +2 -1
  10. data/lib/paperback.rb +14 -0
  11. data/paperback.gemspec +13 -9
  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 +55 -25
  53. data/spec/spec_helper.rb +1 -0
  54. data/spec/unit/paperback_spec.rb +1 -0
  55. metadata +107 -20
  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: 88232ed0cebaaa8e981ad4c712af3db6352b5857fe3f19a9720cd4c04ad9ec98
4
+ data.tar.gz: 1ad5bdd43ff11ed697eb42b554644fc0787580489b936b5cef7f47dce825d27a
5
5
  SHA512:
6
- metadata.gz: 61dd66d5b7c0e2db779b5124b1b7d20553303841fc1c7dbcfe04443f7b1e8b96af4dcad4af5d47e701dbd5e708106e699c90a08ca3c600ff4d6d27fd307bde41
7
- data.tar.gz: fec463faf717472399de4884229a3223b9f64b61136f5f72dfdf327331544d20c3dde37307d5bf33703640850d7267015f17cc00ff1ada99bfafb280b03935c6
6
+ metadata.gz: 0db0b2a87f6044e2bf9fedf40b186ba4227357fbe4a2dd1d1a917832702bc8f5dc4cacbda532dc320d8f9d3a2731cd0317c1f42f040d694b2ed683dd59a9e745
7
+ data.tar.gz: d613529a3706d23b08fd37b82d101356745110ed8c2d3ad318224cabbf1707c55ecb7469f38b91c309bb1a7d33d8a98acabbec7c9824d32dd2aebade3812ab34
@@ -0,0 +1,43 @@
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: ['3.0', 3.1, 3.2, 3.3, 3.4]
24
+
25
+ steps:
26
+ - uses: actions/checkout@v5
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
+ uses: ruby/setup-ruby@v1
35
+ with:
36
+ ruby-version: ${{ matrix.ruby-version }}
37
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
38
+
39
+ - name: Run tests
40
+ run: bundle exec rake test
41
+
42
+ - name: Sorbet Typecheck
43
+ 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,19 @@ This project adheres to [Semantic Versioning](http://semver.org).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.0.6] -- 2025-09-17
8
+
9
+ - Require ruby version >= 3.0
10
+ - Add base64 dependency to fix compatibility with newest ruby versions.
11
+ - Update dependencies.
12
+
13
+ ## [0.0.5] -- 2023-01-17
14
+
15
+ - Upgrade to modern versions of ruby
16
+ - Upgrade a few dependencies, fix type checking
17
+ - Fix tests to work on newer versions of pdftotext
18
+ - Run tests on Github Actions
19
+
7
20
  ## [0.0.4] -- 2019-02-26
8
21
 
9
22
  - 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,47 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
4
+ # Temporarily disable $VERBOSE due to prawn warning
5
+ # https://github.com/prawnpdf/prawn/issues/1349
6
+ $VERBOSE = false
3
7
  require 'prawn'
8
+ $VERBOSE = true
4
9
 
5
10
  # Main class for creating and rendering PDFs
6
11
  module Paperback; class Document
7
- attr_reader :pdf, :debug
12
+ extend T::Sig
8
13
 
14
+ sig {returns(Prawn::Document)}
15
+ attr_reader :pdf
16
+
17
+ sig {returns(T::Boolean)}
18
+ attr_reader :debug
19
+
20
+ sig {params(debug: T::Boolean).void}
9
21
  def initialize(debug: false)
10
22
  log.debug('Document#initialize')
11
- @debug = debug
12
- @pdf = Prawn::Document.new
23
+ @debug = T.let(debug, T::Boolean)
24
+ @pdf = T.let(Prawn::Document.new, Prawn::Document)
25
+ @log = T.let(nil, T.nilable(Logger))
13
26
  end
14
27
 
28
+ sig {returns(Logger)}
15
29
  def log
16
30
  @log ||= Paperback.class_log(self.class)
17
31
  end
18
32
 
33
+ sig do
34
+ params(
35
+ output_file: String,
36
+ draw_opts: T::Hash[Symbol, T.untyped],
37
+ )
38
+ .void
39
+ end
19
40
  def render(output_file:, draw_opts:)
20
41
  log.info('Rendering PDF')
21
42
 
22
43
  # Create all the PDF content
23
- draw_paperback(**draw_opts)
44
+ draw_paperback(**T.unsafe(draw_opts))
24
45
 
25
46
  # Render to output file
26
47
  log.info("Writing PDF to #{output_file.inspect}")
@@ -42,13 +63,25 @@ module Paperback; class Document
42
63
  # content (possibly encrypted) encoded using Base64.
43
64
  # @param [Integer, nil] base64_bytes The length of the original content
44
65
  # before encoding to base64. This is used for the informational header.
66
+ sig do
67
+ params(
68
+ qr_code: RQRCode::QRCode,
69
+ sixword_lines: T::Array[String],
70
+ sixword_bytes: Integer,
71
+ labels: T::Hash[String, T.untyped],
72
+ passphrase_sha: T.nilable(String),
73
+ passphrase_len: T.nilable(Integer),
74
+ sixword_font_size: T.nilable(Float),
75
+ base64_content: T.nilable(String),
76
+ base64_bytes: T.nilable(Integer),
77
+ )
78
+ .void
79
+ end
45
80
  def draw_paperback(qr_code:, sixword_lines:, sixword_bytes:, labels:,
46
81
  passphrase_sha: nil, passphrase_len: nil,
47
82
  sixword_font_size: nil, base64_content: nil,
48
83
  base64_bytes: nil)
49
- unless qr_code.is_a?(RQRCode::QRCode)
50
- raise ArgumentError.new('qr_code must be RQRCode::QRCode')
51
- end
84
+ T.assert_type!(qr_code, RQRCode::QRCode)
52
85
 
53
86
  # Header & QR code page
54
87
  pdf.font('Times-Roman')
@@ -71,11 +104,11 @@ module Paperback; class Document
71
104
 
72
105
  draw_sixword(lines: sixword_lines, sixword_bytes: sixword_bytes,
73
106
  font_size: sixword_font_size,
74
- is_encrypted: passphrase_len)
107
+ is_encrypted: !!passphrase_len)
75
108
 
76
109
  if base64_content
77
- draw_base64(b64_content: base64_content, b64_bytes: base64_bytes,
78
- is_encrypted: passphrase_len)
110
+ draw_base64(b64_content: base64_content, b64_bytes: T.must(base64_bytes),
111
+ is_encrypted: !!passphrase_len)
79
112
  end
80
113
 
81
114
  pdf.number_pages('<page> of <total>', align: :right,
@@ -83,16 +116,27 @@ module Paperback; class Document
83
116
  end
84
117
 
85
118
  # If in debug mode, draw axes on the page to assist with layout
119
+ sig {void}
86
120
  def debug_draw_axes
87
121
  return unless debug
88
122
  pdf.float { pdf.stroke_axis }
89
123
  end
90
124
 
91
125
  # Move cursor down by one line
126
+ sig {void}
92
127
  def add_newline
93
128
  pdf.move_down(pdf.font_size)
94
129
  end
95
130
 
131
+ sig do
132
+ params(
133
+ labels: T::Hash[String, T.untyped],
134
+ passphrase_sha: T.nilable(String),
135
+ passphrase_len: T.nilable(Integer),
136
+ repo_url: String,
137
+ )
138
+ .void
139
+ end
96
140
  def draw_header(labels:, passphrase_sha:, passphrase_len:,
97
141
  repo_url: 'https://github.com/ab/paperback')
98
142
 
@@ -103,7 +147,7 @@ module Paperback; class Document
103
147
  pdf.text(intro, inline_format: true)
104
148
  add_newline
105
149
 
106
- label_pad = labels.keys.map(&:length).max + 1
150
+ label_pad = T.must(labels.keys.map(&:length).max) + 1
107
151
 
108
152
  unless passphrase_sha && passphrase_len
109
153
  labels['Encrypted'] = 'no'
@@ -140,6 +184,16 @@ module Paperback; class Document
140
184
  # @param [Integer] columns The number of text columns on the page
141
185
  # @param [Integer] hunks_per_row The number of 6-word sentences per line
142
186
  # @param [Integer] sixword_bytes Bytesize of the sixword encoded data
187
+ sig do
188
+ params(
189
+ lines: T::Array[String],
190
+ sixword_bytes: Integer,
191
+ columns: Integer,
192
+ hunks_per_row: Integer,
193
+ font_size: T.nilable(Float),
194
+ is_encrypted: T::Boolean,
195
+ ).void
196
+ end
143
197
  def draw_sixword(lines:, sixword_bytes:, columns: 3, hunks_per_row: 1,
144
198
  font_size: nil, is_encrypted: true)
145
199
  font_size ||= 11
@@ -171,6 +225,11 @@ module Paperback; class Document
171
225
  end
172
226
  end
173
227
 
228
+ sig do
229
+ params(
230
+ qr_modules: T::Array[T::Array[T::Boolean]],
231
+ ).void
232
+ end
174
233
  def draw_qr_code(qr_modules:)
175
234
  qr_height = pdf.cursor # entire rest of page
176
235
  qr_width = pdf.bounds.width # entire page width
@@ -203,6 +262,14 @@ module Paperback; class Document
203
262
  end
204
263
 
205
264
  # @param [String] b64_content
265
+ sig do
266
+ params(
267
+ b64_content: String,
268
+ b64_bytes: Integer,
269
+ font_size: T.nilable(Float),
270
+ is_encrypted: T::Boolean,
271
+ ).void
272
+ end
206
273
  def draw_base64(b64_content:, b64_bytes:, font_size: nil, is_encrypted: true)
207
274
  font_size ||= 11
208
275
 
@@ -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
- Digest::SHA256.hexdigest(content)[0...16]
172
+ T.must(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.6'
5
6
  end
data/lib/paperback.rb CHANGED
@@ -1,7 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
1
4
  require 'logger'
5
+ require 'sorbet-runtime'
2
6
 
3
7
  # Paperback is a library for creating paper backups of sensitive data.
4
8
  module Paperback
9
+ extend T::Sig
10
+
11
+ @log = T.let(nil, T.nilable(Logger))
12
+
13
+ sig {returns(Logger)}
5
14
  def self.log
6
15
  return @log if @log
7
16
  @log = Logger.new(STDERR)
@@ -10,6 +19,7 @@ module Paperback
10
19
  @log
11
20
  end
12
21
 
22
+ sig {params(klass: T::Class[T.anything], stream: IO).returns(Logger)}
13
23
  def self.class_log(klass, stream=STDERR)
14
24
  log = Logger.new(stream)
15
25
  log.progname = klass.name
@@ -17,10 +27,14 @@ module Paperback
17
27
  log
18
28
  end
19
29
 
30
+ @log_level = T.let(nil, T.nilable(Integer))
31
+
32
+ sig {returns(Integer)}
20
33
  def self.log_level
21
34
  @log_level ||= Logger::INFO
22
35
  end
23
36
 
37
+ sig {params(val: Integer).returns(Integer)}
24
38
  def self.log_level=(val)
25
39
  @log_level = val
26
40
  end