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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +13 -4
- data/lib/rotp/base32.rb +49 -33
- data/lib/rotp/cli.rb +1 -1
- data/lib/rotp/version.rb +1 -1
- data/spec/lib/rotp/base32_spec.rb +36 -17
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 592d8529aae80f05496b4f96d5888bbffd27f3afd92f2bad98b25a6b8046eee4
|
4
|
+
data.tar.gz: fbc30ba953e373b330df098c2ade00a2bac6cd635d8d488ceaafd3456f41f847
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9e61bbec32b47a1c52c781186dc1d2ef60be00242b70e6ce4c151bf9f4e964cbd402f4e1a0b5f2cddae61787548f927e8f4bcfee3d0c0ceeb32dfa419456eb5
|
7
|
+
data.tar.gz: 96c14aa35cc5c7a359d559b8e4d7d8c29cf2fce8a44d92c125af8552002775d7fa5498b443e4cb5fd0d0a1fafceb695d6fee979a5897c33b9316f1283b8eccf6
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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.
|
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
|
-
###
|
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
|
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
|
data/lib/rotp/base32.rb
CHANGED
@@ -1,51 +1,67 @@
|
|
1
1
|
module ROTP
|
2
2
|
class Base32
|
3
3
|
class Base32Error < RuntimeError; end
|
4
|
-
CHARS = '
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
26
|
+
result.pack('c*')
|
15
27
|
end
|
16
28
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
52
|
+
return out
|
23
53
|
end
|
24
54
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
64
|
+
CHARS.index(q) || raise(Base32Error, "Invalid Base32 Character - '#{q}'")
|
49
65
|
end
|
50
66
|
end
|
51
67
|
end
|
data/lib/rotp/cli.rb
CHANGED
@@ -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.
|
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
|
data/lib/rotp/version.rb
CHANGED
@@ -1,24 +1,25 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ROTP::Base32 do
|
4
|
-
describe '.
|
4
|
+
describe '.random' do
|
5
5
|
context 'without arguments' do
|
6
|
-
let(:base32) { ROTP::Base32.
|
6
|
+
let(:base32) { ROTP::Base32.random }
|
7
7
|
|
8
|
-
it 'is 32
|
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[
|
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.
|
19
|
+
let(:base32) { ROTP::Base32.random 48 }
|
19
20
|
|
20
|
-
it '
|
21
|
-
expect(base32.length).to eq
|
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('
|
37
|
-
expect(ROTP::Base32.decode('
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
expect(ROTP::Base32.decode('
|
44
|
-
|
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('
|
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
|
+
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-
|
11
|
+
date: 2019-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|