class Paperback::Preparer

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

Constants

PassChars

Attributes

data[R]
encrypt[R]
labels[R]
passphrase_file[R]
qr_base64[R]

Public Class Methods

gpg_ascii_dearmor(data) click to toggle source
# File lib/paperback/preparer.rb, line 162
def self.gpg_ascii_dearmor(data)
  cmd = %w[gpg --batch --dearmor]
  out = nil

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

  out
end
gpg_ascii_enarmor(data, strip_comments: true) click to toggle source
# File lib/paperback/preparer.rb, line 145
def self.gpg_ascii_enarmor(data, strip_comments: true)
  cmd = %w[gpg --batch --enarmor]
  out = nil

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

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

  out
end
gpg_encrypt(filename:, password:) click to toggle source
# File lib/paperback/preparer.rb, line 130
def self.gpg_encrypt(filename:, password:)
  cmd = %w[
    gpg -c -o - --batch --cipher-algo aes256 --passphrase-fd 0 --
  ] + [filename]
  out = nil

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

  out
end
log() click to toggle source
# File lib/paperback/preparer.rb, line 64
def self.log
  @log ||= Paperback.class_log(self)
end
new(filename:, encrypt: true, qr_base64: false, qr_level: nil, comment: nil, passphrase_file: nil, include_base64: true) click to toggle source
# File lib/paperback/preparer.rb, line 21
def initialize(filename:, encrypt: true, qr_base64: false, qr_level: nil,
               comment: nil, passphrase_file: nil, include_base64: true)

  log.debug('Preparer#initialize')

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

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

  @encrypt = encrypt

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

  @qr_base64 = qr_base64
  @qr_level = qr_level

  @passphrase_file = passphrase_file

  @include_base64 = !!include_base64

  @labels = {}
  @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
end
random_passphrase(entropy_bits: 256, char_set: PassChars) click to toggle source
# File lib/paperback/preparer.rb, line 118
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) click to toggle source

Compute a truncated SHA256 digest

# File lib/paperback/preparer.rb, line 126
def self.truncated_sha256(content)
  Digest::SHA256.hexdigest(content)[0...16]
end

Public Instance Methods

include_base64?() click to toggle source

Whether to add the Base64 encoding to the generated document.

@return [Boolean]

# File lib/paperback/preparer.rb, line 178
def include_base64?
  !!@include_base64
end
log() click to toggle source
# File lib/paperback/preparer.rb, line 61
def log
  @log ||= Paperback.class_log(self.class)
end
passphrase() click to toggle source
# File lib/paperback/preparer.rb, line 111
def passphrase
  raise "Can't have passphrase without encrypt" unless encrypt
  @passphrase ||= self.class.random_passphrase
end
render(output_filename:, extra_draw_opts: {}) click to toggle source
# File lib/paperback/preparer.rb, line 68
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(passphrase_file, File::CREAT|File::EXCL|File::WRONLY,
               0400) 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

Private Instance Methods

base64_content() click to toggle source
# File lib/paperback/preparer.rb, line 220
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)
  else
    # Otherwise do plain Base64
    Base64.encode64(data)
  end
end
qr_code() click to toggle source
# File lib/paperback/preparer.rb, line 184
def qr_code
  @qr_code ||= qr_code!
end
qr_code!() click to toggle source
# File lib/paperback/preparer.rb, line 188
def qr_code!
  log.info('Generating QR code')

  # Base64 encode data prior to QR encoding as requested
  if qr_base64
    input = base64_content
  else
    input = data
  end

  # If QR level not specified, pick highest level of redundancy possible
  # given the size of the input, up to Q (25% redundancy)
  unless @qr_level
    if input.bytesize <= 1663
      @qr_level = :q
    elsif input.bytesize <= 2331
      @qr_level = :m
    else
      @qr_level = :l
    end
  end

  log.debug("qr_level: #{@qr_level.inspect}")
  RQRCode::QRCode.new(input, level: @qr_level)
end
sixword_lines() click to toggle source
# File lib/paperback/preparer.rb, line 214
def sixword_lines
  log.info('Encoding with Sixword')
  @sixword_lines ||=
    Sixword.pad_encode_to_sentences(data).map(&:downcase)
end