Class: Paperback::Preparer

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/paperback/preparer.rb

Overview

Class wrapping functions to prepare data for paperback storage, including QR code and sixword encoding.

Constant Summary collapse

PassChars =
T.let(
  [*'a'..'z', *'A'..'Z', *'0'..'9'].freeze, T::Array[String]
)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(filename:, encrypt: true, qr_base64: false, qr_level: nil, comment: nil, passphrase_file: nil, include_base64: true) ⇒ Preparer

Returns a new instance of Preparer.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/paperback/preparer.rb', line 44

def initialize(filename:, encrypt: true, qr_base64: false, qr_level: nil,
               comment: nil, passphrase_file: nil, include_base64: true)

  log.debug('Preparer#initialize')

  # lazy initializers, all explicitly set to nil
  @log = T.let(nil, T.nilable(Logger))
  @qr_code = T.let(nil, T.nilable(RQRCode::QRCode))
  @sixword_lines = T.let(nil, T.nilable(T::Array[String]))
  @passphrase = T.let(nil, T.nilable(String))

  log.info("Reading #{filename.inspect}")
  plain_data = File.read(filename)

  log.debug("Read #{plain_data.bytesize} bytes")

  @encrypt = T.let(encrypt, T::Boolean)

  if encrypt
    @data = self.class.gpg_encrypt(filename: filename, password: passphrase)
  else
    @data = T.let(plain_data, String)
  end
  @sha256 = T.let(Digest::SHA256.hexdigest(plain_data), String)

  @qr_base64 = T.let(qr_base64, T::Boolean)
  @qr_level = T.let(qr_level, T.nilable(Symbol))

  @passphrase_file = T.let(passphrase_file, T.nilable(String))

  @include_base64 = T.let(!!include_base64, T::Boolean)

  @labels = T.let({}, T::Hash[String, T.untyped])
  @labels['Filename'] = filename
  @labels['Backed up'] = Time.now.to_s

  stat = File.stat(filename)
  @labels['Mtime'] = stat.mtime
  @labels['Bytes'] = plain_data.bytesize
  @labels['Comment'] = comment if comment

  @labels['SHA256'] = Digest::SHA256.hexdigest(plain_data)

  @document = T.let(Paperback::Document.new, Paperback::Document)
end

Instance Attribute Details

#dataObject (readonly)

Returns the value of attribute data.



19
20
21
# File 'lib/paperback/preparer.rb', line 19

def data
  @data
end

#encryptObject (readonly)

Returns the value of attribute encrypt.



28
29
30
# File 'lib/paperback/preparer.rb', line 28

def encrypt
  @encrypt
end

#labelsObject (readonly)

Returns the value of attribute labels.



22
23
24
# File 'lib/paperback/preparer.rb', line 22

def labels
  @labels
end

#passphrase_fileObject (readonly)

Returns the value of attribute passphrase_file.



31
32
33
# File 'lib/paperback/preparer.rb', line 31

def passphrase_file
  @passphrase_file
end

#qr_base64Object (readonly)

Returns the value of attribute qr_base64.



25
26
27
# File 'lib/paperback/preparer.rb', line 25

def qr_base64
  @qr_base64
end

Class Method Details

.gpg_ascii_dearmor(data) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/paperback/preparer.rb', line 212

def self.gpg_ascii_dearmor(data)
  cmd = %w[gpg --batch --dearmor]
  out = T.let(nil, T.nilable(String))

  log.debug('+ ' + cmd.join(' '))
  Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
                             stdout: Subprocess::PIPE) do |p|
    out, _err = p.communicate(data)
  end

  T.must(out)
end

.gpg_ascii_enarmor(data, strip_comments: true) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/paperback/preparer.rb', line 192

def self.gpg_ascii_enarmor(data, strip_comments: true)
  cmd = %w[gpg --batch --enarmor]
  out = T.let(nil, T.nilable(String))

  log.debug('+ ' + cmd.join(' '))
  Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
                             stdout: Subprocess::PIPE) do |p|
    out, _err = p.communicate(data)
  end

  out = T.must(out)

  if strip_comments
    out = out.each_line.select { |l| !l.start_with?('Comment: ') }.join
  end

  out
end

.gpg_encrypt(filename:, password:) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/paperback/preparer.rb', line 176

def self.gpg_encrypt(filename:, password:)
  cmd = %w[
    gpg -c -o - --batch --cipher-algo aes256 --passphrase-fd 0 --
  ] + [filename]
  out = T.let(nil, T.nilable(String))

  log.debug('+ ' + cmd.join(' '))
  Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
                             stdout: Subprocess::PIPE) do |p|
    out, _err = p.communicate(password)
  end

  T.must(out)
end

.logObject



97
98
99
# File 'lib/paperback/preparer.rb', line 97

def self.log
  @log ||= Paperback.class_log(self)
end

.random_passphrase(entropy_bits: 256, char_set: PassChars) ⇒ Object



162
163
164
165
166
167
# File 'lib/paperback/preparer.rb', line 162

def self.random_passphrase(entropy_bits: 256, char_set: PassChars)
  chars_needed = (entropy_bits / Math.log2(char_set.length)).ceil
  (0...chars_needed).map {
    PassChars.fetch(SecureRandom.random_number(char_set.length))
  }.join
end

.truncated_sha256(content) ⇒ Object



171
172
173
# File 'lib/paperback/preparer.rb', line 171

def self.truncated_sha256(content)
  Digest::SHA256.hexdigest(content)[0...16]
end

Instance Method Details

#include_base64?Boolean

Returns:

  • (Boolean)


229
230
231
# File 'lib/paperback/preparer.rb', line 229

def include_base64?
  !!@include_base64
end

#logObject



93
94
95
# File 'lib/paperback/preparer.rb', line 93

def log
  @log ||= Paperback.class_log(self.class)
end

#passphraseObject



149
150
151
152
# File 'lib/paperback/preparer.rb', line 149

def passphrase
  raise "Can't have passphrase without encrypt" unless encrypt
  @passphrase ||= self.class.random_passphrase
end

#render(output_filename:, extra_draw_opts: {}) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/paperback/preparer.rb', line 102

def render(output_filename:, extra_draw_opts: {})
  log.debug('Preparer#render')

  opts = {
    labels: labels,
    qr_code: qr_code,
    sixword_lines: sixword_lines,
    sixword_bytes: data.bytesize,
  }

  if include_base64?
    opts[:base64_content] = base64_content
    opts[:base64_bytes] = data.bytesize
  end

  if encrypt
    opts[:passphrase_sha] = self.class.truncated_sha256(passphrase)
    opts[:passphrase_len] = passphrase.length
    if passphrase_file
      File.open(
        T.must(passphrase_file),
        File::CREAT | File::EXCL | File::WRONLY,
        0o400
      ) do |f|
        f.write(passphrase)
      end
      log.info("Wrote passphrase to #{passphrase_file.inspect}")
    end
  end

  opts.merge!(extra_draw_opts)

  @document.render(output_file: output_filename, draw_opts: opts)

  log.info('Render complete')

  if encrypt
    puts 'SHA256(passphrase)[0...16]: ' + opts.fetch(:passphrase_sha)
    if !passphrase_file
      puts "Passphrase: #{passphrase}"
    end
  else
    log.info('Content was not encrypted')
  end
end