rotp 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rotp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module ROTP
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.0"
3
3
  end
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('rake', '~>10.1.0')
24
- s.add_development_dependency('rspec', '~>2.13.0')
25
- if RUBY_VERSION < "1.9"
26
- s.add_development_dependency('timecop', "~>0.5.9.2")
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