rotp 4.1.0 → 5.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72a52a1f0f26257e83977969144abf6324fee40c65ef4f6b3b910d30c3bb1e36
4
- data.tar.gz: 212a5ca91186490c07221f7f17e4d39cc05778b89ebf22cf53712de2cfb8944a
3
+ metadata.gz: 592d8529aae80f05496b4f96d5888bbffd27f3afd92f2bad98b25a6b8046eee4
4
+ data.tar.gz: fbc30ba953e373b330df098c2ade00a2bac6cd635d8d488ceaafd3456f41f847
5
5
  SHA512:
6
- metadata.gz: 41b36b57154571a35d8d3f59961c3f1754c2bfb3bf06e2a29e2360b24ba884e7d632f86d3d5491341034a30654ba971d96bfcfecae85687a701230cf8e4523ba
7
- data.tar.gz: d70217238c2d859b674f9cc702e9ffcb4fcb5cdf3a75d9b40306d0dee8f28198363d6001510e2cbc2b507d24cb753127d602b6dec211da77287d7ac04fabef98
6
+ metadata.gz: a9e61bbec32b47a1c52c781186dc1d2ef60be00242b70e6ce4c151bf9f4e964cbd402f4e1a0b5f2cddae61787548f927e8f4bcfee3d0c0ceeb32dfa419456eb5
7
+ data.tar.gz: 96c14aa35cc5c7a359d559b8e4d7d8c29cf2fce8a44d92c125af8552002775d7fa5498b443e4cb5fd0d0a1fafceb695d6fee979a5897c33b9316f1283b8eccf6
@@ -1,5 +1,11 @@
1
1
  ### Changelog
2
2
 
3
+ ### 5.0.0
4
+
5
+ - Clean up base32 implementation to match Google Autheticator
6
+ - BREAKING `Base32.random_base32` renamed to random
7
+ - The argument is now byte length vs output string length for more precise bit strengths
8
+
3
9
  ### 4.1.0
4
10
 
5
11
  - Add a digest option to the CLI #83
