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.
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