rotp 2.1.1 → 6.2.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rotp might be problematic. Click here for more details.

Files changed (53) hide show
  1. checksums.yaml +5 -5
  2. data/.devcontainer/Dockerfile +19 -0
  3. data/.devcontainer/devcontainer.json +37 -0
  4. data/.dockerignore +1 -0
  5. data/.github/workflows/test.yaml +27 -0
  6. data/.gitignore +2 -0
  7. data/CHANGELOG.md +95 -0
  8. data/Dockerfile-2.3 +10 -0
  9. data/Dockerfile-2.7 +11 -0
  10. data/Dockerfile-3.0-rc +12 -0
  11. data/Guardfile +1 -1
  12. data/README.md +125 -31
  13. data/bin/rotp +1 -1
  14. data/docker-compose.yml +37 -0
  15. data/lib/rotp/arguments.rb +6 -5
  16. data/lib/rotp/base32.rb +56 -29
  17. data/lib/rotp/cli.rb +6 -10
  18. data/lib/rotp/hotp.rb +11 -26
  19. data/lib/rotp/otp/uri.rb +79 -0
  20. data/lib/rotp/otp.rb +20 -31
  21. data/lib/rotp/totp.rb +43 -29
  22. data/lib/rotp/version.rb +1 -1
  23. data/lib/rotp.rb +2 -3
  24. data/rotp.gemspec +15 -18
  25. data/spec/lib/rotp/arguments_spec.rb +18 -5
  26. data/spec/lib/rotp/base32_spec.rb +51 -19
  27. data/spec/lib/rotp/cli_spec.rb +42 -3
  28. data/spec/lib/rotp/hotp_spec.rb +39 -60
  29. data/spec/lib/rotp/otp/uri_spec.rb +99 -0
  30. data/spec/lib/rotp/totp_spec.rb +138 -120
  31. data/spec/spec_helper.rb +7 -0
  32. metadata +27 -45
  33. data/.travis.yml +0 -7
  34. data/Gemfile.lock +0 -75
  35. data/Rakefile +0 -9
  36. data/doc/ROTP/HOTP.html +0 -308
  37. data/doc/ROTP/OTP.html +0 -593
  38. data/doc/ROTP/TOTP.html +0 -493
  39. data/doc/Rotp.html +0 -179
  40. data/doc/_index.html +0 -144
  41. data/doc/class_list.html +0 -36
  42. data/doc/css/common.css +0 -1
  43. data/doc/css/full_list.css +0 -53
  44. data/doc/css/style.css +0 -310
  45. data/doc/file.README.html +0 -89
  46. data/doc/file_list.html +0 -38
  47. data/doc/frames.html +0 -13
  48. data/doc/index.html +0 -89
  49. data/doc/js/app.js +0 -203
  50. data/doc/js/full_list.js +0 -149
  51. data/doc/js/jquery.js +0 -154
  52. data/doc/method_list.html +0 -155
  53. data/doc/top-level-namespace.html +0 -88
data/lib/rotp/base32.rb CHANGED
@@ -1,49 +1,76 @@
1
+ require 'securerandom'
2
+
1
3
  module ROTP
2
4
  class Base32
3
5
  class Base32Error < RuntimeError; end
4
- CHARS = "abcdefghijklmnopqrstuvwxyz234567".each_char.to_a
6
+ CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.each_char.to_a
7
+ SHIFT = 5
8
+ MASK = 31
5
9
 
6
10
  class << self
11
+
7
12
  def decode(str)
8
- output = []
9
- str.scan(/.{1,8}/).each do |block|
10
- char_array = decode_block(block).map{|c| c.chr}
11
- output << char_array
13
+ buffer = 0
14
+ idx = 0
15
+ bits_left = 0
16
+ str = str.tr('=', '').upcase
17
+ result = []
18
+ str.split('').each do |char|
19
+ buffer = buffer << SHIFT
20
+ buffer = buffer | (decode_quint(char) & MASK)
21
+ bits_left = bits_left + SHIFT
22
+ if bits_left >= 8
23
+ result[idx] = (buffer >> (bits_left - 8)) & 255
24
+ idx = idx + 1
25
+ bits_left = bits_left - 8
26
+ end
12
27
  end
