rotp 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -1
- data/.travis.yml +2 -0
- data/CHANGELOG.md +61 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +75 -0
- data/Guardfile +14 -0
- data/README.md +118 -0
- data/Rakefile +0 -1
- data/bin/rotp +7 -0
- data/lib/rotp/arguments.rb +89 -0
- data/lib/rotp/cli.rb +56 -0
- data/lib/rotp/version.rb +1 -1
- data/rotp.gemspec +4 -7
- data/spec/lib/rotp/arguments_spec.rb +88 -0
- data/spec/lib/rotp/base32_spec.rb +49 -0
- data/spec/lib/rotp/cli_spec.rb +29 -0
- data/spec/lib/rotp/hotp_spec.rb +142 -0
- data/spec/lib/rotp/totp_spec.rb +235 -0
- data/spec/spec_helper.rb +9 -7
- metadata +54 -26
- data/.rspec +0 -3
- data/README.markdown +0 -163
- data/spec/base_spec.rb +0 -27
- data/spec/hotp_spec.rb +0 -66
- data/spec/totp_spec.rb +0 -115
data/lib/rotp/version.rb
CHANGED
data/rotp.gemspec
CHANGED
@@ -20,11 +20,8 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
21
|
s.require_paths = ["lib"]
|
22
22
|
|
23
|
-
s.add_development_dependency
|
24
|
-
s.add_development_dependency
|
25
|
-
|
26
|
-
|
27
|
-
else
|
28
|
-
s.add_development_dependency('timecop')
|
29
|
-
end
|
23
|
+
s.add_development_dependency 'guard-rspec', '~> 4.5.0'
|
24
|
+
s.add_development_dependency 'rake', '~> 10.4.2'
|
25
|
+
s.add_development_dependency 'rspec', '~> 3.1.0'
|
26
|
+
s.add_development_dependency 'timecop', '~> 0.7.1'
|
30
27
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rotp/arguments'
|
3
|
+
|
4
|
+
RSpec.describe ROTP::Arguments do
|
5
|
+
let(:arguments) { described_class.new filename, argv }
|
6
|
+
let(:argv) { '' }
|
7
|
+
let(:filename) { 'rotp' }
|
8
|
+
let(:options) { arguments.options }
|
9
|
+
|
10
|
+
context 'without options' do
|
11
|
+
describe '#help' do
|
12
|
+
it 'shows the help text' do
|
13
|
+
expect(arguments.to_s).to include 'Usage: '
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#options' do
|
18
|
+
it 'has the default options' do
|
19
|
+
expect(options.mode).to eq :time
|
20
|
+
expect(options.secret).to be_nil
|
21
|
+
expect(options.counter).to eq 0
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'unknown arguments' do
|
27
|
+
let(:argv) { %w(--does-not-exist -xyz) }
|
28
|
+
|
29
|
+
describe '#options' do
|
30
|
+
it 'is in help mode' do
|
31
|
+
expect(options.mode).to eq :help
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'knows about the problem' do
|
35
|
+
expect(options.warnings).to include 'invalid option: --does-not-exist'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'no arguments' do
|
41
|
+
let(:argv) { [] }
|
42
|
+
|
43
|
+
describe '#options' do
|
44
|
+
it 'is in help mode' do
|
45
|
+
expect(options.mode).to eq :help
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'asking for help' do
|
51
|
+
let(:argv) { %w(--help) }
|
52
|
+
|
53
|
+
describe '#options' do
|
54
|
+
it 'is in help mode' do
|
55
|
+
expect(options.mode).to eq :help
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'generating a counter based secret' do
|
61
|
+
let(:argv) { %w(--hmac --secret s3same) }
|
62
|
+
|
63
|
+
describe '#options' do
|
64
|
+
it 'is in hmac mode' do
|
65
|
+
expect(options.mode).to eq :hmac
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'knows the secret' do
|
69
|
+
expect(options.secret).to eq 's3same'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'generating a time based secret' do
|
75
|
+
let(:argv) { %w(--secret s3same) }
|
76
|
+
|
77
|
+
describe '#options' do
|
78
|
+
it 'is in time mode' do
|
79
|
+
expect(options.mode).to eq :time
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'knows the secret' do
|
83
|
+
expect(options.secret).to eq 's3same'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ROTP::Base32 do
|
4
|
+
|
5
|
+
describe '.random_base32' do
|
6
|
+
context 'without arguments' do
|
7
|
+
let(:base32) { ROTP::Base32.random_base32 }
|
8
|
+
|
9
|
+
it 'is 16 characters long' do
|
10
|
+
expect(base32.length).to eq 16
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'is hexadecimal' do
|
14
|
+
expect(base32).to match %r{\A[a-z2-7]+\z}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'with arguments' do
|
19
|
+
let(:base32) { ROTP::Base32.random_base32 32 }
|
20
|
+
|
21
|
+
it 'allows a specific length' do
|
22
|
+
expect(base32.length).to eq 32
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '.decode' do
|
28
|
+
context 'corrupt input data' do
|
29
|
+
it 'raises a sane error' do
|
30
|
+
expect { ROTP::Base32.decode('4BCDEFG234BCDEF1') }.to \
|
31
|
+
raise_error(ROTP::Base32::Base32Error, "Invalid Base32 Character - '1'")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'valid input data' do
|
36
|
+
it 'correctly decodes a string' do
|
37
|
+
expect(ROTP::Base32.decode('F').unpack('H*').first).to eq '28'
|
38
|
+
expect(ROTP::Base32.decode('23').unpack('H*').first).to eq 'd6'
|
39
|
+
expect(ROTP::Base32.decode('234').unpack('H*').first).to eq 'd6f8'
|
40
|
+
expect(ROTP::Base32.decode('234A').unpack('H*').first).to eq 'd6f800'
|
41
|
+
expect(ROTP::Base32.decode('234B').unpack('H*').first).to eq 'd6f810'
|
42
|
+
expect(ROTP::Base32.decode('234BCD').unpack('H*').first).to eq 'd6f8110c'
|
43
|
+
expect(ROTP::Base32.decode('234BCDE').unpack('H*').first).to eq 'd6f8110c80'
|
44
|
+
expect(ROTP::Base32.decode('234BCDEFG').unpack('H*').first).to eq 'd6f8110c8530'
|
45
|
+
expect(ROTP::Base32.decode('234BCDEFG234BCDEFG').unpack('H*').first).to eq 'd6f8110c8536b7c0886429'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rotp/cli'
|
3
|
+
|
4
|
+
RSpec.describe ROTP::CLI do
|
5
|
+
let(:cli) { described_class.new('executable', argv) }
|
6
|
+
let(:output) { cli.output }
|
7
|
+
let(:now) { Time.utc 2012,1,1 }
|
8
|
+
|
9
|
+
before do
|
10
|
+
Timecop.freeze now
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'generating a TOTP' do
|
14
|
+
let(:argv) { %w(--secret JBSWY3DPEHPK3PXP) }
|
15
|
+
|
16
|
+
it 'prints the corresponding token' do
|
17
|
+
expect(output).to eq '068212'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'generating a HOTP' do
|
22
|
+
let(:argv) { %W(--hmac --secret #{'a' * 32} --counter 1234) }
|
23
|
+
|
24
|
+
it 'prints the corresponding token' do
|
25
|
+
expect(output).to eq '161024'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ROTP::HOTP do
|
4
|
+
let(:counter) { 1234 }
|
5
|
+
let(:token) { '161024' }
|
6
|
+
let(:hotp) { ROTP::HOTP.new('a' * 32) }
|
7
|
+
|
8
|
+
describe '#at' do
|
9
|
+
let(:token) { hotp.at counter }
|
10
|
+
|
11
|
+
context 'only the counter as argument' do
|
12
|
+
it 'generates a string OTP' do
|
13
|
+
expect(token).to eq '161024'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'without padding' do
|
18
|
+
let(:token) { hotp.at counter, false }
|
19
|
+
|
20
|
+
it 'generates an integer OTP' do
|
21
|
+
expect(token).to eq 161024
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'RFC compatibility' do
|
26
|
+
let(:hotp) { ROTP::HOTP.new('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') }
|
27
|
+
|
28
|
+
it 'matches the RFC documentation examples' do
|
29
|
+
# 12345678901234567890 in Base32
|
30
|
+
# GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
|
31
|
+
expect(hotp.at(0)).to eq '755224'
|
32
|
+
expect(hotp.at(1)).to eq '287082'
|
33
|
+
expect(hotp.at(2)).to eq '359152'
|
34
|
+
expect(hotp.at(3)).to eq '969429'
|
35
|
+
expect(hotp.at(4)).to eq '338314'
|
36
|
+
expect(hotp.at(5)).to eq '254676'
|
37
|
+
expect(hotp.at(6)).to eq '287922'
|
38
|
+
expect(hotp.at(7)).to eq '162583'
|
39
|
+
expect(hotp.at(8)).to eq '399871'
|
40
|
+
expect(hotp.at(9)).to eq '520489'
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#verify' do
|
47
|
+
let(:verification) { hotp.verify token, counter }
|
48
|
+
|
49
|
+
context 'numeric token' do
|
50
|
+
let(:token) { 161024 }
|
51
|
+
|
52
|
+
it 'raises an error' do
|
53
|
+
expect { verification }.to raise_error
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'string token' do
|
58
|
+
it 'is true' do
|
59
|
+
expect(verification).to be_truthy
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'RFC compatibility' do
|
64
|
+
let(:hotp) { ROTP::HOTP.new('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') }
|
65
|
+
let(:token) { '520489' }
|
66
|
+
|
67
|
+
it 'verifies and does not allow reuse' do
|
68
|
+
expect(hotp.verify(token, 9)).to be_truthy
|
69
|
+
expect(hotp.verify(token, 10)).to be_falsey
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
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 }
|
89
|
+
|
90
|
+
context 'negative retries' do
|
91
|
+
let(:retries) { -1 }
|
92
|
+
|
93
|
+
it 'is false' do
|
94
|
+
expect(verification).to be_falsey
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'zero retries' do
|
99
|
+
let(:retries) { 0 }
|
100
|
+
|
101
|
+
it 'is false' do
|
102
|
+
expect(verification).to be_falsey
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'counter lower than retries' do
|
107
|
+
let(:counter) { 1223 }
|
108
|
+
let(:retries) { 10 }
|
109
|
+
|
110
|
+
it 'is false' do
|
111
|
+
expect(verification).to be_falsey
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context 'counter exactly in retry range' do
|
116
|
+
let(:counter) { 1224 }
|
117
|
+
let(:retries) { 10 }
|
118
|
+
|
119
|
+
it 'is true' do
|
120
|
+
expect(verification).to eq 1234
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context 'counter in retry range' do
|
125
|
+
let(:counter) { 1224 }
|
126
|
+
let(:retries) { 11 }
|
127
|
+
|
128
|
+
it 'is true' do
|
129
|
+
expect(verification).to eq 1234
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'counter too high' do
|
134
|
+
let(:counter) { 1235 }
|
135
|
+
let(:retries) { 3 }
|
136
|
+
|
137
|
+
it 'is false' do
|
138
|
+
expect(verification).to be_falsey
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ROTP::TOTP do
|
4
|
+
let(:now) { Time.utc 2012,1,1 }
|
5
|
+
let(:token) { '068212' }
|
6
|
+
let(:totp) { ROTP::TOTP.new 'JBSWY3DPEHPK3PXP' }
|
7
|
+
|
8
|
+
describe '#at' do
|
9
|
+
context 'with padding' do
|
10
|
+
let(:token) { totp.at now }
|
11
|
+
|
12
|
+
it 'is a string number' do
|
13
|
+
expect(token).to eq '068212'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'without padding' do
|
18
|
+
let(:token) { totp.at now, false }
|
19
|
+
|
20
|
+
it 'is an integer' do
|
21
|
+
expect(token).to eq 68212
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'RFC compatibility' do
|
26
|
+
let(:totp) { ROTP::TOTP.new('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') }
|
27
|
+
|
28
|
+
it 'matches the RFC documentation examples' do
|
29
|
+
expect(totp.at 1111111111).to eq '050471'
|
30
|
+
expect(totp.at 1234567890).to eq '005924'
|
31
|
+
expect(totp.at 2000000000).to eq '279037'
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#verify' do
|
38
|
+
let(:verification) { totp.verify token, now }
|
39
|
+
|
40
|
+
context 'numeric token' do
|
41
|
+
let(:token) { 68212 }
|
42
|
+
|
43
|
+
it 'raises an error' do
|
44
|
+
expect { verification }.to raise_error
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'unpadded string token' do
|
49
|
+
let(:token) { '68212' }
|
50
|
+
|
51
|
+
it 'is false' do
|
52
|
+
expect(verification).to be_falsey
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'correctly padded string token' do
|
57
|
+
it 'is true' do
|
58
|
+
expect(verification).to be_truthy
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'RFC compatibility' do
|
63
|
+
let(:totp) { ROTP::TOTP.new 'wrn3pqx5uqxqvnqr' }
|
64
|
+
|
65
|
+
before do
|
66
|
+
Timecop.freeze now
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'correct time based OTP' do
|
70
|
+
let(:token) { '102705' }
|
71
|
+
let(:now) { Time.at 1297553958 }
|
72
|
+
|
73
|
+
it 'is true' do
|
74
|
+
expect(totp.verify('102705')).to be_truthy
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'wrong time based OTP' do
|
79
|
+
it 'is false' do
|
80
|
+
expect(totp.verify('102705')).to be_falsey
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#provisioning_uri' do
|
87
|
+
let(:uri) { totp.provisioning_uri('mark@percival') }
|
88
|
+
let(:params) { CGI::parse URI::parse(uri).query }
|
89
|
+
|
90
|
+
context 'without issuer' do
|
91
|
+
it 'has the correct format' do
|
92
|
+
expect(uri).to match %r{\Aotpauth:\/\/totp.+}
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'includes the secret as parameter' do
|
96
|
+
expect(params['secret'].first).to eq 'JBSWY3DPEHPK3PXP'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'with issuer' do
|
101
|
+
let(:totp) { ROTP::TOTP.new 'JBSWY3DPEHPK3PXP', issuer: 'FooCo' }
|
102
|
+
|
103
|
+
it 'has the correct format' do
|
104
|
+
expect(uri).to match %r{\Aotpauth:\/\/totp.+}
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'includes the secret as parameter' do
|
108
|
+
expect(params['secret'].first).to eq 'JBSWY3DPEHPK3PXP'
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'includes the issuer as parameter' do
|
112
|
+
expect(params['issuer'].first).to eq 'FooCo'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'with custom interval' do
|
117
|
+
let(:totp) { ROTP::TOTP.new 'JBSWY3DPEHPK3PXP', interval: 60 }
|
118
|
+
|
119
|
+
it 'has the correct format' do
|
120
|
+
expect(uri).to match %r{\Aotpauth:\/\/totp.+}
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'includes the secret as parameter' do
|
124
|
+
expect(params['secret'].first).to eq 'JBSWY3DPEHPK3PXP'
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'includes the interval as period parameter' do
|
128
|
+
expect(params['period'].first).to eq '60'
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe '#verify_with_drift' do
|
134
|
+
let(:verification) { totp.verify_with_drift token, drift, now }
|
135
|
+
let(:drift) { 0 }
|
136
|
+
|
137
|
+
context 'numeric token' do
|
138
|
+
let(:token) { 68212 }
|
139
|
+
|
140
|
+
it 'raises an error' do
|
141
|
+
# In the "old" specs this was not tested due to a typo. What is the expected behavior here?
|
142
|
+
expect { verification }.to raise_error
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'unpadded string token' do
|
147
|
+
let(:token) { '68212' }
|
148
|
+
|
149
|
+
it 'is false' do
|
150
|
+
# Not sure whether this should be tested. It didn't exist in the "old" specs
|
151
|
+
expect(verification).to be_falsey
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'correctly padded string token' do
|
156
|
+
let(:token) { '068212' }
|
157
|
+
|
158
|
+
it 'is true' do
|
159
|
+
expect(verification).to be_truthy
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'slightly old number' do
|
164
|
+
let(:token) { totp.at now - 30 }
|
165
|
+
let(:drift) { 60 }
|
166
|
+
|
167
|
+
it 'is true' do
|
168
|
+
expect(verification).to be_truthy
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'slightly new number' do
|
173
|
+
let(:token) { totp.at now + 60 }
|
174
|
+
let(:drift) { 60 }
|
175
|
+
|
176
|
+
it 'is true' do
|
177
|
+
expect(verification).to be_truthy
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context 'outside of drift range' do
|
182
|
+
let(:token) { totp.at now - 60 }
|
183
|
+
let(:drift) { 30 }
|
184
|
+
|
185
|
+
it 'is false' do
|
186
|
+
expect(verification).to be_falsey
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'drift is not multiple of TOTP interval' do
|
191
|
+
context 'slightly old number' do
|
192
|
+
let(:token) { totp.at now - 45 }
|
193
|
+
let(:drift) { 45 }
|
194
|
+
|
195
|
+
it 'is true' do
|
196
|
+
expect(verification).to be_truthy
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
context 'slightly new number' do
|
201
|
+
let(:token) { totp.at now + 40 }
|
202
|
+
let(:drift) { 40 }
|
203
|
+
|
204
|
+
it 'is true' do
|
205
|
+
expect(verification).to be_truthy
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe '#now' do
|
212
|
+
before do
|
213
|
+
Timecop.freeze now
|
214
|
+
end
|
215
|
+
|
216
|
+
context 'Google Authenticator' do
|
217
|
+
let(:totp) { ROTP::TOTP.new 'wrn3pqx5uqxqvnqr' }
|
218
|
+
let(:now) { Time.at 1297553958 }
|
219
|
+
|
220
|
+
it 'matches the known output' do
|
221
|
+
expect(totp.now).to eq '102705'
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
context 'Dropbox 26 char secret output' do
|
226
|
+
let(:totp) { ROTP::TOTP.new 'tjtpqea6a42l56g5eym73go2oa' }
|
227
|
+
let(:now) { Time.at 1378762454 }
|
228
|
+
|
229
|
+
it 'matches the known output' do
|
230
|
+
expect(totp.now).to eq '747864'
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|