paperback 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +3 -0
- data/.rubocop-disables.yml +133 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +24 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +676 -0
- data/README.md +38 -0
- data/Rakefile +15 -0
- data/bin/paperback +76 -0
- data/lib/paperback.rb +32 -0
- data/lib/paperback/cli.rb +12 -0
- data/lib/paperback/document.rb +174 -0
- data/lib/paperback/preparer.rb +206 -0
- data/lib/paperback/version.rb +5 -0
- data/paperback.gemspec +38 -0
- data/spec/spec_helper.rb +88 -0
- data/spec/unit/paperback_spec.rb +5 -0
- metadata +208 -0
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Paperback
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/paperback.svg)](https://rubygems.org/gems/paperback)
|
4
|
+
[![Build status](https://travis-ci.org/ab/paperback.svg)](https://travis-ci.org/ab/paperback)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/ab/paperback.svg)](https://codeclimate.com/github/ab/paperback)
|
6
|
+
[![Inline Docs](http://inch-ci.org/github/ab/paperback.svg?branch=master)](http://www.rubydoc.info/github/ab/paperback/master)
|
7
|
+
|
8
|
+
*Paperback* is a library that facilitates the creation of paper offline backups
|
9
|
+
of small amounts of important data, such as encryption keys.
|
10
|
+
|
11
|
+
It is designed to be used for long-term paper storage. Arbitrary data to be
|
12
|
+
backed up is encoded using QR codes and
|
13
|
+
[sixword](https://github.com/ab/sixword) English text.
|
14
|
+
|
15
|
+
By default, the backup data is GPG-encrypted with a symmetric passphrase to
|
16
|
+
avoid exposing data to the printer (or scanner, assuming you cover the
|
17
|
+
passphrase when scanning).
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Typical usage will be through the `paperback` executable.
|
22
|
+
|
23
|
+
```sh
|
24
|
+
# Back up the content in data.key
|
25
|
+
paperback data.key out.pdf
|
26
|
+
```
|
27
|
+
|
28
|
+
### More complex patterns
|
29
|
+
|
30
|
+
See the [YARD documentation](http://www.rubydoc.info/github/ab/paperback/master).
|
31
|
+
|
32
|
+
## Contributing
|
33
|
+
|
34
|
+
1. Fork it
|
35
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
36
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
37
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
38
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
task :default do
|
6
|
+
sh 'rake -T'
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new(:spec)
|
10
|
+
|
11
|
+
def alias_task(alias_task, original)
|
12
|
+
desc "Alias for rake #{original}"
|
13
|
+
task alias_task, Rake.application[original].arg_names => original
|
14
|
+
end
|
15
|
+
alias_task(:test, :spec)
|
data/bin/paperback
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$VERBOSE = true
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require_relative '../lib/paperback'
|
6
|
+
|
7
|
+
BaseName = File.basename($0)
|
8
|
+
|
9
|
+
def main
|
10
|
+
options = {}
|
11
|
+
|
12
|
+
optparse = OptionParser.new do |opts|
|
13
|
+
opts.banner = <<-EOM
|
14
|
+
usage: #{BaseName} [OPTION]... INPUT OUT_PDF
|
15
|
+
|
16
|
+
Create a printable PDF backup of INPUT file and write to OUT_PDF. The saved PDF
|
17
|
+
file will be suitable for printing. By default, the paper backup will contain
|
18
|
+
the input data encoded as a QR code for ease of scanning and as sixword-encoded
|
19
|
+
English text for more natural paper printing.
|
20
|
+
|
21
|
+
In order to prevent the input content from being exposed to the printer, by
|
22
|
+
default, the input content will be encrypted with a symmetric passphrase that
|
23
|
+
is echoed to the terminal. There will be a placeholder in the PDF where you can
|
24
|
+
manually add the passphrase by hand and pen.
|
25
|
+
|
26
|
+
For example:
|
27
|
+
|
28
|
+
# Create a backup of secret.key in backup.pdf
|
29
|
+
#{BaseName} secret.key backup.pdf
|
30
|
+
|
31
|
+
Options:
|
32
|
+
EOM
|
33
|
+
|
34
|
+
opts.on('-h', '--help', 'Display this message', ' ') do
|
35
|
+
STDERR.puts opts, ''
|
36
|
+
exit 0
|
37
|
+
end
|
38
|
+
opts.on('--version', 'Print version number', ' ') do
|
39
|
+
puts 'paperback ' + Paperback::VERSION
|
40
|
+
exit 0
|
41
|
+
end
|
42
|
+
opts.on('-v', '--verbose', 'Be more verbose', ' ') do
|
43
|
+
Paperback.log_level -= 1
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
|
48
|
+
opts.on('--no-encrypt', 'Skip encryption of input') do |val|
|
49
|
+
options[:encrypt] = val
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on('-c', '--comment TEXT', 'Add TEXT comment to output') do |val|
|
53
|
+
options[:comment] = val
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('--passphrase-out FILE',
|
57
|
+
'Write generated passphrase to FILE') do |val|
|
58
|
+
options[:passphrase_file] = val
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
optparse.parse!
|
63
|
+
|
64
|
+
case ARGV.length
|
65
|
+
when 2
|
66
|
+
options[:input] = ARGV.fetch(0)
|
67
|
+
options[:output] = ARGV.fetch(1)
|
68
|
+
else
|
69
|
+
STDERR.puts optparse, ''
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
|
73
|
+
Paperback::CLI.create_backup(**options)
|
74
|
+
end
|
75
|
+
|
76
|
+
main
|
data/lib/paperback.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
# Paperback is a library for creating paper backups of sensitive data.
|
4
|
+
module Paperback
|
5
|
+
def self.log
|
6
|
+
return @log if @log
|
7
|
+
@log = Logger.new(STDERR)
|
8
|
+
@log.progname = self.name
|
9
|
+
@log.level = log_level
|
10
|
+
@log
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.class_log(klass, stream=STDERR)
|
14
|
+
log = Logger.new(stream)
|
15
|
+
log.progname = klass.name
|
16
|
+
log.level = log_level
|
17
|
+
log
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.log_level
|
21
|
+
@log_level ||= Logger::INFO
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.log_level=(val)
|
25
|
+
@log_level = val
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
require_relative 'paperback/version'
|
30
|
+
require_relative 'paperback/cli'
|
31
|
+
require_relative 'paperback/document'
|
32
|
+
require_relative 'paperback/preparer'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Paperback
|
2
|
+
module CLI
|
3
|
+
def self.create_backup(input:, output:, encrypt: true, qr_base64: true,
|
4
|
+
qr_level: :l, comment: nil, passphrase_file: nil)
|
5
|
+
prep = Paperback::Preparer.new(filename: input, encrypt: encrypt,
|
6
|
+
qr_base64: qr_base64, qr_level: qr_level,
|
7
|
+
passphrase_file: passphrase_file,
|
8
|
+
comment: comment)
|
9
|
+
prep.render(output_filename: output)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'prawn'
|
2
|
+
|
3
|
+
# Main class for creating and rendering PDFs
|
4
|
+
module Paperback; class Document
|
5
|
+
attr_reader :pdf, :debug
|
6
|
+
|
7
|
+
def initialize(debug: false)
|
8
|
+
log.debug('Document#initialize')
|
9
|
+
@debug = debug
|
10
|
+
@pdf = Prawn::Document.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def log
|
14
|
+
@log ||= Paperback.class_log(self.class)
|
15
|
+
end
|
16
|
+
|
17
|
+
def render(output_file:, draw_opts:)
|
18
|
+
log.info('Rendering PDF')
|
19
|
+
|
20
|
+
# Create all the PDF content
|
21
|
+
draw_paperback(**draw_opts)
|
22
|
+
|
23
|
+
# Render to output file
|
24
|
+
log.info("Writing PDF to #{output_file.inspect}")
|
25
|
+
pdf.render_file(output_file)
|
26
|
+
end
|
27
|
+
|
28
|
+
# High level method to draw the paperback content on the pdf document
|
29
|
+
def draw_paperback(qr_code:, sixword_lines:, sixword_bytes:,
|
30
|
+
labels:, passphrase_sha: nil, passphrase_len: nil)
|
31
|
+
unless qr_code.is_a?(RQRCode::QRCode)
|
32
|
+
raise ArgumentError.new('qr_code must be RQRCode::QRCode')
|
33
|
+
end
|
34
|
+
|
35
|
+
# Header & QR code page
|
36
|
+
pdf.font('Times-Roman')
|
37
|
+
|
38
|
+
debug_draw_axes
|
39
|
+
|
40
|
+
draw_header(labels: labels, passphrase_sha: passphrase_sha,
|
41
|
+
passphrase_len: passphrase_len)
|
42
|
+
|
43
|
+
add_newline
|
44
|
+
|
45
|
+
draw_qr_code(qr_modules: qr_code.modules)
|
46
|
+
|
47
|
+
pdf.stroke_color '000000'
|
48
|
+
pdf.fill_color '000000'
|
49
|
+
|
50
|
+
# Sixword page
|
51
|
+
|
52
|
+
pdf.start_new_page
|
53
|
+
|
54
|
+
draw_sixword(lines: sixword_lines, sixword_bytes: sixword_bytes)
|
55
|
+
|
56
|
+
pdf.number_pages('<page> of <total>', align: :right,
|
57
|
+
at: [pdf.bounds.right - 100, -2])
|
58
|
+
end
|
59
|
+
|
60
|
+
# If in debug mode, draw axes on the page to assist with layout
|
61
|
+
def debug_draw_axes
|
62
|
+
return unless debug
|
63
|
+
pdf.float { pdf.stroke_axis }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Move cursor down by one line
|
67
|
+
def add_newline
|
68
|
+
pdf.move_down(pdf.font_size)
|
69
|
+
end
|
70
|
+
|
71
|
+
def draw_header(labels:, passphrase_sha:, passphrase_len:,
|
72
|
+
repo_url: 'https://github.com/ab/paperback')
|
73
|
+
|
74
|
+
intro = [
|
75
|
+
"This is a paper backup produced by `paperback`. ",
|
76
|
+
"<u><link href='#{repo_url}'>#{repo_url}</link></u>",
|
77
|
+
].join
|
78
|
+
pdf.text(intro, inline_format: true)
|
79
|
+
add_newline
|
80
|
+
|
81
|
+
label_pad = labels.keys.map(&:length).max + 1
|
82
|
+
|
83
|
+
unless passphrase_sha && passphrase_len
|
84
|
+
labels['Encrypted'] = 'no'
|
85
|
+
end
|
86
|
+
|
87
|
+
pdf.font('Courier') do
|
88
|
+
labels.each_pair do |k, v|
|
89
|
+
pdf.text("#{(k + ':').ljust(label_pad)} #{v}")
|
90
|
+
end
|
91
|
+
|
92
|
+
if passphrase_sha
|
93
|
+
pdf.text("SHA256(passphrase)[0...16]: #{passphrase_sha}")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
add_newline
|
98
|
+
|
99
|
+
if passphrase_len
|
100
|
+
pdf.font('Helvetica') do
|
101
|
+
pdf.font_size(12.8) do
|
102
|
+
pdf.text('Passphrase: ' + '_ ' * passphrase_len)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
pdf.move_down(8)
|
107
|
+
pdf.indent(72) do
|
108
|
+
pdf.text('Be sure to cover the passphrase when scanning the QR code!')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# @param [Array<String>] lines An array of sixword sentences to print
|
114
|
+
# @param [Integer] columns The number of text columns on the page
|
115
|
+
# @param [Integer] hunks_per_row The number of 6-word sentences per line
|
116
|
+
# @param [Integer] sixword_bytes Bytesize of the sixword encoded data
|
117
|
+
def draw_sixword(lines:, sixword_bytes:, columns: 3, hunks_per_row: 1)
|
118
|
+
debug_draw_axes
|
119
|
+
|
120
|
+
numbered = lines.each_slice(hunks_per_row).each_with_index.map { |row, i|
|
121
|
+
"#{i * hunks_per_row + 1}: #{row.map(&:strip).join('. ')}"
|
122
|
+
}
|
123
|
+
|
124
|
+
header = [
|
125
|
+
"This sixword text encodes #{sixword_bytes} bytes in #{lines.length}",
|
126
|
+
" six-word sentences.",
|
127
|
+
" Decode with `sixword -d`"
|
128
|
+
].join
|
129
|
+
|
130
|
+
pdf.font('Times-Roman') do
|
131
|
+
pdf.text(header)
|
132
|
+
add_newline
|
133
|
+
end
|
134
|
+
|
135
|
+
pdf.column_box([0, pdf.cursor], columns: columns, width: pdf.bounds.width) do
|
136
|
+
pdf.font('Times-Roman') do
|
137
|
+
pdf.font_size(11) do
|
138
|
+
pdf.text(numbered.join("\n"))
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def draw_qr_code(qr_modules:)
|
145
|
+
qr_height = pdf.cursor # entire rest of page
|
146
|
+
qr_width = pdf.bounds.width # entire page width
|
147
|
+
|
148
|
+
# number of modules plus 2 for quiet area
|
149
|
+
qr_code_size = qr_modules.length + 2
|
150
|
+
|
151
|
+
pixel_height = qr_height / qr_code_size
|
152
|
+
pixel_width = qr_width / qr_code_size
|
153
|
+
|
154
|
+
pdf.bounding_box([0, pdf.cursor], width: qr_width, height: qr_height) do
|
155
|
+
if debug
|
156
|
+
pdf.stroke_color('888888')
|
157
|
+
pdf.stroke_bounds
|
158
|
+
end
|
159
|
+
|
160
|
+
qr_modules.each do |row|
|
161
|
+
pdf.move_down(pixel_height)
|
162
|
+
|
163
|
+
row.each_with_index do |pixel_val, col_i|
|
164
|
+
pdf.stroke do
|
165
|
+
pdf.stroke_color(pixel_val ? '000000' : 'ffffff')
|
166
|
+
pdf.fill_color(pixel_val ? '000000' : 'ffffff')
|
167
|
+
xy = [(col_i + 1) * pixel_width, pdf.cursor]
|
168
|
+
pdf.fill_and_stroke_rectangle(xy, pixel_width, pixel_height)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end; end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'digest/sha2'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
require 'rqrcode'
|
8
|
+
require 'sixword'
|
9
|
+
require 'subprocess'
|
10
|
+
|
11
|
+
module Paperback
|
12
|
+
# Class wrapping functions to prepare data for paperback storage, including
|
13
|
+
# QR code and sixword encoding.
|
14
|
+
class Preparer
|
15
|
+
attr_reader :data
|
16
|
+
attr_reader :labels
|
17
|
+
attr_reader :qr_base64
|
18
|
+
attr_reader :encrypt
|
19
|
+
attr_reader :passphrase_file
|
20
|
+
|
21
|
+
def initialize(filename:, encrypt: true, qr_base64: false, qr_level: nil,
|
22
|
+
comment: nil, passphrase_file: nil)
|
23
|
+
|
24
|
+
log.debug('Preparer#initialize')
|
25
|
+
|
26
|
+
log.info("Reading #{filename.inspect}")
|
27
|
+
plain_data = File.read(filename)
|
28
|
+
|
29
|
+
log.debug("Read #{plain_data.bytesize} bytes")
|
30
|
+
|
31
|
+
@encrypt = encrypt
|
32
|
+
|
33
|
+
if encrypt
|
34
|
+
@data = self.class.gpg_encrypt(filename: filename, password: passphrase)
|
35
|
+
else
|
36
|
+
@data = plain_data
|
37
|
+
end
|
38
|
+
@sha256 = Digest::SHA256.hexdigest(plain_data)
|
39
|
+
|
40
|
+
@qr_base64 = qr_base64
|
41
|
+
@qr_level = qr_level
|
42
|
+
|
43
|
+
@passphrase_file = passphrase_file
|
44
|
+
|
45
|
+
@labels = {}
|
46
|
+
@labels['Filename'] = filename
|
47
|
+
@labels['Backed up'] = Time.now.to_s
|
48
|
+
|
49
|
+
stat = File.stat(filename)
|
50
|
+
@labels['Mtime'] = stat.mtime
|
51
|
+
@labels['Bytes'] = plain_data.bytesize
|
52
|
+
@labels['Comment'] = comment if comment
|
53
|
+
|
54
|
+
@labels['SHA256'] = Digest::SHA256.hexdigest(plain_data)
|
55
|
+
|
56
|
+
@document = Paperback::Document.new
|
57
|
+
end
|
58
|
+
|
59
|
+
def log
|
60
|
+
@log ||= Paperback.class_log(self.class)
|
61
|
+
end
|
62
|
+
def self.log
|
63
|
+
@log ||= Paperback.class_log(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def render(output_filename:)
|
67
|
+
log.debug('Preparer#render')
|
68
|
+
|
69
|
+
opts = {
|
70
|
+
labels: labels,
|
71
|
+
qr_code: qr_code,
|
72
|
+
sixword_lines: sixword_lines,
|
73
|
+
sixword_bytes: data.bytesize,
|
74
|
+
}
|
75
|
+
|
76
|
+
if encrypt
|
77
|
+
opts[:passphrase_sha] = self.class.truncated_sha256(passphrase)
|
78
|
+
opts[:passphrase_len] = passphrase.length
|
79
|
+
if passphrase_file
|
80
|
+
File.open(passphrase_file, File::CREAT|File::EXCL|File::WRONLY,
|
81
|
+
0400) do |f|
|
82
|
+
f.write(passphrase)
|
83
|
+
end
|
84
|
+
log.info("Wrote passphrase to #{passphrase_file.inspect}")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
@document.render(output_file: output_filename, draw_opts: opts)
|
89
|
+
|
90
|
+
log.info('Render complete')
|
91
|
+
|
92
|
+
if encrypt && !passphrase_file
|
93
|
+
puts "Passphrase: #{passphrase}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def passphrase
|
98
|
+
raise "Can't have passphrase without encrypt" unless encrypt
|
99
|
+
@passphrase ||= self.class.random_passphrase
|
100
|
+
end
|
101
|
+
|
102
|
+
PassChars = [*'a'..'z', *'A'..'Z', *'0'..'9'].freeze
|
103
|
+
|
104
|
+
def self.random_passphrase(entropy_bits: 256, char_set: PassChars)
|
105
|
+
chars_needed = (entropy_bits / Math.log2(char_set.length)).ceil
|
106
|
+
(0...chars_needed).map {
|
107
|
+
PassChars.fetch(SecureRandom.random_number(char_set.length))
|
108
|
+
}.join
|
109
|
+
end
|
110
|
+
|
111
|
+
# Compute a truncated SHA256 digest
|
112
|
+
def self.truncated_sha256(content)
|
113
|
+
Digest::SHA256.hexdigest(content)[0...16]
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.gpg_encrypt(filename:, password:)
|
117
|
+
cmd = %w[
|
118
|
+
gpg -c -o - --batch --cipher-algo aes256 --passphrase-fd 0 --
|
119
|
+
] + [filename]
|
120
|
+
out = nil
|
121
|
+
|
122
|
+
log.debug('+ ' + cmd.join(' '))
|
123
|
+
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
|
124
|
+
stdout: Subprocess::PIPE) do |p|
|
125
|
+
out, _err = p.communicate(password)
|
126
|
+
end
|
127
|
+
|
128
|
+
out
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.gpg_ascii_enarmor(data, strip_comments: true)
|
132
|
+
cmd = %w[gpg --batch --enarmor]
|
133
|
+
out = nil
|
134
|
+
|
135
|
+
log.debug('+ ' + cmd.join(' '))
|
136
|
+
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
|
137
|
+
stdout: Subprocess::PIPE) do |p|
|
138
|
+
out, _err = p.communicate(data)
|
139
|
+
end
|
140
|
+
|
141
|
+
if strip_comments
|
142
|
+
out = out.each_line.select { |l| !l.start_with?('Comment: ') }.join
|
143
|
+
end
|
144
|
+
|
145
|
+
out
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.gpg_ascii_dearmor(data)
|
149
|
+
cmd = %w[gpg --batch --dearmor]
|
150
|
+
out = nil
|
151
|
+
|
152
|
+
log.debug('+ ' + cmd.join(' '))
|
153
|
+
Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
|
154
|
+
stdout: Subprocess::PIPE) do |p|
|
155
|
+
out, _err = p.communicate(data)
|
156
|
+
end
|
157
|
+
|
158
|
+
out
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def qr_code
|
164
|
+
@qr_code ||= qr_code!
|
165
|
+
end
|
166
|
+
|
167
|
+
def qr_code!
|
168
|
+
log.info('Generating QR code')
|
169
|
+
|
170
|
+
# Base64 encode data prior to QR encoding as requested
|
171
|
+
if qr_base64
|
172
|
+
if encrypt
|
173
|
+
# If data is already GPG encrypted, use GPG's base64 armor
|
174
|
+
input = self.class.gpg_ascii_enarmor(data)
|
175
|
+
else
|
176
|
+
# Otherwise do plain Base64
|
177
|
+
input = Base64.encode64(data)
|
178
|
+
end
|
179
|
+
else
|
180
|
+
input = data
|
181
|
+
end
|
182
|
+
|
183
|
+
# If QR level not specified, pick highest level of redundancy possible
|
184
|
+
# given the size of the input, up to Q (25% redundancy)
|
185
|
+
unless @qr_level
|
186
|
+
if input.bytesize <= 1663
|
187
|
+
@qr_level = :q
|
188
|
+
elsif input.bytesize <= 2331
|
189
|
+
@qr_level = :m
|
190
|
+
else
|
191
|
+
@qr_level = :l
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
log.debug("qr_level: #{@qr_level.inspect}")
|
196
|
+
RQRCode::QRCode.new(input, level: @qr_level)
|
197
|
+
end
|
198
|
+
|
199
|
+
def sixword_lines
|
200
|
+
log.info('Encoding with Sixword')
|
201
|
+
@sixword_lines ||=
|
202
|
+
Sixword.pad_encode_to_sentences(data).map(&:downcase)
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|