13
- output.join
28
+ result.pack('c*')
14
29
  end
15
30
 
16
- def random_base32(length=16)
17
- b32 = ''
18
- OpenSSL::Random.random_bytes(length).each_byte do |b|
19
- b32 << CHARS[b % 32]
31
+ def encode(b)
32
+ data = b.unpack('c*')
33
+ out = String.new
34
+ buffer = data[0]
35
+ idx = 1
36
+ bits_left = 8
37
+ while bits_left > 0 || idx < data.length
38
+ if bits_left < SHIFT
39
+ if idx < data.length
40
+ buffer = buffer << 8
41
+ buffer = buffer | (data[idx] & 255)
42
+ bits_left = bits_left + 8
43
+ idx = idx + 1
44
+ else
45
+ pad = SHIFT - bits_left
46
+ buffer = buffer << pad
47
+ bits_left = bits_left + pad
48
+ end
49
+ end
50
+ val = MASK & (buffer >> (bits_left - SHIFT))
51
+ bits_left = bits_left - SHIFT
52
+ out.concat(CHARS[val])
20
53
  end
21
- b32
54
+ return out
22
55
  end
23
56
 
24
- private
57
+ # Defaults to 160 bit long secret (meaning a 32 character long base32 secret)
58
+ def random(byte_length = 20)
59
+ rand_bytes = SecureRandom.random_bytes(byte_length)
60
+ self.encode(rand_bytes)
61
+ end
25
62
 
26
- def decode_block(block)
27
- length = block.scan(/[^=]/).length
28
- quints = block.each_char.map {|c| decode_quint(c)}
29
- bytes = []
30
- bytes[0] = (quints[0] << 3) + (quints[1] ? quints[1] >> 2 : 0)
31
- return bytes if length < 3
32
- bytes[1] = ((quints[1] & 3) << 6) + (quints[2] << 1) + (quints[3] ? quints[3] >> 4 : 0)
33
- return bytes if length < 4
34
- bytes[2] = ((quints[3] & 15) << 4) + (quints[4] ? quints[4] >> 1 : 0)
35
- return bytes if length < 6
36
- bytes[3] = ((quints[4] & 1) << 7) + (quints[5] << 2) + (quints[6] ? quints[6] >> 3 : 0)
37
- return bytes if length < 7
38
- bytes[4] = ((quints[6] & 7) << 5) + (quints[7] || 0)
39
- bytes
63
+ # Prevent breaking changes
64
+ def random_base32(str_len = 32)
65
+ byte_length = str_len * 5/8
66
+ random(byte_length)
40
67
  end
41
68
 
69
+ private
70
+
42
71
  def decode_quint(q)
43
- CHARS.index(q.downcase) or raise(Base32Error, "Invalid Base32 Character - '#{q}'")
72
+ CHARS.index(q) || raise(Base32Error, "Invalid Base32 Character - '#{q}'")
44
73
  end
45
-
46
74
  end
47
-
48
75
  end
49
76
  end
data/lib/rotp/cli.rb CHANGED
@@ -9,19 +9,19 @@ 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
- if [:time, :hmac].include?(options.mode)
19
+ if %i[time hmac].include?(options.mode)
18
20
  if options.secret.to_s == ''
19
21
  red 'You must also specify a --secret. Try --help for help.'
