rotp 3.3.1 → 4.0.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 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: