rotp 3.3.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 6597229fd1ace9419ec1212d4cdedaa8392d5e2c
4
- data.tar.gz: 7784edcd67e532d6e1c14e229c79e2b1c6c5bb6e
2
+ SHA256:
3
+ metadata.gz: 681041e122df89a1947016e02fd7c11e3102ffc3eed78c1dcdcb5056af116afd
4
+ data.tar.gz: 0333b10d91241c31b7fb2b3b5b7bd0d99f0e02b471ce0d10dc5961ae98cb89d7
5
5
  SHA512:
6
- metadata.gz: cc83e697d928afc3be726fa0a7569f87bb5d69362ae429af512e35bc5f15d6cce947c510ab6204b6cac99601a3870600577360750f3e9af35762e316ce45080b
7
- data.tar.gz: 5cdefb29436b550ecd825baa13cc2365c994705f575b474d9a8d02ecad1764ce9982bc04537ea95afbfe24b304c1a1a71e339f4223f496138f7eaeaa76261050
6
+ metadata.gz: a7454a2072e7f6b4cabd84338b2c9b1d06363102230d2c93bd244f0a486004b6363e9cd50aece371c951ae68798a6cc109798948c11034f679030bfd7106e464
7
+ data.tar.gz: 18e79a2ca2a61d9b6a2dd902744e344fe1be9b7f4ac2c3d4674d6c83929f69b2395d794c9d9081115678fbfc81599f50a07cf8b287ce1c7f8ed05dcee4322e13
@@ -4,6 +4,5 @@ rvm:
4
4
  - 2.3.0
5
5
  - 2.1.0
6
6
  - 2.0.0
7
- - 1.9.3
8
7
  script:
9
8
  - bundle exec rspec
@@ -1,9 +1,15 @@
1
1
  ### Changelog
2
2
 
3
+ #### 4.0.0
4
+
5
+ - Simplify API
6
+ - Remove support for Ruby < 2.0
7
+
3
8
  #### 3.3.1
4
9
 
5
10
  - Add OpenSSL as a requirement for Ruby 2.5. Fixes #70 & #64
6
11
  - Allow Base32 with padding. #71
12
+ - Prevent verify with drift being negative #69
7
13
 
8
14
  #### 3.3.0
9
15
 