20
- elsif options.secret.to_s.chars.any? { |c| ROTP::Base32::CHARS.index(c.downcase) == nil }
22
+ elsif options.secret.to_s.chars.any? { |c| ROTP::Base32::CHARS.index(c.upcase).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
 
@@ -31,12 +31,9 @@ module ROTP
31
31
  return arguments.to_s if options.mode == :help
32
32
 
33
33
  if options.mode == :time
34
- ROTP::TOTP.new(options.secret).now
34
+ ROTP::TOTP.new(options.secret, options).now
35
35
  elsif options.mode == :hmac
36
- ROTP::HOTP.new(options.secret).at options.counter
37
-
38
- else
39
- fail NotImplementedError
36
+ ROTP::HOTP.new(options.secret, options).at options.counter
40
37
  end
41
38
  end
42
39
 
@@ -51,6 +48,5 @@ module ROTP
51
48
  def red(string)
52
49
  "\033[31m#{string}\033[0m"
53
50
  end
54
-
55
51
  end
56
52
  end
data/lib/rotp/hotp.rb CHANGED
@@ -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)
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 do |c|
17
+ super(otp, at(c))
29
18
  end
30
-
31
- false
32
19
  end
33
20
 
34
21
  # Returns the provisioning URI for the OTP
@@ -37,10 +24,8 @@ module ROTP
37
24
  # @param [String] name of the account
38
25
  # @param [Integer] initial_count starting counter value, defaults to 0
39
26
  # @return [String] provisioning uri
40
- def provisioning_uri(name, initial_count=0)
41
- encode_params("otpauth://hotp/#{URI.encode(name)}", :secret=>secret, :counter=>initial_count)
27
+ def provisioning_uri(name, initial_count = 0)
28
+ OTP::URI.new(self, account_name: name, counter: initial_count).to_s
42
29
  end
43
-
44
30
  end
45
-
46
31
  end
@@ -0,0 +1,79 @@
1
+ module ROTP
2
+ class OTP
3
+ # https://github.com/google/google-authenticator/wiki/Key-Uri-Format
4
+ class URI
5
+ def initialize(otp, account_name:, counter: nil)
6
+ @otp = otp
7
+ @account_name = account_name
8
+ @counter = counter
9
+ end
10
+
11
+ def to_s
12
+ "otpauth://#{type}/#{label}?#{parameters}"
13
+ end
14
+
15
+ private
16
+
17
+ def algorithm
18
+ return unless %w[sha256 sha512].include?(@otp.digest)
19
+
20
+ @otp.digest.upcase
21
+ end
22
+
23
+ def counter
24
+ return if @otp.is_a?(TOTP)
25
+ fail if @counter.nil?
26
+
27
+ @counter
28
+ end
29
+
30
+ def digits
31
+ return if @otp.digits == DEFAULT_DIGITS
32
+
33
+ @otp.digits
34
+ end
35
+
36
+ def issuer
37
+ return if @otp.is_a?(HOTP)
38
+
39
+ @otp.issuer&.strip&.tr(':', '_')
40
+ end
41
+
42
+ def label
43
+ [issuer, @account_name.rstrip]
44
+ .compact
45
+ .map { |s| s.tr(':', '_') }
46
+ .map { |s| ERB::Util.url_encode(s) }
47
+ .join(':')
48
+ end
49
+
50
+ def parameters
51
+ {
52
+ secret: @otp.secret,
53
+ issuer: issuer,
54
+ algorithm: algorithm,
55
+ digits: digits,
56
+ period: period,
57
+ counter: counter,
58
+ }
59
+ .reject { |_, v| v.nil? }
60
+ .map { |k, v| "#{k}=#{ERB::Util.url_encode(v)}" }
61
+ .join('&')
62
+ end
63
+
64
+ def period
65
+ return if @otp.is_a?(HOTP)
66
+ return if @otp.interval == DEFAULT_INTERVAL
67
+
68
+ @otp.interval
69
+ end
70
+
71
+ def type
72
+ case @otp
73
+ when TOTP then 'totp'
74
+ when HOTP then 'hotp'
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
data/lib/rotp/otp.rb CHANGED
@@ -1,26 +1,26 @@
1
1
  module ROTP
2
2
  class OTP
3
3
  attr_reader :secret, :digits, :digest
4
+ DEFAULT_DIGITS = 6
4
5
 
5
6
  # @param [String] secret in the form of base32
6
7
  # @option options digits [Integer] (6)
7
- # Number of integers in the OTP
8
+ # Number of integers in the OTP.
8
9
  # Google Authenticate only supports 6 currently
