rotp 2.1.1 → 6.2.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rotp might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.devcontainer/Dockerfile +19 -0
- data/.devcontainer/devcontainer.json +37 -0
- data/.dockerignore +1 -0
- data/.github/workflows/test.yaml +27 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +95 -0
- data/Dockerfile-2.3 +10 -0
- data/Dockerfile-2.7 +11 -0
- data/Dockerfile-3.0-rc +12 -0
- data/Guardfile +1 -1
- data/README.md +125 -31
- data/bin/rotp +1 -1
- data/docker-compose.yml +37 -0
- data/lib/rotp/arguments.rb +6 -5
- data/lib/rotp/base32.rb +56 -29
- data/lib/rotp/cli.rb +6 -10
- data/lib/rotp/hotp.rb +11 -26
- data/lib/rotp/otp/uri.rb +79 -0
- data/lib/rotp/otp.rb +20 -31
- data/lib/rotp/totp.rb +43 -29
- data/lib/rotp/version.rb +1 -1
- data/lib/rotp.rb +2 -3
- data/rotp.gemspec +15 -18
- data/spec/lib/rotp/arguments_spec.rb +18 -5
- data/spec/lib/rotp/base32_spec.rb +51 -19
- data/spec/lib/rotp/cli_spec.rb +42 -3
- data/spec/lib/rotp/hotp_spec.rb +39 -60
- data/spec/lib/rotp/otp/uri_spec.rb +99 -0
- data/spec/lib/rotp/totp_spec.rb +138 -120
- data/spec/spec_helper.rb +7 -0
- metadata +27 -45
- data/.travis.yml +0 -7
- data/Gemfile.lock +0 -75
- data/Rakefile +0 -9
- data/doc/ROTP/HOTP.html +0 -308
- data/doc/ROTP/OTP.html +0 -593
- data/doc/ROTP/TOTP.html +0 -493
- data/doc/Rotp.html +0 -179
- data/doc/_index.html +0 -144
- data/doc/class_list.html +0 -36
- data/doc/css/common.css +0 -1
- data/doc/css/full_list.css +0 -53
- data/doc/css/style.css +0 -310
- data/doc/file.README.html +0 -89
- data/doc/file_list.html +0 -38
- data/doc/frames.html +0 -13
- data/doc/index.html +0 -89
- data/doc/js/app.js +0 -203
- data/doc/js/full_list.js +0 -149
- data/doc/js/jquery.js +0 -154
- data/doc/method_list.html +0 -155
- data/doc/top-level-namespace.html +0 -88
@@ -1,25 +1,33 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ROTP::Base32 do
|
4
|
-
|
5
|
-
describe '.random_base32' do
|
4
|
+
describe '.random' do
|
6
5
|
context 'without arguments' do
|
7
|
-
let(:base32) { ROTP::Base32.
|
6
|
+
let(:base32) { ROTP::Base32.random }
|
8
7
|
|
9
|
-
it 'is
|
10
|
-
expect(base32.length).to eq
|
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
|
10
|
+
expect(base32.length).to eq 32
|
11
11
|
end
|
12
12
|
|
13
|
-
it 'is
|
14
|
-
expect(base32).to match
|
13
|
+
it 'is base32 charset' do
|
14
|
+
expect(base32).to match(/\A[A-Z2-7]+\z/)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
18
|
context 'with arguments' do
|
19
|
-
let(:base32) { ROTP::Base32.
|
19
|
+
let(:base32) { ROTP::Base32.random 48 }
|
20
20
|
|
21
|
-
it '
|
22
|
-
expect(base32.length).to eq
|
21
|
+
it 'returns the appropriate byte length code' do
|
22
|
+
expect(ROTP::Base32.decode(base32).length).to eq 48
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'alias to older random_base32' do
|
27
|
+
let(:base32) { ROTP::Base32.random_base32(36) }
|
28
|
+
it 'is base32 charset' do
|
29
|
+
expect(base32.length).to eq 36
|
30
|
+
expect(ROTP::Base32.decode(base32).length).to eq 22
|
23
31
|
end
|
24
32
|
end
|
25
33
|
end
|
@@ -34,16 +42,40 @@ RSpec.describe ROTP::Base32 do
|
|
34
42
|
|
35
43
|
context 'valid input data' do
|
36
44
|
it 'correctly decodes a string' do
|
37
|
-
expect(ROTP::Base32.decode('
|
38
|
-
expect(ROTP::Base32.decode('
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
expect(ROTP::Base32.decode('
|
45
|
-
|
45
|
+
expect(ROTP::Base32.decode('2EB7C66WC5TSO').unpack('H*').first).to eq 'd103f17bd6176727'
|
46
|
+
expect(ROTP::Base32.decode('Y6Y5ZCAC7NABCHSJ').unpack('H*').first).to eq 'c7b1dc8802fb40111e49'
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'correctly decode strings with trailing bits (not a multiple of 8)' do
|
50
|
+
# Dropbox style 26 characters (26*5==130 bits or 16.25 bytes, but chopped to 128)
|
51
|
+
# Matches the behavior of Google Authenticator, drops extra 2 empty bits
|
52
|
+
expect(ROTP::Base32.decode('YVT6Z2XF4BQJNBMTD7M6QBQCEM').unpack('H*').first).to eq 'c567eceae5e0609685931fd9e8060223'
|
53
|
+
|
54
|
+
# For completeness, test all the possibilities allowed by Google Authenticator
|
55
|
+
# Drop the incomplete empty extra 4 bits (28*5==140bits or 17.5 bytes, chopped to 136 bits)
|
56
|
+
expect(ROTP::Base32.decode('5GGZQB3WN6LD7V3L5HPDYTQUANEQ').unpack('H*').first).to eq 'e98d9807766f963fd76be9de3c4e140349'
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with padding' do
|
61
|
+
it 'correctly decodes a string' do
|
62
|
+
expect(ROTP::Base32.decode('234A===').unpack('H*').first).to eq 'd6f8'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '.encode' do
|
70
|
+
context 'encode input data' do
|
71
|
+
it 'correctly encodes data' do
|
72
|
+
expect(ROTP::Base32.encode(hex_to_bin('3c204da94294ff82103ee34e96f74b48'))).to eq 'HQQE3KKCST7YEEB64NHJN52LJA'
|
46
73
|
end
|
47
74
|
end
|
48
75
|
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
def hex_to_bin(s)
|
80
|
+
s.scan(/../).map { |x| x.hex }.pack('c*')
|
49
81
|
end
|
data/spec/lib/rotp/cli_spec.rb
CHANGED
@@ -4,26 +4,65 @@ require 'rotp/cli'
|
|
4
4
|
RSpec.describe ROTP::CLI do
|
5
5
|
let(:cli) { described_class.new('executable', argv) }
|
6
6
|
let(:output) { cli.output }
|
7
|
-
let(:now) { Time.utc 2012,1,1 }
|
7
|
+
let(:now) { Time.utc 2012, 1, 1 }
|
8
8
|
|
9
9
|
before do
|
10
10
|
Timecop.freeze now
|
11
11
|
end
|
12
12
|
|
13
13
|
context 'generating a TOTP' do
|
14
|
-
let(:argv) { %w
|
14
|
+
let(:argv) { %w[--secret JBSWY3DPEHPK3PXP] }
|
15
15
|
|
16
16
|
it 'prints the corresponding token' do
|
17
17
|
expect(output).to eq '068212'
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
context 'generating a TOTP with sha256 digest' do
|
22
|
+
let(:argv) { %w[--secret JBSWY3DPEHPK3PXP --digest sha256] }
|
23
|
+
|
24
|
+
it 'prints the corresponding token' do
|
25
|
+
expect(output).to eq '544902'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'generating a TOTP with no secret' do
|
30
|
+
let(:argv) { %w[--time --secret] }
|
31
|
+
|
32
|
+
it 'prints the corresponding token' do
|
33
|
+
expect(output).to match 'You must also specify a --secret'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'generating a TOTP with bad base32 secret' do
|
38
|
+
let(:argv) { %W[--time --secret #{'1' * 32}] }
|
39
|
+
|
40
|
+
it 'prints the corresponding token' do
|
41
|
+
expect(output).to match 'Secret must be in RFC4648 Base32 format'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'trying to generate an unsupport type' do
|
46
|
+
let(:argv) { %W[--notreal --secret #{'a' * 32}] }
|
47
|
+
|
48
|
+
it 'prints the corresponding token' do
|
49
|
+
expect(output).to match 'invalid option: --notreal'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
21
53
|
context 'generating a HOTP' do
|
22
|
-
let(:argv) { %W
|
54
|
+
let(:argv) { %W[--hmac --secret #{'a' * 32} --counter 1234] }
|
23
55
|
|
24
56
|
it 'prints the corresponding token' do
|
25
57
|
expect(output).to eq '161024'
|
26
58
|
end
|
27
59
|
end
|
28
60
|
|
61
|
+
context 'generating a HOTP' do
|
62
|
+
let(:argv) { %W[--hmac --secret #{'a' * 32} --counter 1234 --digest sha256] }
|
63
|
+
|
64
|
+
it 'prints the corresponding token' do
|
65
|
+
expect(output).to eq '325941'
|
66
|
+
end
|
67
|
+
end
|
29
68
|
end
|
data/spec/lib/rotp/hotp_spec.rb
CHANGED
@@ -14,11 +14,9 @@ RSpec.describe ROTP::HOTP do
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
context '
|
18
|
-
|
19
|
-
|
20
|
-
it 'generates an integer OTP' do
|
21
|
-
expect(token).to eq 161024
|
17
|
+
context 'invalid counter' do
|
18
|
+
it 'raises an error' do
|
19
|
+
expect { hotp.at(-123_456) }.to raise_error(ArgumentError)
|
22
20
|
end
|
23
21
|
end
|
24
22
|
|
@@ -39,7 +37,6 @@ RSpec.describe ROTP::HOTP do
|
|
39
37
|
expect(hotp.at(8)).to eq '399871'
|
40
38
|
expect(hotp.at(9)).to eq '520489'
|
41
39
|
end
|
42
|
-
|
43
40
|
end
|
44
41
|
end
|
45
42
|
|
@@ -47,10 +44,10 @@ RSpec.describe ROTP::HOTP do
|
|
47
44
|
let(:verification) { hotp.verify token, counter }
|
48
45
|
|
49
46
|
context 'numeric token' do
|
50
|
-
let(:token) {
|
47
|
+
let(:token) { 161_024 }
|
51
48
|
|
52
49
|
it 'raises an error' do
|
53
|
-
expect { verification }.to raise_error
|
50
|
+
expect { verification }.to raise_error(ArgumentError)
|
54
51
|
end
|
55
52
|
end
|
56
53
|
|
@@ -69,74 +66,56 @@ RSpec.describe ROTP::HOTP do
|
|
69
66
|
expect(hotp.verify(token, 10)).to be_falsey
|
70
67
|
end
|
71
68
|
end
|
72
|
-
|
73
|
-
|
74
|
-
describe '#provisioning_uri' do
|
75
|
-
let(:uri) { hotp.provisioning_uri('mark@percival') }
|
76
|
-
let(:params) { CGI::parse URI::parse(uri).query }
|
77
|
-
|
78
|
-
it 'has the correct format' do
|
79
|
-
expect(uri).to match %r{\Aotpauth:\/\/hotp.+}
|
80
|
-
end
|
81
|
-
|
82
|
-
it 'includes the secret as parameter' do
|
83
|
-
expect(params['secret'].first).to eq 'a' * 32
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
describe '#verify_with_retries' do
|
88
|
-
let(:verification) { hotp.verify_with_retries token, counter, retries }
|
69
|
+
describe 'with retries' do
|
70
|
+
let(:verification) { hotp.verify token, counter, retries: retries }
|
89
71
|
|
90
|
-
|
91
|
-
|
72
|
+
context 'counter outside than retries' do
|
73
|
+
let(:counter) { 1223 }
|
74
|
+
let(:retries) { 10 }
|
92
75
|
|
93
|
-
|
94
|
-
|
76
|
+
it 'is false' do
|
77
|
+
expect(verification).to be_falsey
|
78
|
+
end
|
95
79
|
end
|
96
|
-
end
|
97
80
|
|
98
|
-
|
99
|
-
|
81
|
+
context 'counter exactly in retry range' do
|
82
|
+
let(:counter) { 1224 }
|
83
|
+
let(:retries) { 10 }
|
100
84
|
|
101
|
-
|
102
|
-
|
85
|
+
it 'is true' do
|
86
|
+
expect(verification).to eq 1234
|
87
|
+
end
|
103
88
|
end
|
104
|
-
end
|
105
89
|
|
106
|
-
|
107
|
-
|
108
|
-
|
90
|
+
context 'counter in retry range' do
|
91
|
+
let(:counter) { 1224 }
|
92
|
+
let(:retries) { 11 }
|
109
93
|
|
110
|
-
|
111
|
-
|
94
|
+
it 'is true' do
|
95
|
+
expect(verification).to eq 1234
|
96
|
+
end
|
112
97
|
end
|
113
|
-
end
|
114
98
|
|
115
|
-
|
116
|
-
|
117
|
-
|
99
|
+
context 'counter ahead of token' do
|
100
|
+
let(:counter) { 1235 }
|
101
|
+
let(:retries) { 3 }
|
118
102
|
|
119
|
-
|
120
|
-
|
103
|
+
it 'is false' do
|
104
|
+
expect(verification).to be_falsey
|
105
|
+
end
|
121
106
|
end
|
122
107
|
end
|
108
|
+
end
|
123
109
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
it 'is true' do
|
129
|
-
expect(verification).to eq 1234
|
130
|
-
end
|
110
|
+
describe '#provisioning_uri' do
|
111
|
+
it 'accepts the account name' do
|
112
|
+
expect(hotp.provisioning_uri('mark@percival'))
|
113
|
+
.to eq 'otpauth://hotp/mark%40percival?secret=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&counter=0'
|
131
114
|
end
|
132
115
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
it 'is false' do
|
138
|
-
expect(verification).to be_falsey
|
139
|
-
end
|
116
|
+
it 'also accepts a custom counter value' do
|
117
|
+
expect(hotp.provisioning_uri('mark@percival', 17))
|
118
|
+
.to eq 'otpauth://hotp/mark%40percival?secret=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&counter=17'
|
140
119
|
end
|
141
120
|
end
|
142
121
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ROTP::OTP::URI do
|
4
|
+
it 'meets basic functionality' do
|
5
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
|
6
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
7
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'includes issuer' do
|
11
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Example')
|
12
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
13
|
+
expect(uri.to_s).to eq 'otpauth://totp/Example:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example'
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'encodes the account name' do
|
17
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Provider1')
|
18
|
+
uri = described_class.new(otp, account_name: 'Alice Smith')
|
19
|
+
expect(uri.to_s).to eq 'otpauth://totp/Provider1:Alice%20Smith?secret=JBSWY3DPEHPK3PXP&issuer=Provider1'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'encodes the issuer' do
|
23
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Big Corporation')
|
24
|
+
uri = described_class.new(otp, account_name: ' alice@bigco.com')
|
25
|
+
expect(uri.to_s).to eq 'otpauth://totp/Big%20Corporation:%20alice%40bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big%20Corporation'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'includes non-default SHA256 algorithm' do
|
29
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digest: 'sha256')
|
30
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
31
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256'
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'includes non-default SHA512 algorithm' do
|
35
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digest: 'sha512')
|
36
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
37
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'includes non-default 8 digits' do
|
41
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digits: 8)
|
42
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
43
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&digits=8'
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'includes non-default period for TOTP' do
|
47
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', interval: 35)
|
48
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
49
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&period=35'
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'includes non-default counter for HOTP' do
|
53
|
+
otp = ROTP::HOTP.new('JBSWY3DPEHPK3PXP')
|
54
|
+
uri = described_class.new(otp, account_name: 'alice@google.com', counter: 17)
|
55
|
+
expect(uri.to_s).to eq 'otpauth://hotp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&counter=17'
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'can include all parameters' do
|
59
|
+
otp = ROTP::TOTP.new(
|
60
|
+
'HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ',
|
61
|
+
digest: 'sha512',
|
62
|
+
digits: 8,
|
63
|
+
interval: 60,
|
64
|
+
issuer: 'ACME Co',
|
65
|
+
)
|
66
|
+
uri = described_class.new(otp, account_name: 'john.doe@email.com')
|
67
|
+
expect(uri.to_s).to eq'otpauth://totp/ACME%20Co:john.doe%40email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA512&digits=8&period=60'
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'strips leading and trailing whitespace from the issuer' do
|
71
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: ' Big Corporation ')
|
72
|
+
uri = described_class.new(otp, account_name: ' alice@bigco.com')
|
73
|
+
expect(uri.to_s).to eq 'otpauth://totp/Big%20Corporation:%20alice%40bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big%20Corporation'
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'strips trailing whitespace from the account name' do
|
77
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
|
78
|
+
uri = described_class.new(otp, account_name: ' alice@google.com ')
|
79
|
+
expect(uri.to_s).to eq 'otpauth://totp/%20%20alice%40google.com?secret=JBSWY3DPEHPK3PXP'
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'replaces colons in the issuer with underscores' do
|
83
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Big:Corporation')
|
84
|
+
uri = described_class.new(otp, account_name: 'alice@bigco.com')
|
85
|
+
expect(uri.to_s).to eq 'otpauth://totp/Big_Corporation:alice%40bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big_Corporation'
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'replaces colons in the account name with underscores' do
|
89
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
|
90
|
+
uri = described_class.new(otp, account_name: 'Alice:Smith')
|
91
|
+
expect(uri.to_s).to eq 'otpauth://totp/Alice_Smith?secret=JBSWY3DPEHPK3PXP'
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'handles email account names with sub-addressing' do
|
95
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
|
96
|
+
uri = described_class.new(otp, account_name: 'alice+1234@google.com')
|
97
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%2B1234%40google.com?secret=JBSWY3DPEHPK3PXP'
|
98
|
+
end
|
99
|
+
end
|