data/README.md CHANGED
@@ -4,16 +4,24 @@
4
4
  [![Gem Version](https://badge.fury.io/rb/rotp.svg)](https://rubygems.org/gems/rotp)
5
5
  [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/mdp/rotp/blob/master/LICENSE)
6
6
 
7
- A ruby library for generating one time passwords (HOTP & TOTP) according to [RFC 4226](http://tools.ietf.org/html/rfc4226) and [RFC 6238](http://tools.ietf.org/html/rfc6238).
7
+ A ruby library for generating and validating one time passwords (HOTP & TOTP) according to [RFC 4226](http://tools.ietf.org/html/rfc4226) and [RFC 6238](http://tools.ietf.org/html/rfc6238).
8
8
 
9
- ROTP is compatible with the [Google Authenticator](https://github.com/google/google-authenticator) available for [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2) and [iPhone](https://itunes.apple.com/en/app/google-authenticator/id388497605).
9
+ ROTP is compatible with [Google Authenticator](https://github.com/google/google-authenticator) available for [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2) and [iPhone](https://itunes.apple.com/en/app/google-authenticator/id388497605) and any other TOTP based implementations.
10
10
 
11
- Many websites use this for [multi-factor authentication](https://www.youtube.com/watch?v=17rykTIX_HY), such as GMail, Facebook, Amazon EC2, WordPress, and Salesforce. You can find the whole [list here](https://en.wikipedia.org/wiki/Google_Authenticator#Usage).
11
+ Many websites use this for [multi-factor authentication](https://www.youtube.com/watch?v=17rykTIX_HY), such as GMail, Facebook, Amazon EC2, WordPress, and Salesforce. You can find a more complete [list here](https://en.wikipedia.org/wiki/Google_Authenticator#Usage).
12
12
 
13
13
  ## Dependencies
14
14
 
15
15
  * OpenSSL
16
- * Ruby 1.9.3 or higher
16
+ * Ruby 2.0 or higher
17
+
18
+ ## Breaking changes in >= 4.0
19
+
20
+ - Simplified API
21
+ - `verify` now takes options for `drift` and `after`
22
+ - `verify` returns a timestamp if true, nil if false
23
+ - Dropping support for Ruby < 2.0
24
+ - Docs for 3.x can be found [here](https://github.com/mdp/rotp/tree/v3.3.0)
17
25
 
18
26
  ## Installation
19
27
 
@@ -26,20 +34,17 @@ gem install rotp
26
34
  ### Time based OTP's
27
35
 
28
36
  ```ruby
29
- totp = ROTP::TOTP.new("base32secret3232")
37
+ totp = ROTP::TOTP.new("base32secret3232", issuer: "My Service")
30
38
  totp.now # => "492039"
31
39
 
32
- # OTP verified for current time
33
- totp.verify("492039") # => true
34
- sleep 30
35
- totp.verify("492039") # => false
36
- ```
40
+ # OTP verified for current time - returns timestamp of the current interval
41
+ # period.
42
+ totp.verify("492039") # => 1474590700
37
43
 
38
- Optionally, you can provide an issuer which will be used as a title in Google Authenticator.
44
+ sleep 30
39
45
 
40
- ```ruby
41
- totp = ROTP::TOTP.new("base32secret3232", issuer: "My Service")
42
- totp.provisioning_uri("alice@google.com")
46
+ # OTP fails to verify - returns nil
47
+ totp.verify("492039") # => nil
43
48
  ```
44
49
 
45
50
  ### Counter based OTP's
@@ -51,69 +56,76 @@ hotp.at(1) # => "595254"
51
56
  hotp.at(1401) # => "259769"
52
57
 
53
58
  # OTP verified with a counter
54
- hotp.verify("259769", 1401) # => true
55
- hotp.verify("259769", 1402) # => false
56
- ```
57
-
58
- ### Verifying a Time based OTP with drift
59
-
60
- Some users devices may be slightly behind or ahead of the actual time. ROTP allows users to verify
61
- an OTP code with an specific amount of 'drift'
62
-
63
- ```ruby
64
- totp = ROTP::TOTP.new("base32secret3232")
65
- totp.now # => "492039"
66
-
67
- # OTP verified for current time with 120 seconds allowed drift
68
- totp.verify_with_drift("492039", 60, Time.now - 30) # => true
69
- totp.verify_with_drift("492039", 60, Time.now - 90) # => false
59
+ hotp.verify("316439", 1401) # => 1401
60
+ hotp.verify("316439", 1402) # => nil
70
61
  ```
71
62
 
72
63
  ### Preventing reuse of Time based OTP's
73
64
 
74
- In order to prevent reuse of time based tokens within the interval window (default 30 seconds)
75
- it is necessary to store the last time an OTP was used. The following is an example of this in action:
65
+ By keeping track of the last time a user's OTP was verified, we can prevent token reuse during
66
+ the interval window (default 30 seconds)
67
+
68
+ The following is an example of this in action:
76
69
 
77
70
  ```ruby
78
71
  User.find(someUserID)
79
72
  totp = ROTP::TOTP.new(user.otp_secret)
80
73
  totp.now # => "492039"
81
74
 
82
- user.last_otp_at # => 1472145530
75
+ user.last_otp_at # => 1432703530
83
76
 
84
77
  # Verify the OTP
85
- verified_at_timestamp = totp.verify_with_drift_and_prior("492039", 0, user.last_otp_at) #=> 1472145760
78
+ last_otp_at = totp.verify("492039", after: user.last_otp_at) #=> 1472145760
79
+ # ROTP returns the timestamp(int) of the current period
86
80
  # Store this on the user's account
87
- user.update(last_otp_at: verified_at_timestamp)
88
- verified_at_timestamp = totp.verify_with_drift_and_prior("492039", 0, user.last_otp_at) #=> false
81
+ user.update(last_otp_at: last_otp_at)
82
+ # Someone attempts to reused the OTP inside the 30s window
83
+ last_otp_at = totp.verify("492039", after: user.last_otp_at) #=> nil
84
+ # It fails to verify because we are still in the same 30s interval window
85
+ ```
86
+
87
+ ### Verifying a Time based OTP with drift
88
+
89
+ Some users may enter a code just after it has expired. By adding 'drift' you can allow
90
+ for a recently expired token to remain valid.
91
+
92
+ ```ruby
93
+ totp = ROTP::TOTP.new("base32secret3232")
94
+ now = Time.at(1474590600) #2016-09-23 00:30:00 UTC
95
+ totp.at(now) # => "250939"
96
+
97
+ # OTP verified for current time along with 15 seconds earlier
98
+ # ie. User enters a code just after it expired
99
+ totp.verify("250939", drift_behind: 15, at: now + 35) # => 1474590600
100
+ # User waits too long. Fails to validate previous OTP
101
+ totp.verify("250939", drift_behind: 15, at: now + 45) # => nil
89
102
  ```
90
103
 
91
104
  ### Generating a Base32 Secret key
92
105
 
93
106
  ```ruby
94
- ROTP::Base32.random_base32 # returns a 16 character base32 secret. Compatible with Google Authenticator
107
+ ROTP::Base32.random_base32 # returns a 32 character base32 secret. Compatible with Google Authenticator
95
108
  ```
96
109
 
97
110
  Note: The Base32 format conforms to [RFC 4648 Base32](http://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet)
98
111
 
99
- ### Google Authenticator Compatible URI's
112
+ ### Generating QR Codes for provisioning mobile apps
100
113
 
101
- Provisioning URI's generated by ROTP are compatible with the Google Authenticator App
102
- to be scanned with the in-built QR Code scanner.
114
+ Provisioning URI's generated by ROTP are compatible with most One Time Password applications, including
115
+ Google Authenticator.
103
116
 
104
117
  ```ruby
105
118
  totp.provisioning_uri("alice@google.com") # => 'otpauth://totp/issuer:alice@google.com?secret=JBSWY3DPEHPK3PXP'
106
119
  hotp.provisioning_uri("alice@google.com", 0) # => 'otpauth://hotp/issuer:alice@google.com?secret=JBSWY3DPEHPK3PXP&counter=0'
107
120
  ```
108
121
 
109
- This can then be rendered as a QR Code which can then be scanned and added to the users
110
- list of OTP credentials.
122
+ This can then be rendered as a QR Code which the user can scan using their mobile phone and the appropriate application.
111
123
 
112
124
  #### Working example
113
125
 
114
126
  Scan the following barcode with your phone, using Google Authenticator
115
127
 
116
- ![QR Code for OTP](http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP)
128
+ ![QR Code for OTP](https://cloud.githubusercontent.com/assets/2868/18771262/54f109dc-80f2-11e6-863f-d2be62ee587a.png)
117
129
 
118
130
  Now run the following and compare the output
119
131
 
@@ -133,8 +145,7 @@ bundle exec rspec
133
145
 
134
146
  ## Executable Usage
135
147
 
136
- Once the rotp rubygem is installed on your system, you should be able to run the `rotp` executable
137
- (if not, you might find trouble-shooting help [at this stackoverflow question](http://stackoverflow.com/a/909980)).
148
+ The rotp rubygem includes an executable for helping with testing and debugging
138
149
 
139
150
  ```bash
140
151
  # Try this to get an overview of the commands
@@ -151,7 +162,7 @@ Have a look at the [contributors graph](https://github.com/mdp/rotp/graphs/contr
151
162
 
152
163
  ## License
153
164
 
154
- MIT Copyright (C) 2011 by Mark Percival, see [LICENSE](https://github.com/mdp/rotp/blob/master/LICENSE) for details.
165
+ MIT Copyright (C) 2016 by Mark Percival, see [LICENSE](https://github.com/mdp/rotp/blob/master/LICENSE) for details.
155
166
 
156
167
  ## Other implementations
157
168
 
@@ -14,7 +14,7 @@ module ROTP
14
14
  output.join
15
15
  end
16
16
 
17
- def random_base32(length=16)
17
+ def random_base32(length=32)
18
18
  b32 = String.new
19
19
  SecureRandom.random_bytes(length).each_byte do |b|
20
20
  b32 << CHARS[b % 32]
@@ -9,9 +9,11 @@ module ROTP
9
9
  @argv = argv
10
10
  end
11
11
 
12
+ # :nocov:
12
13
  def run
13
14
  puts output
14
15
  end
16
+ # :nocov:
15
17
 
16
18
  def errors
17
19
  if [:time, :hmac].include?(options.mode)
@@ -20,8 +22,6 @@ module ROTP
20
22
  elsif options.secret.to_s.chars.any? { |c| ROTP::Base32::CHARS.index(c.downcase) == nil }
21
23
  red 'Secret must be in RFC4648 Base32 format - http://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet'
22
24
  end
23
- elsif options.mode == :hmac && options.counter.to_i < 0
24
- red 'You must also specify a --counter. Try --help for help.'
25
25
  end
26
26
  end
27
27
 
@@ -34,9 +34,6 @@ module ROTP
34
34
  ROTP::TOTP.new(options.secret).now
35
35
  elsif options.mode == :hmac
36
36
  ROTP::HOTP.new(options.secret).at options.counter
37
-
38
- else
39
- fail NotImplementedError
40
37
  end
41
38
  end
42
39
 
@@ -2,33 +2,20 @@ module ROTP
2
2
  class HOTP < OTP
3
3
  # Generates the OTP for the given count
4
4
  # @param [Integer] count counter
5
- # @option [Boolean] padding (false) Issue the number as a 0 padded string
6
5
  # @returns [Integer] OTP
7
- def at(count, padding=true)
8
- generate_otp(count, padding)
6
+ def at(count)
7
+ generate_otp(count)
9
8
  end
10
9
 
11
10
  # Verifies the OTP passed in against the current time OTP
12
- # @param [String/Integer] otp the OTP to check against
13
- # @param [Integer] counter the counter of the OTP
14
- def verify(otp, counter)
15
- super(otp, self.at(counter))
16
- end
17
-
18
- # Verifies the OTP passed in against the current time OTP, with a given number of retries.
19
- # Returns the counter that was verified successfully
20
- # @param [String/Integer] otp the OTP to check against
21
- # @param [Integer] initial counter the counter of the OTP
22
- # @param [Integer] number of retries
23
- def verify_with_retries(otp, initial_count, retries = 1)
24
- return false if retries <= 0
25
-
26
- 1.upto(retries) do |counter|
27
- current_counter = initial_count + counter
28
- return current_counter if verify(otp, current_counter)
29
- end
30
-
31
- false
11
+ # @param otp [String/Integer] the OTP to check against
12
+ # @param counter [Integer] the counter of the OTP
13
+ # @param retries [Integer] number of counters to incrementally retry
14
+ def verify(otp, counter, retries: 0)
15
+ counters = (counter..counter+retries).to_a
16
+ counters.find { |c|
17
+ super(otp, self.at(c))
18
+ }
32
19
  end
33
20
 
34
21
  # Returns the provisioning URI for the OTP
@@ -21,7 +21,7 @@ module ROTP
21
21
  # @option padded [Boolean] (false) Output the otp as a 0 padded string
22
22
  # Usually either the counter, or the computed integer
23
23
  # based on the Unix timestamp
24
- def generate_otp(input, padded=true)
24
+ def generate_otp(input)
25
25
  hmac = OpenSSL::HMAC.digest(
26
26
  OpenSSL::Digest.new(digest),
27
27
  byte_secret,
@@ -33,19 +33,14 @@ module ROTP
33
33
  (hmac[offset + 1].ord & 0xff) << 16 |
34
34
  (hmac[offset + 2].ord & 0xff) << 8 |
35
35
  (hmac[offset + 3].ord & 0xff)
36
- if padded
37
- (code % 10 ** digits).to_s.rjust(digits, '0')
38
- else
39
- code % 10 ** digits
40
- end
36
+ (code % 10 ** digits).to_s.rjust(digits, '0')
41
37
  end
42
38
 
43
39
  private
44
40
 
45
41
  def verify(input, generated)
46
- unless input.is_a?(String) && generated.is_a?(String)
47
- raise ArgumentError, "ROTP only verifies strings - See: https://github.com/mdp/rotp/issues/32"
48
- end
42
+ raise ArgumentError, "`otp` should be a String" unless
43
+ input.is_a?(String)
49
44
  time_constant_compare(input, generated)
50
45
  end
51
46
 
@@ -14,64 +14,46 @@ module ROTP
14
14
 
15
15
  # Accepts either a Unix timestamp integer or a Time object.
16
16
  # Time objects will be adjusted to UTC automatically
17
- # @param [Time/Integer] time the time to generate an OTP for
18
- # @option [Boolean] padding (true) Issue the number as a 0 padded string
19
- def at(time, padding=true)
20
- unless time.class == Time
21
- time = Time.at(time.to_i)
22
- end
23
-
24
- generate_otp(timecode(time), padding)
17
+ # @param time [Time/Integer] the time to generate an OTP for, integer unix timestamp or Time object
18
+ def at(time)
19
+ generate_otp(timecode(time))
25
20
  end
26
21
 
27
22
  # Generate the current time OTP
28
23
  # @return [Integer] the OTP as an integer
29
- def now(padding=true)
30
- generate_otp(timecode(Time.now), padding)
31
- end
32
-
33
- # Verifies the OTP passed in against the current time OTP
34
- # @param [String/Integer] otp the OTP to check against
35
- def verify(otp, time = Time.now)
36
- super(otp, self.at(time))
37
- end
38
-
39
- # Verifies the OTP passed in against the current time OTP
40
- # and adjacent intervals up to +drift+.
41
- # @param [String] otp the OTP to check against
42
- # @param [Integer] drift the number of seconds that the client
43
- # and server are allowed to drift apart
44
- def verify_with_drift(otp, drift, time = Time.now)
45
- time = time.to_i
46
- times = (time-drift..time+drift).step(interval).to_a
47
- times << time + drift if times.last < time + drift
48
- times.any? { |ti| verify(otp, ti) }
24
+ def now()
25
+ generate_otp(timecode(Time.now))
49
26
  end
50
27
 
51
28
  # Verifies the OTP passed in against the current time OTP
52
29
  # and adjacent intervals up to +drift+. Excludes OTPs
53
- # from prior_time and earlier. Returns time value of
30
+ # from `after` and earlier. Returns time value of
54
31
  # matching OTP code for use in subsequent call.
55
- # @param [String] otp the OTP to check against
56
- # @param [Integer] drift the number of seconds that the client
57
- # and server are allowed to drift apart
58
- # @param [Integer] time value of previous match
59
- def verify_with_drift_and_prior(otp, drift, prior_time = nil, time = Time.now)
60
- # calculate normalized bin start times based on drift
61
- first_bin = (time - drift).to_i / interval * interval
62
- last_bin = (time + drift).to_i / interval * interval
32
+ # @param otp [String] the one time password to verify
33
+ # @param drift_behind [Integer] how many seconds to look back
34
+ # @param drift_ahead [Integer] how many seconds to look ahead
35
+ # @param after [Integer] prevent token reuse, last login timestamp
36
+ # @param at [Time] time at which to generate and verify a particular
37
+ # otp. default Time.now
38
+ # @return [Integer, nil] the last successful timestamp
39
+ # interval
40
+ def verify(otp, drift_ahead: 0, drift_behind: 0, after: nil, at: Time.now)
41
+ timecodes = get_timecodes(at, drift_behind, drift_ahead)
63
42
 
64
- # if prior_time was supplied, adjust first bin if necessary to exclude it
65
- if prior_time
66
- prior_bin = prior_time.to_i / interval * interval
67
- first_bin = prior_bin + interval if prior_bin >= first_bin
68
- # fail if we've already used the last available OTP code
69
- return if first_bin > last_bin
43
+ if after
44
+ timecodes = timecodes.select { |t| t > timecode(after) }
70
45
  end
71
- times = (first_bin..last_bin).step(interval).to_a
72
- times.find { |ti| verify(otp, ti) }
46
+
47
+ result = nil
48
+ timecodes.each { |t|
49
+ if (super(otp, self.generate_otp(t)))
50
+ result = t * interval
51
+ end
52
+ }
53
+ return result
73
54
  end
74
55
 
56
+
75
57
  # Returns the provisioning URI for the OTP
76
58
  # This can then be encoded in a QR Code and used
77
59
  # to provision the Google Authenticator app
@@ -95,8 +77,24 @@ module ROTP
95
77
 
96
78
  private
97
79
 
80
+ # Get back an array of timecodes for a period
81
+ def get_timecodes(at, drift_behind, drift_ahead)
82
+ now = timeint(at)
83
+ timecode_start = timecode(now - drift_behind)
84
+ timecode_end = timecode(now + drift_ahead)
85
+ return (timecode_start..timecode_end).step(1).to_a
86
+ end
87
+
88
+ # Ensure UTC int
89
+ def timeint(time)
90
+ unless time.class == Time
91
+ return time.to_i
92
+ end
93
+ return time.utc.to_i
94
+ end
95
+
98
96
  def timecode(time)
99
- time.utc.to_i / interval
97
+ return timeint(time) / interval
100
98
  end
101
99
 
102
100
  end
@@ -1,3 +1,3 @@
1
1
  module ROTP
2
- VERSION = "3.3.1"
2
+ VERSION = "4.0.0"
3
3
  end
@@ -71,6 +71,20 @@ RSpec.describe ROTP::Arguments do
71
71
  end
72
72
  end
73
73
 
74
+ context 'generating a counter based secret' do
75
+ let(:argv) { %w(--time --secret s3same) }
76
+
77
+ describe '#options' do
78
+ it 'is in hmac 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
+
74
88
  context 'generating a time based secret' do
75
89
  let(:argv) { %w(--secret s3same) }
76
90
 
@@ -6,8 +6,8 @@ RSpec.describe ROTP::Base32 do
6
6
  context 'without arguments' do
7
7
  let(:base32) { ROTP::Base32.random_base32 }
8
8
 
9
- it 'is 16 characters long' do
10
- expect(base32.length).to eq 16
9
+ it 'is 32 characters long' do
10
+ expect(base32.length).to eq 32
11
11
  end
12
12
 
13
13
  it 'is hexadecimal' do
@@ -18,6 +18,30 @@ RSpec.describe ROTP::CLI do
18
18
  end
19
19
  end
20
20
 
21
+ context 'generating a TOTP with no secret' do
22
+ let(:argv) { %W(--time --secret) }
23
+
24
+ it 'prints the corresponding token' do
25
+ expect(output).to match 'You must also specify a --secret'
26
+ end
27
+ end
28
+
29
+ context 'generating a TOTP with bad base32 secret' do
30
+ let(:argv) { %W(--time --secret #{'1' * 32}) }
31
+
32
+ it 'prints the corresponding token' do
33
+ expect(output).to match 'Secret must be in RFC4648 Base32 format'
34
+ end
35
+ end
36
+
37
+ context 'trying to generate an unsupport type' do
38
+ let(:argv) { %W(--notreal --secret #{'a' * 32}) }
39
+
40
+ it 'prints the corresponding token' do
41
+ expect(output).to match 'invalid option: --notreal'
42
+ end
43
+ end
44
+
21
45
  context 'generating a HOTP' do
22
46
  let(:argv) { %W(--hmac --secret #{'a' * 32} --counter 1234) }
23
47
 
@@ -14,14 +14,6 @@ RSpec.describe ROTP::HOTP do
14
14
  end
15
15
  end
16
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
17
  context 'RFC compatibility' do
26
18
  let(:hotp) { ROTP::HOTP.new('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') }
27
19
 
@@ -69,6 +61,45 @@ RSpec.describe ROTP::HOTP do
69
61
  expect(hotp.verify(token, 10)).to be_falsey
70
62
  end
71
63
  end
64
+ describe 'with retries' do
65
+ let(:verification) { hotp.verify token, counter, retries:retries }
66
+
67
+ context 'counter outside than retries' do
68
+ let(:counter) { 1223 }
69
+ let(:retries) { 10 }
70
+
71
+ it 'is false' do
72
+ expect(verification).to be_falsey
73
+ end
74
+ end
75
+
76
+ context 'counter exactly in retry range' do
77
+ let(:counter) { 1224 }
78
+ let(:retries) { 10 }
79
+
80
+ it 'is true' do
81
+ expect(verification).to eq 1234
82
+ end
83
+ end
84
+
85
+ context 'counter in retry range' do
86
+ let(:counter) { 1224 }
87
+ let(:retries) { 11 }
88
+
89
+ it 'is true' do
90
+ expect(verification).to eq 1234
91
+ end
92
+ end
93
+
94
+ context 'counter ahead of token' do
95
+ let(:counter) { 1235 }
96
+ let(:retries) { 3 }
97
+
98
+ it 'is false' do
99
+ expect(verification).to be_falsey
100
+ end
101
+ end
102
+ end
72
103
  end
73
104
 
74
105
  describe '#provisioning_uri' do
@@ -98,59 +129,4 @@ RSpec.describe ROTP::HOTP do
98
129
  end
99
130
  end
100
131
 
101
- describe '#verify_with_retries' do
102
- let(:verification) { hotp.verify_with_retries token, counter, retries }
103
-
104
- context 'negative retries' do
105
- let(:retries) { -1 }
106
-
107
- it 'is false' do
108
- expect(verification).to be_falsey
109
- end
110
- end
111
-
112
- context 'zero retries' do
113
- let(:retries) { 0 }
114
-
115
- it 'is false' do
116
- expect(verification).to be_falsey
117
- end
118
- end
119
-
120
- context 'counter lower than retries' do
121
- let(:counter) { 1223 }
122
- let(:retries) { 10 }
123
-
124
- it 'is false' do
125
- expect(verification).to be_falsey
126
- end
127
- end
128
-
129
- context 'counter exactly in retry range' do
130
- let(:counter) { 1224 }
131
- let(:retries) { 10 }
132
-
133
- it 'is true' do
134
- expect(verification).to eq 1234
135
- end
136
- end
137
-
138
- context 'counter in retry range' do
139
- let(:counter) { 1224 }
140
- let(:retries) { 11 }
141
-
142
- it 'is true' do
143
- expect(verification).to eq 1234
144
- end
145
- end
146
-
147
- context 'counter too high' do
148
- let(:counter) { 1235 }
149
- let(:retries) { 3 }
150
-
151
- it 'is false' do
152
- expect(verification).to be_falsey
153
- end
154
- end
155
- end
156
132
  end
@@ -1,25 +1,18 @@
1
1
  require 'spec_helper'
2
2
 
3
+ TEST_TIME = Time.utc 2016,9,23,9 # 2016-09-23 09:00:00 UTC
4
+ TEST_TOKEN = "082630"
5
+
3
6
  RSpec.describe ROTP::TOTP do
4
- let(:now) { Time.utc 2012,1,1 }
5
- let(:token) { '068212' }
7
+ let(:now) { TEST_TIME }
8
+ let(:token) { TEST_TOKEN }
6
9
  let(:totp) { ROTP::TOTP.new 'JBSWY3DPEHPK3PXP' }
7
10
 
8
11
  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
12
+ let(:token) { totp.at now }
16
13
 
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
14
+ it 'is a string number' do
15
+ expect(token).to eq TEST_TOKEN
23
16
  end
24
17
 
25
18
  context 'RFC compatibility' do
@@ -35,26 +28,26 @@ RSpec.describe ROTP::TOTP do
35
28
  end
36
29
 
37
30
  describe '#verify' do
38
- let(:verification) { totp.verify token, now }
31
+ let(:verification) { totp.verify token, at: now }
39
32
 
40
33
  context 'numeric token' do
41
- let(:token) { 68212 }
34
+ let(:token) { 82630 }
42
35
 
43
- it 'raises an error' do
36
+ it 'raises an error with an integer' do
44
37
  expect { verification }.to raise_error(ArgumentError)
45
38
  end
46
39
  end
47
40
 
48
41
  context 'unpadded string token' do
49
- let(:token) { '68212' }
42
+ let(:token) { '82630' }
50
43
 
51
- it 'is false' do
44
+ it 'fails to verify' do
52
45
  expect(verification).to be_falsey
53
46
  end
54
47
  end
55
48
 
56
49
  context 'correctly padded string token' do
57
- it 'is true' do
50
+ it 'verifies' do
58
51
  expect(verification).to be_truthy
59
52
  end
60
53
  end
@@ -70,17 +63,168 @@ RSpec.describe ROTP::TOTP do
70
63
  let(:token) { '102705' }
71
64
  let(:now) { Time.at 1297553958 }
72
65
 
73
- it 'is true' do
66
+ it 'verifies' do
74
67
  expect(totp.verify('102705')).to be_truthy
75
68
  end
76
69
  end
77
70
 
78
71
  context 'wrong time based OTP' do
79
- it 'is false' do
72
+ it 'fails to verify' do
80
73
  expect(totp.verify('102705')).to be_falsey
81
74
  end
82
75
  end
83
76
  end
77
+ context 'invalidating reused tokens' do
78
+ let(:verification) {
79
+ totp.verify token,
80
+ after: after,
81
+ at: now
82
+ }
83
+ let(:after) { nil }
84
+
85
+ context 'passing in the `after` timestamp' do
86
+ let(:after) {
87
+ totp.verify TEST_TOKEN, after: nil, at: now
88
+ }
89
+
90
+ it 'returns a timecode' do
91
+ expect(after).to be_kind_of(Integer)
92
+ expect(after).to be_within(30).of(now.to_i)
93
+ end
94
+
95
+ context 'reusing same token' do
96
+ it 'is false' do
97
+ expect(verification).to be_falsy
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ def get_timecodes(at, b, a)
105
+ # Test the private method
106
+ totp.send('get_timecodes', at, b, a)
107
+ end
108
+
109
+ describe "drifting timecodes" do
110
+ it 'should get timecodes behind' do
111
+ expect(get_timecodes(TEST_TIME+15, 15, 0)).to eq([49154040])
112
+ expect(get_timecodes(TEST_TIME, 15, 0)).to eq([49154039, 49154040])
113
+ expect(get_timecodes(TEST_TIME, 40, 0)).to eq([49154038, 49154039, 49154040])
114
+ expect(get_timecodes(TEST_TIME, 90, 0)).to eq([49154037, 49154038, 49154039, 49154040])
115
+ end
116
+ it 'should get timecodes ahead' do
117
+ expect(get_timecodes(TEST_TIME, 0, 15)).to eq([49154040])
118
+ expect(get_timecodes(TEST_TIME+15, 0, 15)).to eq([49154040, 49154041])
119
+ expect(get_timecodes(TEST_TIME, 0, 30)).to eq([49154040, 49154041])
120
+ expect(get_timecodes(TEST_TIME, 0, 70)).to eq([49154040, 49154041, 49154042])
121
+ expect(get_timecodes(TEST_TIME, 0, 90)).to eq([49154040, 49154041, 49154042, 49154043])
122
+ end
123
+ it 'should get timecodes behind and ahead' do
124
+ expect(get_timecodes(TEST_TIME, 30, 30)).to eq([49154039, 49154040, 49154041])
125
+ expect(get_timecodes(TEST_TIME, 60, 60)).to eq([49154038, 49154039, 49154040, 49154041, 49154042])
126
+ end
127
+ end
128
+
129
+ describe '#verify with drift' do
130
+ let(:verification) { totp.verify token, drift_ahead: drift_ahead, drift_behind: drift_behind, at: now }
131
+ let(:drift_ahead) { 0 }
132
+ let(:drift_behind) { 0 }
133
+
134
+
135
+ context 'with an old OTP' do
136
+ let(:token) { totp.at TEST_TIME - 30 } # Previous token at 2016-09-23 08:59:30 UTC
137
+ let(:drift_behind) { 15 }
138
+
139
+ # Tested at 2016-09-23 09:00:00 UTC, and with drift back to 2016-09-23 08:59:45 UTC
140
+ # This would therefore include 2 intervals
141
+ it 'inside of drift range' do
142
+ expect(verification).to be_truthy
143
+ end
144
+
145
+ # Tested at 2016-09-23 09:00:20 UTC, and with drift back to 2016-09-23 09:00:05 UTC
146
+ # This only includes 1 interval, therefore only the current token is valid
147
+ context 'outside of drift range' do
148
+ let(:now) { TEST_TIME + 20 }
149
+
150
+ it 'is nil' do
151
+ expect(verification).to be_nil
152
+ end
153
+ end
154
+
155
+ end
156
+
157
+ context 'with a future OTP' do
158
+ let(:token) { totp.at TEST_TIME + 30 } # The next valid token - 2016-09-23 09:00:30 UTC
159
+ let(:drift_ahead) { 15 }
160
+
161
+ # Tested at 2016-09-23 09:00:00 UTC, and ahead to 2016-09-23 09:00:15 UTC
162
+ # This only includes 1 interval, therefore only the current token is valid
163
+ it 'outside of drift range' do
164
+ expect(verification).to be_falsey
165
+ end
166
+ # Tested at 2016-09-23 09:00:20 UTC, and with drift ahead to 2016-09-23 09:00:35 UTC
167
+ # This would therefore include 2 intervals
168
+ context 'inside of drift range' do
169
+ let(:now) { TEST_TIME + 20 }
170
+
171
+ it 'is true' do
172
+ expect(verification).to be_truthy
173
+ end
174
+ end
175
+ end
176
+
177
+ end
178
+
179
+ describe '#verify with drift and prevent token reuse' do
180
+ let(:verification) { totp.verify token, drift_ahead: drift_ahead, drift_behind: drift_behind, after: after, at: now }
181
+ let(:drift_ahead) { 0 }
182
+ let(:drift_behind) { 0 }
183
+ let(:after) { nil }
184
+
185
+ context 'with the `after` timestamp set' do
186
+
187
+ context 'older token' do
188
+ let(:token) { totp.at TEST_TIME - 30 }
189
+ let(:drift_behind) { 15 }
190
+
191
+ it 'is true' do
192
+ expect(verification).to be_truthy
193
+ expect(verification).to eq((TEST_TIME - 30).to_i)
194
+ end
195
+
196
+ context 'after it has been used' do
197
+ let(:after) {
198
+ totp.verify token, after: nil, at: now, drift_behind: drift_behind
199
+ }
200
+ it 'is false' do
201
+ expect(verification).to be_falsey
202
+ end
203
+ end
204
+
205
+ end
206
+
207
+ context 'newer token' do
208
+ let(:token) { totp.at TEST_TIME + 30 }
209
+ let(:drift_ahead) { 15 }
210
+ let(:now) { TEST_TIME + 15 }
211
+
212
+ it 'is true' do
213
+ expect(verification).to be_truthy
214
+ expect(verification).to eq((TEST_TIME + 30).to_i)
215
+ end
216
+
217
+ context 'after it has been used' do
218
+ let(:after) {
219
+ totp.verify token, after: nil, at: now, drift_ahead: drift_ahead
220
+ }
221
+ it 'is false' do
222
+ expect(verification).to be_falsey
223
+ end
224
+ end
225
+
226
+ end
227
+ end
84
228
  end
85
229
 
86
230
  describe '#provisioning_uri' do
@@ -161,115 +305,6 @@ RSpec.describe ROTP::TOTP do
161
305
 
162
306
  end
163
307
 
164
- describe 'invalid_verification with nil time as argument' do
165
- let(:verification) { totp.verify_with_drift token, drift, nil }
166
-
167
- context 'positive drift' do
168
- let(:token) { totp.at now - 30 }
169
- let(:drift) { 60 }
170
-
171
- it 'raises error' do
172
- expect do
173
- verification
174
- end.to raise_error(ArgumentError)
175
- end
176
- end
177
- end
178
-
179
- describe '#verify_with_drift' do
180
- let(:verification) { totp.verify_with_drift token, drift, now }
181
- let(:drift) { 0 }
182
-
183
-
184
- context 'slightly old number' do
185
- let(:token) { totp.at now - 30 }
186
- let(:drift) { 60 }
187
-
188
- it 'is true' do
189
- expect(verification).to be_truthy
190
- end
191
- end
192
-
193
- context 'slightly new number' do
194
- let(:token) { totp.at now + 60 }
195
- let(:drift) { 60 }
196
-
197
- it 'is true' do
198
- expect(verification).to be_truthy
199
- end
200
- end
201
-
202
- context 'outside of drift range' do
203
- let(:token) { totp.at now - 60 }
204
- let(:drift) { 30 }
205
-
206
- it 'is false' do
207
- expect(verification).to be_falsey
208
- end
209
- end
210
-
211
- context 'drift is not multiple of TOTP interval' do
212
- context 'slightly old number' do
213
- let(:token) { totp.at now - 45 }
214
- let(:drift) { 45 }
215
-
216
- it 'is true' do
217
- expect(verification).to be_truthy
218
- end
219
- end
220
-
221
- context 'slightly new number' do
222
- let(:token) { totp.at now + 40 }
223
- let(:drift) { 40 }
224
-
225
- it 'is true' do
226
- expect(verification).to be_truthy
227
- end
228
- end
229
- end
230
- end
231
-
232
- describe '#verify_with_drift_and_prior' do
233
- let(:verification) { totp.verify_with_drift_and_prior token, drift, prior, now }
234
- let(:drift) { 0 }
235
- let(:prior) { nil }
236
-
237
- context 'with a prior verify' do
238
- let(:prior) { totp.verify_with_drift_and_prior '068212', 0, nil, now }
239
-
240
- it 'returns a timecode' do
241
- expect(prior).to be_kind_of(Integer)
242
- expect(prior).to be_within(30).of(now.to_i)
243
- end
244
-
245
- context 'reusing same token' do
246
- it 'is false' do
247
- expect(verification).to be_falsy
248
- end
249
- end
250
-
251
- context 'newer token' do
252
- let(:token) { totp.at now + 40 }
253
- let(:drift) { 40 }
254
-
255
- it 'is true' do
256
- expect(verification).to be_kind_of(Integer)
257
- expect(verification).to be_within(30).of(now.to_i)
258
- expect(verification).to be_truthy
259
- end
260
- end
261
-
262
- context 'older token' do
263
- let(:token) { totp.at now - 40 }
264
- let(:drift) { 40 }
265
-
266
- it 'is false' do
267
- expect(verification).to be_falsy
268
- end
269
- end
270
- end
271
- end
272
-
273
308
  describe '#now' do
274
309
  before do
275
310
  Timecop.freeze now
@@ -1,6 +1,10 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter "/spec/"
4
+ end
5
+
1
6
  require 'rotp'
2
7
  require 'timecop'
3
- require 'simplecov'
4
8
 
5
9
  RSpec.configure do |config|
6
10
  config.disable_monkey_patching!
@@ -13,6 +17,5 @@ RSpec.configure do |config|
13
17
  end
14
18
  end
15
19
 
16
- SimpleCov.start
17
20
 
18
21
  require_relative '../lib/rotp'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rotp
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.1
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Percival
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-02 00:00:00.000000000 Z
11
+ date: 2018-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -140,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
140
  version: '0'
141
141
  requirements: []
142
142
  rubyforge_project: rotp
143
- rubygems_version: 2.2.2
143
+ rubygems_version: 2.7.6
144
144
  signing_key:
145
145
  specification_version: 4
146
146
  summary: A Ruby library for generating and verifying one time passwords
@@ -151,4 +151,3 @@ test_files:
151
151
  - spec/lib/rotp/hotp_spec.rb
152
152
  - spec/lib/rotp/totp_spec.rb
153
153
  - spec/spec_helper.rb
154
- has_rdoc: