sixword 0.3.1 → 0.3.2
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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop-disables.yml +174 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +40 -0
- data/README.md +66 -3
- data/Rakefile +0 -1
- data/bin/sixword +10 -1
- data/lib/sixword.rb +174 -11
- data/lib/sixword/cli.rb +34 -3
- data/lib/sixword/hex.rb +62 -1
- data/lib/sixword/lib.rb +92 -9
- data/lib/sixword/version.rb +2 -1
- data/lib/sixword/words.rb +1 -0
- data/sixword.gemspec +4 -1
- data/spec/sixword/hex_spec.rb +13 -14
- data/spec/sixword/lib_spec.rb +6 -8
- data/spec/sixword_spec.rb +13 -14
- data/spec/spec_helper.rb +97 -0
- data/spec/test_vectors.rb +1 -1
- metadata +51 -5
- data/spec/rspec_helper.rb +0 -10
data/lib/sixword/cli.rb
CHANGED
@@ -1,9 +1,33 @@
|
|
1
1
|
module Sixword
|
2
|
+
|
3
|
+
# The Sixword::CLI class implements all of the complex processing needed for
|
4
|
+
# the sixword Command Line Interface.
|
2
5
|
class CLI
|
6
|
+
|
7
|
+
# Exception for certain input validation errors
|
3
8
|
class CLIError < StandardError; end
|
4
9
|
|
5
|
-
|
10
|
+
# @return [String] Input filename
|
11
|
+
attr_reader :filename
|
12
|
+
|
13
|
+
# @return [Hash] Options hash
|
14
|
+
attr_reader :options
|
15
|
+
|
16
|
+
# @return [File, IO] Stream opened from #filename
|
17
|
+
attr_reader :stream
|
18
|
+
|
19
|
+
# @return [:encode, :decode]
|
20
|
+
attr_reader :mode
|
6
21
|
|
22
|
+
# Create a Sixword CLI to operate on filename with options
|
23
|
+
#
|
24
|
+
# @param filename [String] Input file name (or '-' for stdin)
|
25
|
+
# @param options [Hash]
|
26
|
+
#
|
27
|
+
# @option options [:encode, :decode] :mode (:encode)
|
28
|
+
# @option options [Boolean] :pad (false)
|
29
|
+
# @option options [String] :hex_style
|
30
|
+
#
|
7
31
|
def initialize(filename, options)
|
8
32
|
@filename = filename
|
9
33
|
@options = {mode: :encode, pad: false}.merge(options)
|
@@ -24,18 +48,25 @@ module Sixword
|
|
24
48
|
end
|
25
49
|
end
|
26
50
|
|
51
|
+
# Return the value of the :pad option.
|
52
|
+
# @return [Boolean]
|
27
53
|
def pad?
|
28
54
|
options.fetch(:pad)
|
29
55
|
end
|
30
56
|
|
57
|
+
# Return true if we are in encoding mode, false otherwise (decoding).
|
58
|
+
# @return [Boolean]
|
31
59
|
def encoding?
|
32
60
|
mode == :encode
|
33
61
|
end
|
34
62
|
|
63
|
+
# Return the value of the :hex_style option.
|
64
|
+
# @return [String, nil]
|
35
65
|
def hex_style
|
36
66
|
options[:hex_style]
|
37
67
|
end
|
38
68
|
|
69
|
+
# Format data as hex in various styles.
|
39
70
|
def print_hex(data, chunk_index, cols=80)
|
40
71
|
case hex_style
|
41
72
|
when 'lower', 'lowercase'
|
@@ -59,6 +90,7 @@ module Sixword
|
|
59
90
|
end
|
60
91
|
end
|
61
92
|
|
93
|
+
# Run the encoding/decoding operation, printing the result to stdout.
|
62
94
|
def run!
|
63
95
|
if encoding?
|
64
96
|
do_encode! do |encoded|
|
@@ -87,7 +119,6 @@ module Sixword
|
|
87
119
|
raise ArgumentError.new("block is required")
|
88
120
|
end
|
89
121
|
|
90
|
-
arr = []
|
91
122
|
read_input_by_6_words do |arr|
|
92
123
|
yield Sixword.decode(arr, padding_ok: pad?)
|
93
124
|
end
|
@@ -140,7 +171,7 @@ module Sixword
|
|
140
171
|
while true
|
141
172
|
buf = stream.read(block_size)
|
142
173
|
if buf.nil?
|
143
|
-
break #EOF
|
174
|
+
break # EOF
|
144
175
|
end
|
145
176
|
|
146
177
|
buf.scan(/\S+/) do |word|
|
data/lib/sixword/hex.rb
CHANGED
@@ -1,12 +1,24 @@
|
|
1
1
|
module Sixword
|
2
|
+
# Various hexadecimal string encoding and decoding functions
|
2
3
|
module Hex
|
3
4
|
HexValid = /\A[a-fA-F0-9]+\z/
|
4
5
|
HexStrip = /[\s:.-]+/
|
5
6
|
|
7
|
+
# Return whether string is entirely hexadecimal.
|
8
|
+
# @param string [String]
|
9
|
+
# @return [Boolean]
|
10
|
+
# @see [HexValid]
|
11
|
+
#
|
6
12
|
def self.valid_hex?(string)
|
7
13
|
!!(string =~ HexValid)
|
8
14
|
end
|
9
15
|
|
16
|
+
# Return whether single character string is one of the fill characters that
|
17
|
+
# are OK to strip from a hexadecimal string.
|
18
|
+
# @param char [String] String of length == 1
|
19
|
+
# @return [Boolean]
|
20
|
+
# @see [HexStrip]
|
21
|
+
#
|
10
22
|
def self.strip_char?(char)
|
11
23
|
unless char.length == 1
|
12
24
|
raise ArgumentError.new("Must pass single character string")
|
@@ -14,22 +26,71 @@ module Sixword
|
|
14
26
|
!!(char =~ HexStrip)
|
15
27
|
end
|
16
28
|
|
29
|
+
# Encode a byte string as hexadecimal.
|
30
|
+
#
|
31
|
+
# @param bytes [String]
|
32
|
+
# @return [String] hexadecimal string
|
33
|
+
#
|
17
34
|
def self.encode(bytes)
|
18
35
|
bytes.unpack('H*').fetch(0)
|
19
36
|
end
|
20
37
|
|
38
|
+
# Encode a byte string as hexadecimal, returning it in slices joined by a
|
39
|
+
# delimiter. This is useful for generating colon or space separated strings
|
40
|
+
# like those commonly used in fingerprints.
|
41
|
+
#
|
42
|
+
# @param bytes [String]
|
43
|
+
# @param slice [Integer]
|
44
|
+
# @param delimiter [String]
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
# >> encode_slice("9T]B\xF0\x039\xFF", 2, ':')
|
50
|
+
# => "39:54:5d:42:f0:03:39:ff"
|
51
|
+
#
|
21
52
|
def self.encode_slice(bytes, slice, delimiter)
|
22
53
|
encode(bytes).each_char.each_slice(slice).map(&:join).join(delimiter)
|
23
54
|
end
|
24
55
|
|
56
|
+
# Encode a byte string as a GPG style fingerprint: uppercase in slices of 4
|
57
|
+
# separated by spaces.
|
58
|
+
#
|
59
|
+
# @param bytes [String]
|
60
|
+
# @return [String]
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# >> encode_fingerprint("9T]B\xF0\x039\xFF")
|
64
|
+
# => "3954 5D42 F003 39FF"
|
65
|
+
#
|
25
66
|
def self.encode_fingerprint(bytes)
|
26
67
|
encode_slice(bytes, 4, ' ').upcase
|
27
68
|
end
|
28
69
|
|
70
|
+
# Encode a byte string in hex with colons: lowercase in slices of 2
|
71
|
+
# separated by colons.
|
72
|
+
#
|
73
|
+
# @param bytes [String]
|
74
|
+
# @return [String]
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# >> encode_colons("9T]B\xF0\x039\xFF")
|
78
|
+
# => "39:54:5d:42:f0:03:39:ff"
|
79
|
+
#
|
80
|
+
#
|
29
81
|
def self.encode_colons(bytes)
|
30
82
|
encode_slice(bytes, 2, ':')
|
31
83
|
end
|
32
84
|
|
85
|
+
# Decode a hexadecimal string to a byte string.
|
86
|
+
#
|
87
|
+
# @param hex_string [String]
|
88
|
+
# @param strip_chars [Boolean] Whether to accept and strip whitespace and
|
89
|
+
# other delimiters (see {HexStrip})
|
90
|
+
# @return [String]
|
91
|
+
#
|
92
|
+
# @raise ArgumentError on invalid hex input
|
93
|
+
#
|
33
94
|
def self.decode(hex_string, strip_chars=true)
|
34
95
|
if strip_chars
|
35
96
|
hex_string = hex_string.gsub(HexStrip, '')
|
@@ -39,7 +100,7 @@ module Sixword
|
|
39
100
|
raise ArgumentError.new("Invalid value for hex: #{hex_string.inspect}")
|
40
101
|
end
|
41
102
|
|
42
|
-
unless hex_string.length
|
103
|
+
unless hex_string.length.even?
|
43
104
|
raise ArgumentError.new("Odd length hex: #{hex_string.inspect}")
|
44
105
|
end
|
45
106
|
|
data/lib/sixword/lib.rb
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
module Sixword
|
2
|
+
|
3
|
+
# The Lib module contains various internal utility functions. They are not
|
4
|
+
# really part of the public API and will probably not be useful to external
|
5
|
+
# callers.
|
2
6
|
module Lib
|
7
|
+
|
8
|
+
# Encode an array of 8 bytes as an array of 6 words.
|
9
|
+
#
|
10
|
+
# @param byte_array [Array<Fixnum>] An array of length 8 containing
|
11
|
+
# integers in 0..255
|
12
|
+
#
|
13
|
+
# @return [Array<String>] An array of length 6 containing String words from
|
14
|
+
# {Sixword::WORDS}
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# >> Sixword::Lib.encode_64_bits([0] * 8)
|
18
|
+
# => ["A", "A", "A", "A", "A", "A"]
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# >> Sixword::Lib.encode_64_bits([0xff] * 8)
|
22
|
+
# => ["YOKE", "YOKE", "YOKE", "YOKE", "YOKE", "YEAR"]
|
23
|
+
#
|
3
24
|
def self.encode_64_bits(byte_array)
|
4
25
|
unless byte_array.length == 8
|
5
26
|
raise ArgumentError.new("Must pass an 8-byte array")
|
@@ -23,6 +44,22 @@ module Sixword
|
|
23
44
|
encoded
|
24
45
|
end
|
25
46
|
|
47
|
+
# Decode an array of 6 words into a 64-bit integer (representing 8 bytes).
|
48
|
+
#
|
49
|
+
# @param word_array [Array<String>] A 6 element array of String words
|
50
|
+
# @param padding_ok [Boolean]
|
51
|
+
#
|
52
|
+
# @return [Array(Integer, Integer)] a 64-bit integer (the data) and the
|
53
|
+
# length of the byte array that it represents (will always be 8 unless
|
54
|
+
# padding_ok)
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# >> Sixword::Lib.decode_6_words(%w{COAT ACHE A A A ACT6}, true)
|
58
|
+
# => [26729, 2]
|
59
|
+
#
|
60
|
+
# >> Sixword::Lib.decode_6_words(%w{ACRE ADEN INN SLID MAD PAP}, false)
|
61
|
+
# => [5217737340628397156, 8]
|
62
|
+
#
|
26
63
|
def self.decode_6_words(word_array, padding_ok)
|
27
64
|
unless word_array.length == 6
|
28
65
|
raise ArgumentError.new("Must pass a six-word array")
|
@@ -68,7 +105,15 @@ module Sixword
|
|
68
105
|
[int, 8 - padding]
|
69
106
|
end
|
70
107
|
|
71
|
-
# Extract the padding from a word
|
108
|
+
# Extract the numeric padding from a word.
|
109
|
+
#
|
110
|
+
# @param word [String]
|
111
|
+
# @return [Array(String, Integer)] The String word, the Integer padding
|
112
|
+
#
|
113
|
+
# @example
|
114
|
+
# >> Sixword::Lib.extract_padding("WORD3")
|
115
|
+
# => ["WORD", 3]
|
116
|
+
#
|
72
117
|
def self.extract_padding(word)
|
73
118
|
unless word[-1] =~ /[1-7]/
|
74
119
|
raise ArgumentError.new("Not a valid padded word: #{word.inspect}")
|
@@ -77,11 +122,34 @@ module Sixword
|
|
77
122
|
return word[0...-1], Integer(word[-1])
|
78
123
|
end
|
79
124
|
|
125
|
+
# Decode an array of 6 words into a String of bytes.
|
126
|
+
#
|
127
|
+
# @param word_array [Array<String>] A 6 element array of String words
|
128
|
+
# @param padding_ok [Boolean]
|
129
|
+
#
|
130
|
+
# @return [String]
|
131
|
+
#
|
132
|
+
# @see Sixword.decode_6_words
|
133
|
+
# @see Sixword.int_to_byte_array
|
134
|
+
#
|
135
|
+
# @example
|
136
|
+
# >> Lib.decode_6_words_to_bstring(%w{COAT ACHE A A A ACT6}, true)
|
137
|
+
# => "hi"
|
138
|
+
#
|
139
|
+
# >> Lib.decode_6_words_to_bstring(%w{ACRE ADEN INN SLID MAD PAP}, false)
|
140
|
+
# => "Hi world"
|
141
|
+
#
|
80
142
|
def self.decode_6_words_to_bstring(word_array, padding_ok)
|
81
143
|
int_to_byte_array(*decode_6_words(word_array, padding_ok)).
|
82
144
|
map(&:chr).join
|
83
145
|
end
|
84
146
|
|
147
|
+
# Given a word, return the 11 bits it represents as an integer (i.e. its
|
148
|
+
# index in the WORDS list).
|
149
|
+
#
|
150
|
+
# @param word [String]
|
151
|
+
# @return [Fixnum] An integer 0..2047
|
152
|
+
#
|
85
153
|
def self.word_to_bits(word)
|
86
154
|
word = word.upcase
|
87
155
|
return WORDS_HASH.fetch(word)
|
@@ -94,6 +162,13 @@ module Sixword
|
|
94
162
|
end
|
95
163
|
|
96
164
|
# Compute two-bit parity on a byte array by summing each pair of bits.
|
165
|
+
# TODO: figure out which is faster
|
166
|
+
#
|
167
|
+
# @param byte_array [Array<Fixnum>]
|
168
|
+
# @return [Fixnum] An integer 0..3
|
169
|
+
#
|
170
|
+
# @see parity_int
|
171
|
+
#
|
97
172
|
def self.parity_array(byte_array)
|
98
173
|
|
99
174
|
# sum pairs of bits through the whole array
|
@@ -109,7 +184,15 @@ module Sixword
|
|
109
184
|
parity & 0b11
|
110
185
|
end
|
111
186
|
|
112
|
-
# Compute parity
|
187
|
+
# Compute two-bit parity on a 64-bit integer representing an 8-byte array
|
188
|
+
# by summing each pair of bits.
|
189
|
+
# TODO: figure out which is faster
|
190
|
+
#
|
191
|
+
# @param int [Integer] A 64-bit integer representing 8 bytes
|
192
|
+
# @return [Fixnum] An integer 0..3
|
193
|
+
#
|
194
|
+
# @see parity_array
|
195
|
+
#
|
113
196
|
def self.parity_int(int)
|
114
197
|
parity = 0
|
115
198
|
while int > 0
|
@@ -122,14 +205,14 @@ module Sixword
|
|
122
205
|
|
123
206
|
# Given an array of bytes, pack them into a single Integer.
|
124
207
|
#
|
125
|
-
#
|
208
|
+
# @example
|
126
209
|
#
|
127
210
|
# >> byte_array_to_int([1, 2])
|
128
211
|
# => 258
|
129
212
|
#
|
130
213
|
# @param byte_array [Array<Fixnum>]
|
131
214
|
#
|
132
|
-
# @return Integer
|
215
|
+
# @return [Integer]
|
133
216
|
#
|
134
217
|
def self.byte_array_to_int(byte_array)
|
135
218
|
int = 0
|
@@ -142,19 +225,19 @@ module Sixword
|
|
142
225
|
|
143
226
|
# Given an Integer, unpack it into an array of bytes.
|
144
227
|
#
|
145
|
-
#
|
146
|
-
#
|
228
|
+
# @example
|
147
229
|
# >> int_to_byte_array(258)
|
148
230
|
# => [1, 2]
|
149
231
|
#
|
232
|
+
# @example
|
150
233
|
# >> int_to_byte_array(258, 3)
|
151
234
|
# => [0, 1, 2]
|
152
235
|
#
|
153
236
|
# @param int [Integer]
|
154
|
-
# @param length [Integer]
|
155
|
-
#
|
237
|
+
# @param length [Integer] Left zero padded size of byte array to return. If
|
238
|
+
# not provided, no leading zeroes will be added.
|
156
239
|
#
|
157
|
-
# @return Array<Fixnum>
|
240
|
+
# @return [Array<Fixnum>]
|
158
241
|
#
|
159
242
|
def self.int_to_byte_array(int, length=nil)
|
160
243
|
unless int >= 0
|
data/lib/sixword/version.rb
CHANGED
data/lib/sixword/words.rb
CHANGED
data/sixword.gemspec
CHANGED
@@ -28,8 +28,11 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.require_paths = ['lib']
|
29
29
|
|
30
30
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
31
|
+
spec.add_development_dependency 'pry'
|
31
32
|
spec.add_development_dependency 'rake'
|
32
|
-
spec.add_development_dependency 'rspec'
|
33
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
34
|
+
spec.add_development_dependency 'rubocop', '~> 0'
|
35
|
+
spec.add_development_dependency 'yard'
|
33
36
|
|
34
37
|
spec.required_ruby_version = '>= 1.9.3'
|
35
38
|
end
|
data/spec/sixword/hex_spec.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# coding: binary
|
2
|
-
require_relative '../rspec_helper'
|
3
2
|
|
4
|
-
describe Sixword::Hex do
|
3
|
+
RSpec.describe Sixword::Hex do
|
5
4
|
TestCases = {
|
6
5
|
"\x73\xe2\x16\xb5\x36\x3f\x23\x77" => [
|
7
6
|
"73e216b5363f2377",
|
@@ -88,38 +87,38 @@ describe Sixword::Hex do
|
|
88
87
|
|
89
88
|
it 'should decode and encode random hex strings correctly' do
|
90
89
|
TestCases.each do |binary, hexes|
|
91
|
-
lower,
|
92
|
-
Sixword::Hex.encode(binary).
|
93
|
-
Sixword::Hex.decode(lower).
|
90
|
+
lower, _finger, _colons = hexes
|
91
|
+
expect(Sixword::Hex.encode(binary)).to eq(lower)
|
92
|
+
expect(Sixword::Hex.decode(lower)).to eq(binary)
|
94
93
|
end
|
95
94
|
end
|
96
95
|
|
97
96
|
it 'should decode and encode random hex fingerprints correctly' do
|
98
97
|
TestCases.each do |binary, hexes|
|
99
|
-
|
100
|
-
Sixword::Hex.encode_fingerprint(binary).
|
101
|
-
Sixword::Hex.decode(finger).
|
98
|
+
_lower, finger, _colons = hexes
|
99
|
+
expect(Sixword::Hex.encode_fingerprint(binary)).to eq(finger)
|
100
|
+
expect(Sixword::Hex.decode(finger)).to eq(binary)
|
102
101
|
end
|
103
102
|
end
|
104
103
|
|
105
104
|
it 'should decode and encode random colon hexes correctly' do
|
106
105
|
TestCases.each do |binary, hexes|
|
107
|
-
|
108
|
-
Sixword::Hex.encode_colons(binary).
|
109
|
-
Sixword::Hex.decode(colons).
|
106
|
+
_lower, _finger, colons = hexes
|
107
|
+
expect(Sixword::Hex.encode_colons(binary)).to eq(colons)
|
108
|
+
expect(Sixword::Hex.decode(colons)).to eq(binary)
|
110
109
|
end
|
111
110
|
end
|
112
111
|
|
113
112
|
it 'should accept all valid hex characters' do
|
114
|
-
Sixword::Hex.valid_hex?('abcdefABCDEF0123456789').
|
113
|
+
expect(Sixword::Hex.valid_hex?('abcdefABCDEF0123456789')).to eq(true)
|
115
114
|
end
|
116
115
|
|
117
116
|
it 'should reject invalid hex characters' do
|
118
117
|
('g'..'z').each do |c|
|
119
|
-
Sixword::Hex.valid_hex?(c).
|
118
|
+
expect(Sixword::Hex.valid_hex?(c)).to eq(false)
|
120
119
|
end
|
121
120
|
('G'..'Z').each do |c|
|
122
|
-
Sixword::Hex.valid_hex?(c).
|
121
|
+
expect(Sixword::Hex.valid_hex?(c)).to eq(false)
|
123
122
|
end
|
124
123
|
end
|
125
124
|
end
|