sixword 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|