rotp 4.1.0 → 5.0.0

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