9
10
  # @option options digest [String] (sha1)
10
- # Digest used in the HMAC
11
+ # Digest used in the HMAC.
11
12
  # Google Authenticate only supports 'sha1' currently
12
13
  # @returns [OTP] OTP instantiation
13
14
  def initialize(s, options = {})
14
- @digits = options[:digits] || 6
15
- @digest = options[:digest] || "sha1"
15
+ @digits = options[:digits] || DEFAULT_DIGITS
16
+ @digest = options[:digest] || 'sha1'
16
17
  @secret = s
17
18
  end
18
19
 
19
20
  # @param [Integer] input the number used seed the HMAC
20
- # @option padded [Boolean] (false) Output the otp as a 0 padded string
21
21
  # Usually either the counter, or the computed integer
22
22
  # based on the Unix timestamp
23
- def generate_otp(input, padded=true)
23
+ def generate_otp(input)
24
24
  hmac = OpenSSL::HMAC.digest(
25
25
  OpenSSL::Digest.new(digest),
26
26
  byte_secret,
@@ -29,22 +29,19 @@ module ROTP
29
29
 
30
30
  offset = hmac[-1].ord & 0xf
31
31
  code = (hmac[offset].ord & 0x7f) << 24 |
32
- (hmac[offset + 1].ord & 0xff) << 16 |
33
- (hmac[offset + 2].ord & 0xff) << 8 |
34
- (hmac[offset + 3].ord & 0xff)
35
- if padded
36
- (code % 10 ** digits).to_s.rjust(digits, '0')
37
- else
38
- code % 10 ** digits
39
- end
32
+ (hmac[offset + 1].ord & 0xff) << 16 |
33
+ (hmac[offset + 2].ord & 0xff) << 8 |
34
+ (hmac[offset + 3].ord & 0xff)
35
+ code_str = (10 ** digits + (code % 10 ** digits)).to_s
36
+ code_str[-digits..-1]
40
37
  end
41
38
 
42
39
  private
43
40
 
44
41
  def verify(input, generated)
45
- unless input.is_a?(String) && generated.is_a?(String)
46
- raise ArgumentError, "ROTP only verifies strings - See: https://github.com/mdp/rotp/issues/32"
47
- end
42
+ raise ArgumentError, '`otp` should be a String' unless
43
+ input.is_a?(String)
44
+
48
45
  time_constant_compare(input, generated)
49
46
  end
50
47
 
@@ -57,34 +54,26 @@ module ROTP
57
54
  # along with the secret
58
55
  #
59
56
  def int_to_bytestring(int, padding = 8)
57
+ unless int >= 0
58
+ raise ArgumentError, '#int_to_bytestring requires a positive number'
59
+ end
60
+
60
61
  result = []
61
62
  until int == 0
62
63
  result << (int & 0xFF).chr
63
- int >>= 8
64
+ int >>= 8
64
65
  end
65
66
  result.reverse.join.rjust(padding, 0.chr)
66
67
  end
67
68
 
68
- # A very simple param encoder
69
- def encode_params(uri, params)
70
- params_str = "?"
71
- params.each do |k,v|
72
- if v
73
- params_str << "#{k}=#{CGI::escape(v.to_s)}&"
74
- end
75
- end
76
- params_str.chop!
77
- uri + params_str
78
- end
79
-
80
69
  # constant-time compare the strings
81
70
  def time_constant_compare(a, b)
82
71
  return false if a.empty? || b.empty? || a.bytesize != b.bytesize
72
+
83
73
  l = a.unpack "C#{a.bytesize}"
84
74
  res = 0
85
75
  b.each_byte { |byte| res |= byte ^ l.shift }
86
76
  res == 0
87
77
  end
88
-
89
78
  end
90
79
  end
data/lib/rotp/totp.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  module ROTP
2
2
  DEFAULT_INTERVAL = 30
3
3
  class TOTP < OTP
4
-
5
4
  attr_reader :interval, :issuer
6
5
 
7
6
  # @option options [Integer] interval (30) the time interval in seconds for OTP
@@ -14,54 +13,69 @@ module ROTP
14
13
 
15
14
  # Accepts either a Unix timestamp integer or a Time object.
16
15
  # 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
- generate_otp(timecode(time), padding)
16
+ # @param time [Time/Integer] the time to generate an OTP for, integer unix timestamp or Time object
17
+ def at(time)
18
+ generate_otp(timecode(time))
24
19
  end
25
20
 
26
21
  # Generate the current time OTP
27
22
  # @return [Integer] the OTP as an integer
28
- def now(padding=true)
29
- generate_otp(timecode(Time.now), padding)
23
+ def now
24
+ generate_otp(timecode(Time.now))
30
25
  end
31
26
 
32
27
  # Verifies the OTP passed in against the current time OTP
33
- # @param [String/Integer] otp the OTP to check against
34
- def verify(otp, time = Time.now)
35
- super(otp, self.at(time))
36
- end
28
+ # and adjacent intervals up to +drift+. Excludes OTPs
29
+ # from `after` and earlier. Returns time value of
30
+ # matching OTP code for use in subsequent call.
31
+ # @param otp [String] the one time password to verify
32
+ # @param drift_behind [Integer] how many seconds to look back
33
+ # @param drift_ahead [Integer] how many seconds to look ahead
34
+ # @param after [Integer] prevent token reuse, last login timestamp
35
+ # @param at [Time] time at which to generate and verify a particular
36
+ # otp. default Time.now
37
+ # @return [Integer, nil] the last successful timestamp
38
+ # interval
39
+ def verify(otp, drift_ahead: 0, drift_behind: 0, after: nil, at: Time.now)
40
+ timecodes = get_timecodes(at, drift_behind, drift_ahead)
37
41
 
38
- # Verifies the OTP passed in against the current time OTP
39
- # and adjacent intervals up to +drift+.
40
- # @param [String] otp the OTP to check against
41
- # @param [Integer] drift the number of seconds that the client
42
- # and server are allowed to drift apart
43
- def verify_with_drift(otp, drift, time = Time.now)
44
- time = time.to_i
45
- times = (time-drift..time+drift).step(interval).to_a
46
- times << time + drift if times.last < time + drift
47
- times.any? { |ti| verify(otp, ti) }
42
+ timecodes = timecodes.select { |t| t > timecode(after) } if after
43
+
44
+ result = nil
45
+ timecodes.each do |t|
46
+ result = t * interval if super(otp, generate_otp(t))
47
+ end
48
+ result
48
49
  end
49
50
 
50
51
  # Returns the provisioning URI for the OTP
51
52
  # This can then be encoded in a QR Code and used
52
53
  # to provision the Google Authenticator app
53
54
  # @param [String] name of the account
54
- # @return [String] provisioning uri
55
+ # @return [String] provisioning URI
55
56
  def provisioning_uri(name)
56
- encode_params("otpauth://totp/#{URI.encode(name)}",
57
- :secret => secret, :period => (interval==30 ? nil : interval), :issuer => issuer)
57
+ OTP::URI.new(self, account_name: name).to_s
58
58
  end
59
59
 
60
60
  private
61
61
 
62
- def timecode(time)
63
- time.utc.to_i / interval
62
+ # Get back an array of timecodes for a period
63
+ def get_timecodes(at, drift_behind, drift_ahead)
64
+ now = timeint(at)
65
+ timecode_start = timecode(now - drift_behind)
66
+ timecode_end = timecode(now + drift_ahead)
67
+ (timecode_start..timecode_end).step(1).to_a
68
+ end
69
+
70
+ # Ensure UTC int
71
+ def timeint(time)
72
+ return time.to_i unless time.class == Time
73
+
74
+ time.utc.to_i
64
75
  end
65
76
 
77
+ def timecode(time)
78
+ timeint(time) / interval
79
+ end
66
80
  end
67
81
  end
data/lib/rotp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module ROTP
2
- VERSION = "2.1.1"
2
+ VERSION = '6.2.2'.freeze
3
3
  end
data/lib/rotp.rb CHANGED
@@ -1,11 +1,10 @@
1
- require 'cgi'
2
- require 'uri'
3
1
  require 'openssl'
2
+ require 'erb'
4
3
  require 'rotp/base32'
5
4
  require 'rotp/otp'
5
+ require 'rotp/otp/uri'
6
6
  require 'rotp/hotp'
7
7
  require 'rotp/totp'
8
8
 
9
-
10
9
  module ROTP
11
10
  end
data/rotp.gemspec CHANGED
@@ -1,27 +1,24 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "rotp/version"
1
+ require './lib/rotp/version'
4
2
 
5
3
  Gem::Specification.new do |s|
6
- s.name = "rotp"
4
+ s.name = 'rotp'
7
5
  s.version = ROTP::VERSION
8
6
  s.platform = Gem::Platform::RUBY
9
- s.license = "MIT"
10
- s.authors = ["Mark Percival"]
11
- s.email = ["mark@markpercival.us"]
12
- s.homepage = "http://github.com/mdp/rotp"
13
- s.summary = %q{A Ruby library for generating and verifying one time passwords}
14
- s.description = %q{Works for both HOTP and TOTP, and includes QR Code provisioning}
15
-
16
- s.rubyforge_project = "rotp"
7
+ s.required_ruby_version = '>= 2.3'
8
+ s.license = 'MIT'
9
+ s.authors = ['Mark Percival']
10
+ s.email = ['mark@markpercival.us']
11
+ s.homepage = 'https://github.com/mdp/rotp'
12
+ s.summary = 'A Ruby library for generating and verifying one time passwords'
13
+ s.description = 'Works for both HOTP and TOTP, and includes QR Code provisioning'
17
14
 
18
15
  s.files = `git ls-files`.split("\n")
19
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
- s.require_paths = ["lib"]
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
18
+ s.require_paths = ['lib']
22
19
 
23
- s.add_development_dependency 'guard-rspec', '~> 4.5'
24
- s.add_development_dependency 'rake', '~> 10.4'
25
- s.add_development_dependency 'rspec', '~> 3.1'
26
- s.add_development_dependency 'timecop', '~> 0.7'
20
+ s.add_development_dependency "rake", "~> 13.0"
21
+ s.add_development_dependency 'rspec', '~> 3.5'
22
+ s.add_development_dependency 'simplecov', '~> 0.12'
23
+ s.add_development_dependency 'timecop', '~> 0.8'
27
24
  end
@@ -24,7 +24,7 @@ RSpec.describe ROTP::Arguments do
24
24
  end
25
25
 
26
26
  context 'unknown arguments' do
27
- let(:argv) { %w(--does-not-exist -xyz) }
27
+ let(:argv) { %w[--does-not-exist -xyz] }
28
28
 
29
29
  describe '#options' do
30
30
  it 'is in help mode' do
@@ -48,7 +48,7 @@ RSpec.describe ROTP::Arguments do
48
48
  end
49
49
 
50
50
  context 'asking for help' do
51
- let(:argv) { %w(--help) }
51
+ let(:argv) { %w[--help] }
52
52
 
53
53
  describe '#options' do
54
54
  it 'is in help mode' do
@@ -58,7 +58,7 @@ RSpec.describe ROTP::Arguments do
58
58
  end
59
59
 
60
60
  context 'generating a counter based secret' do
61
- let(:argv) { %w(--hmac --secret s3same) }
61
+ let(:argv) { %w[--hmac --secret s3same] }
62
62
 
63
63
  describe '#options' do
64
64
  it 'is in hmac mode' do
@@ -71,8 +71,22 @@ 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
- let(:argv) { %w(--secret s3same) }
89
+ let(:argv) { %w[--secret s3same] }
76
90
 
77
91
  describe '#options' do
78
92
  it 'is in time mode' do
@@ -84,5 +98,4 @@ RSpec.describe ROTP::Arguments do
84
98
  end
85
99
  end
86
100
  end
87
-
88
101
  end