rotp 2.0.0 → 2.1.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/.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
|