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.
- checksums.yaml +5 -5
- data/.devcontainer/Dockerfile +19 -0
- data/.devcontainer/devcontainer.json +37 -0
- data/.dockerignore +1 -0
- data/.github/workflows/test.yaml +27 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +95 -0
- data/Dockerfile-2.3 +10 -0
- data/Dockerfile-2.7 +11 -0
- data/Dockerfile-3.0-rc +12 -0
- data/Guardfile +1 -1
- data/README.md +125 -31
- data/bin/rotp +1 -1
- data/docker-compose.yml +37 -0
- data/lib/rotp/arguments.rb +6 -5
- data/lib/rotp/base32.rb +56 -29
- data/lib/rotp/cli.rb +6 -10
- data/lib/rotp/hotp.rb +11 -26
- data/lib/rotp/otp/uri.rb +79 -0
- data/lib/rotp/otp.rb +20 -31
- data/lib/rotp/totp.rb +43 -29
- data/lib/rotp/version.rb +1 -1
- data/lib/rotp.rb +2 -3
- data/rotp.gemspec +15 -18
- data/spec/lib/rotp/arguments_spec.rb +18 -5
- data/spec/lib/rotp/base32_spec.rb +51 -19
- data/spec/lib/rotp/cli_spec.rb +42 -3
- data/spec/lib/rotp/hotp_spec.rb +39 -60
- data/spec/lib/rotp/otp/uri_spec.rb +99 -0
- data/spec/lib/rotp/totp_spec.rb +138 -120
- data/spec/spec_helper.rb +7 -0
- metadata +27 -45
- data/.travis.yml +0 -7
- data/Gemfile.lock +0 -75
- data/Rakefile +0 -9
- data/doc/ROTP/HOTP.html +0 -308
- data/doc/ROTP/OTP.html +0 -593
- data/doc/ROTP/TOTP.html +0 -493
- data/doc/Rotp.html +0 -179
- data/doc/_index.html +0 -144
- data/doc/class_list.html +0 -36
- data/doc/css/common.css +0 -1
- data/doc/css/full_list.css +0 -53
- data/doc/css/style.css +0 -310
- data/doc/file.README.html +0 -89
- data/doc/file_list.html +0 -38
- data/doc/frames.html +0 -13
- data/doc/index.html +0 -89
- data/doc/js/app.js +0 -203
- data/doc/js/full_list.js +0 -149
- data/doc/js/jquery.js +0 -154
- data/doc/method_list.html +0 -155
- 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 =
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
28
|
+
result.pack('c*')
|
14
29
|
end
|
15
30
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
54
|
+
return out
|
22
55
|
end
|
23
56
|
|
24
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
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 [
|
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.
|
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
|
8
|
-
generate_otp(count
|
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]
|
13
|
-
# @param [Integer]
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
data/lib/rotp/otp/uri.rb
ADDED
@@ -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] ||
|
15
|
-
@digest = options[:digest] ||
|
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
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
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 >>=
|
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]
|
18
|
-
|
19
|
-
|
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
|
29
|
-
generate_otp(timecode(Time.now)
|
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
|
-
#
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
55
|
+
# @return [String] provisioning URI
|
55
56
|
def provisioning_uri(name)
|
56
|
-
|
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
|
-
|
63
|
-
|
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
data/lib/rotp.rb
CHANGED
data/rotp.gemspec
CHANGED
@@ -1,27 +1,24 @@
|
|
1
|
-
|
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 =
|
4
|
+
s.name = 'rotp'
|
7
5
|
s.version = ROTP::VERSION
|
8
6
|
s.platform = Gem::Platform::RUBY
|
9
|
-
s.
|
10
|
-
s.
|
11
|
-
s.
|
12
|
-
s.
|
13
|
-
s.
|
14
|
-
s.
|
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 = [
|
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
|
24
|
-
s.add_development_dependency '
|
25
|
-
s.add_development_dependency '
|
26
|
-
s.add_development_dependency 'timecop', '~> 0.
|
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
|
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
|
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
|
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
|
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
|