data/README.md CHANGED
@@ -16,7 +16,16 @@ Many websites use this for [multi-factor authentication](https://www.youtube.com
16
16
  * OpenSSL
17
17
  * Ruby 2.0 or higher
18
18
 
19
- ## Breaking changes in >= 4.0
19
+ ## Breaking changes
20
+
21
+ ### Breaking changes in >= 5.0
22
+
23
+ - `ROTP::Base32.random_base32` is now `ROTP::Base32.random` and the argument
24
+ has changed from secret string length to byte length to allow for more
25
+ precision
26
+ - Cleaned up the Base32 implementation to better match Google Authenticator's version
27
+
28
+ ### Breaking changes in >= 4.0
20
29
 
21
30
  - Simplified API
22
31
  - `verify` now takes options for `drift` and `after`
@@ -108,7 +117,7 @@ totp.verify("250939", drift_behind: 15, at: now + 45) # => nil
108
117
  ### Generating a Base32 Secret key
109
118
 
110
119
  ```ruby
111
- ROTP::Base32.random_base32 # returns a 32 character base32 secret. Compatible with Google Authenticator
120
+ ROTP::Base32.random # returns a 160 bit (32 character) base32 secret. Compatible with Google Authenticator
112
121
  ```
113
122
 
114
123
  Note: The Base32 format conforms to [RFC 4648 Base32](http://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet)
@@ -150,7 +159,7 @@ bundle install
150
159
  bundle exec rspec
151
160
  ```
152
161
 
153
- ### Testign with Docker
162
+ ### Testing with Docker
154
163
 
155
164
  In order to make it easier to test against different ruby version, ROTP comes
156
165
  with a set of Dockerfiles for each version that we test against in Travis
@@ -162,7 +171,7 @@ docker run --rm -v $(pwd):/usr/src/app rotp_2.6
162
171
 
163
172
  ## Executable Usage
164
173
 
165
- The rotp rubygem includes an executable for helping with testing and debugging
174
+ The rotp rubygem includes CLI version to help with testing and debugging
166
175
 
167
176
  ```bash
168
177
  # Try this to get an overview of the commands
@@ -1,51 +1,67 @@
1
1
  module ROTP
2
2
  class Base32
3
3
  class Base32Error < RuntimeError; end
4
- CHARS = 'abcdefghijklmnopqrstuvwxyz234567'.each_char.to_a
4
+ CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.each_char.to_a
5
+ SHIFT = 5
6
+ MASK = 31
5
7
 
6
8
  class << self
9
+
7
10
  def decode(str)
8
- str = str.tr('=', '')
9
- output = []
10
- str.scan(/.{1,8}/).each do |block|
11
- char_array = decode_block(block).map(&:chr)
12
- output << char_array
11
+ buffer = 0
12
+ idx = 0
13
+ bits_left = 0
14
+ str = str.tr('=', '').upcase
15
+ result = []
16
+ str.split('').each do |char|
17
+ buffer = buffer << SHIFT
18
+ buffer = buffer | (decode_quint(char) & MASK)
19
+ bits_left = bits_left + SHIFT
20
+ if bits_left >= 8
21
+ result[idx] = (buffer >> (bits_left - 8)) & 255
22
+ idx = idx + 1
23
+ bits_left = bits_left - 8
24
+ end
13
25
  end
14
- output.join
26
+ result.pack('c*')
15
27
  end
16
28
 
17
- def random_base32(length = 32)
18
- b32 = ''
19
- SecureRandom.random_bytes(length).each_byte do |b|
20
- b32 << CHARS[b % 32]
29
+ def encode(b)
30
+ data = b.unpack('c*')
31
+ out = ''
32
+ buffer = data[0]
33
+ idx = 1
34
+ bits_left = 8
35
+ while bits_left > 0 || idx < data.length
36
+ if bits_left < SHIFT
37
+ if idx < data.length
38
+ buffer = buffer << 8
39
+ buffer = buffer | (data[idx] & 255)
40
+ bits_left = bits_left + 8
41
+ idx = idx + 1
42
+ else
43
+ pad = SHIFT - bits_left
44
+ buffer = buffer << pad
45
+ bits_left = bits_left + pad
46
+ end
47
+ end
48
+ val = MASK & (buffer >> (bits_left - SHIFT))
49
+ bits_left = bits_left - SHIFT
50
+ out.concat(CHARS[val])
21
51
  end
22
- b32
52
+ return out
23
53
  end
24
54
 
25
- private
26
-
27
- def decode_block(block)
28
- length = block.scan(/[^=]/).length
29
- quints = block.each_char.map { |c| decode_quint(c) }
30
- bytes = []
31
- bytes[0] = (quints[0] << 3) + (quints[1] ? quints[1] >> 2 : 0)
32
- return bytes if length < 3
33
-
34
- bytes[1] = ((quints[1] & 3) << 6) + (quints[2] << 1) + (quints[3] ? quints[3] >> 4 : 0)
35
- return bytes if length < 4
36
-
37
- bytes[2] = ((quints[3] & 15) << 4) + (quints[4] ? quints[4] >> 1 : 0)
38
- return bytes if length < 6
39
-
40
- bytes[3] = ((quints[4] & 1) << 7) + (quints[5] << 2) + (quints[6] ? quints[6] >> 3 : 0)
41
- return bytes if length < 7
42
-
43
- bytes[4] = ((quints[6] & 7) << 5) + (quints[7] || 0)
44
- bytes
55
+ # Defaults to 160 bit long secret (meaning a 32 character long base32 secret)
56
+ def random(byte_length = 20)
57
+ rand_bytes = SecureRandom.random_bytes(byte_length)
58
+ self.encode(rand_bytes)
45
59
  end
46
60
 
61
+ private
62
+
47
63
  def decode_quint(q)
48
- CHARS.index(q.downcase) || raise(Base32Error, "Invalid Base32 Character - '#{q}'")
64
+ CHARS.index(q) || raise(Base32Error, "Invalid Base32 Character - '#{q}'")
49
65
  end
50
66
  end
51
67
  end
@@ -19,7 +19,7 @@ module ROTP
19
19
  if %i[time hmac].include?(options.mode)
20
20
  if options.secret.to_s == ''
21
21
  red 'You must also specify a --secret. Try --help for help.'
22
- elsif options.secret.to_s.chars.any? { |c| ROTP::Base32::CHARS.index(c.downcase).nil? }
22
+ elsif options.secret.to_s.chars.any? { |c| ROTP::Base32::CHARS.index(c.upcase).nil? }
23
23
  red 'Secret must be in RFC4648 Base32 format - http://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet'
24
24
  end
25
25
  end
@@ -1,3 +1,3 @@
1
1
  module ROTP
2
- VERSION = '4.1.0'.freeze
2
+ VERSION = '5.0.0'.freeze
3
3
  end
@@ -1,24 +1,25 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe ROTP::Base32 do
4
- describe '.random_base32' do
4
+ describe '.random' do
5
5
  context 'without arguments' do
6
- let(:base32) { ROTP::Base32.random_base32 }
6
+ let(:base32) { ROTP::Base32.random }
7
7
 
8
- it 'is 32 characters long' do
8
+ it 'is 20 bytes (160 bits) long (resulting in a 32 character base32 code)' do
9
+ expect(ROTP::Base32.decode(base32).length).to eq 20
9
10
  expect(base32.length).to eq 32
10
11
  end
11
12
 
12
13
  it 'is base32 charset' do
13
- expect(base32).to match(/\A[a-z2-7]+\z/)
14
+ expect(base32).to match(/\A[A-Z2-7]+\z/)
14
15
  end
15
16
  end
16
17
 
17
18
  context 'with arguments' do
18
- let(:base32) { ROTP::Base32.random_base32 32 }
19
+ let(:base32) { ROTP::Base32.random 48 }
19
20
 
20
- it 'allows a specific length' do
21
- expect(base32.length).to eq 32
21
+ it 'returns the appropriate byte length code' do
22
+ expect(ROTP::Base32.decode(base32).length).to eq 48
22
23
  end
23
24
  end
24
25
  end
@@ -33,22 +34,40 @@ RSpec.describe ROTP::Base32 do
33
34
 
34
35
  context 'valid input data' do
35
36
  it 'correctly decodes a string' do
36
- expect(ROTP::Base32.decode('F').unpack('H*').first).to eq '28'
37
- expect(ROTP::Base32.decode('23').unpack('H*').first).to eq 'd6'
38
- expect(ROTP::Base32.decode('234').unpack('H*').first).to eq 'd6f8'
39
- expect(ROTP::Base32.decode('234A').unpack('H*').first).to eq 'd6f800'
40
- expect(ROTP::Base32.decode('234B').unpack('H*').first).to eq 'd6f810'
41
- expect(ROTP::Base32.decode('234BCD').unpack('H*').first).to eq 'd6f8110c'
42
- expect(ROTP::Base32.decode('234BCDE').unpack('H*').first).to eq 'd6f8110c80'
43
- expect(ROTP::Base32.decode('234BCDEFG').unpack('H*').first).to eq 'd6f8110c8530'
44
- expect(ROTP::Base32.decode('234BCDEFG234BCDEFG').unpack('H*').first).to eq 'd6f8110c8536b7c0886429'
37
+ expect(ROTP::Base32.decode('2EB7C66WC5TSO').unpack('H*').first).to eq 'd103f17bd6176727'
38
+ expect(ROTP::Base32.decode('Y6Y5ZCAC7NABCHSJ').unpack('H*').first).to eq 'c7b1dc8802fb40111e49'
39
+ end
40
+
41
+ it 'correctly decode strings with trailing bits (not a multiple of 8)' do
42
+ # Dropbox style 26 characters (26*5==130 bits or 16.25 bytes, but chopped to 128)
43
+ # Matches the behavior of Google Authenticator, drops extra 2 empty bits
44
+ expect(ROTP::Base32.decode('YVT6Z2XF4BQJNBMTD7M6QBQCEM').unpack('H*').first).to eq 'c567eceae5e0609685931fd9e8060223'
45
+
46
+ # For completeness, test all the possibilities allowed by Google Authenticator
47
+ # Drop the incomplete empty extra 4 bits (28*5==140bits or 17.5 bytes, chopped to 136 bits)
48
+ expect(ROTP::Base32.decode('5GGZQB3WN6LD7V3L5HPDYTQUANEQ').unpack('H*').first).to eq 'e98d9807766f963fd76be9de3c4e140349'
49
+
45
50
  end
46
51
 
47
52
  context 'with padding' do
48
53
  it 'correctly decodes a string' do
49
- expect(ROTP::Base32.decode('F==').unpack('H*').first).to eq '28'
54
+ expect(ROTP::Base32.decode('234A===').unpack('H*').first).to eq 'd6f8'
50
55
  end
51
56
  end
57
+
52
58
  end
53
59
  end
60
+
61
+ describe '.encode' do
62
+ context 'encode input data' do
63
+ it 'correctly encodes data' do
64
+ expect(ROTP::Base32.encode(hex_to_bin('3c204da94294ff82103ee34e96f74b48'))).to eq 'HQQE3KKCST7YEEB64NHJN52LJA'
65
+ end
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ def hex_to_bin(s)
72
+ s.scan(/../).map { |x| x.hex }.pack('c*')
54
73
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rotp
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Percival
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-02-28 00:00:00.000000000 Z
11
+ date: 2019-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable