lib/paperback/preparer.rb in paperback-0.0.4 vs lib/paperback/preparer.rb in paperback-0.0.5
- old
+ new
@@ -1,5 +1,6 @@
+# typed: strict
# frozen_string_literal: true
require 'base64'
require 'digest/sha2'
require 'securerandom'
@@ -10,63 +11,96 @@
module Paperback
# Class wrapping functions to prepare data for paperback storage, including
# QR code and sixword encoding.
class Preparer
+ extend T::Sig
+
+ sig {returns(String)}
attr_reader :data
+
+ sig {returns(T::Hash[String, T.untyped])}
attr_reader :labels
+
+ sig {returns(T::Boolean)}
attr_reader :qr_base64
+
+ sig {returns(T::Boolean)}
attr_reader :encrypt
+
+ sig {returns(T.nilable(String))}
attr_reader :passphrase_file
+ sig do
+ params(
+ filename: String,
+ encrypt: T::Boolean,
+ qr_base64: T::Boolean,
+ qr_level: T.nilable(Symbol),
+ comment: T.nilable(String),
+ passphrase_file: T.nilable(String),
+ include_base64: T::Boolean,
+ ).void
+ end
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 = encrypt
+ @encrypt = T.let(encrypt, T::Boolean)
if encrypt
@data = self.class.gpg_encrypt(filename: filename, password: passphrase)
else
- @data = plain_data
+ @data = T.let(plain_data, String)
end
- @sha256 = Digest::SHA256.hexdigest(plain_data)
+ @sha256 = T.let(Digest::SHA256.hexdigest(plain_data), String)
- @qr_base64 = qr_base64
- @qr_level = qr_level
+ @qr_base64 = T.let(qr_base64, T::Boolean)
+ @qr_level = T.let(qr_level, T.nilable(Symbol))
- @passphrase_file = passphrase_file
+ @passphrase_file = T.let(passphrase_file, T.nilable(String))
- @include_base64 = !!include_base64
+ @include_base64 = T.let(!!include_base64, T::Boolean)
- @labels = {}
+ @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 = Paperback::Document.new
+ @document = T.let(Paperback::Document.new, Paperback::Document)
end
+ @log = T.let(nil, T.nilable(Logger))
+
+ sig {returns(Logger)}
def log
@log ||= Paperback.class_log(self.class)
end
+ sig {returns(Logger)}
def self.log
@log ||= Paperback.class_log(self)
end
+ sig {params(output_filename: String, extra_draw_opts: T::Hash[T.untyped, T.untyped]).void}
def render(output_filename:, extra_draw_opts: {})
log.debug('Preparer#render')
opts = {
labels: labels,
@@ -82,12 +116,15 @@
if encrypt
opts[:passphrase_sha] = self.class.truncated_sha256(passphrase)
opts[:passphrase_len] = passphrase.length
if passphrase_file
- File.open(passphrase_file, File::CREAT|File::EXCL|File::WRONLY,
- 0400) do |f|
+ 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
@@ -97,96 +134,112 @@
@document.render(output_file: output_filename, draw_opts: opts)
log.info('Render complete')
if encrypt
- puts "SHA256(passphrase)[0...16]: " + opts.fetch(:passphrase_sha)
+ 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
+ sig {returns(String)}
def passphrase
raise "Can't have passphrase without encrypt" unless encrypt
@passphrase ||= self.class.random_passphrase
end
- PassChars = [*'a'..'z', *'A'..'Z', *'0'..'9'].freeze
+ PassChars = T.let(
+ [*'a'..'z', *'A'..'Z', *'0'..'9'].freeze, T::Array[String]
+ )
+ sig do
+ params(entropy_bits: Integer, char_set: T::Array[String])
+ .returns(String)
+ end
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
# Compute a truncated SHA256 digest
+ sig {params(content: String).returns(String)}
def self.truncated_sha256(content)
Digest::SHA256.hexdigest(content)[0...16]
end
+ sig {params(filename: String, password: String).returns(String)}
def self.gpg_encrypt(filename:, password:)
cmd = %w[
gpg -c -o - --batch --cipher-algo aes256 --passphrase-fd 0 --
] + [filename]
- out = nil
+ out = T.let(nil, T.nilable(String))
log.debug('+ ' + cmd.join(' '))
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
- stdout: Subprocess::PIPE) do |p|
+ stdout: Subprocess::PIPE) do |p|
out, _err = p.communicate(password)
end
- out
+ T.must(out)
end
+ sig {params(data: String, strip_comments: T::Boolean).returns(String)}
def self.gpg_ascii_enarmor(data, strip_comments: true)
cmd = %w[gpg --batch --enarmor]
- out = nil
+ out = T.let(nil, T.nilable(String))
log.debug('+ ' + cmd.join(' '))
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
- stdout: Subprocess::PIPE) do |p|
+ 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
+ sig {params(data: String).returns(String)}
def self.gpg_ascii_dearmor(data)
cmd = %w[gpg --batch --dearmor]
- out = nil
+ out = T.let(nil, T.nilable(String))
log.debug('+ ' + cmd.join(' '))
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
- stdout: Subprocess::PIPE) do |p|
+ stdout: Subprocess::PIPE) do |p|
out, _err = p.communicate(data)
end
- out
+ T.must(out)
end
# Whether to add the Base64 encoding to the generated document.
#
# @return [Boolean]
+ sig {returns(T::Boolean)}
def include_base64?
!!@include_base64
end
private
+ sig {returns(RQRCode::QRCode)}
def qr_code
@qr_code ||= qr_code!
end
+ sig {returns(RQRCode::QRCode)}
def qr_code!
log.info('Generating QR code')
# Base64 encode data prior to QR encoding as requested
if qr_base64
@@ -209,15 +262,17 @@
log.debug("qr_level: #{@qr_level.inspect}")
RQRCode::QRCode.new(input, level: @qr_level)
end
+ sig {returns(T::Array[String])}
def sixword_lines
log.info('Encoding with Sixword')
@sixword_lines ||=
Sixword.pad_encode_to_sentences(data).map(&:downcase)
end
+ sig {returns(String)}
def base64_content
log.debug('Encoding with Base64')
if encrypt
# If data is already GPG encrypted, use GPG's base64 armor
self.class.gpg_ascii_enarmor(data)