rotp 4.0.0 → 6.3.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 +4 -4
- data/.devcontainer/Dockerfile +19 -0
- data/.devcontainer/devcontainer.json +41 -0
- data/.github/workflows/release.yaml +36 -0
- data/.github/workflows/test.yaml +26 -0
- data/.release-please-manifest.json +3 -0
- data/CHANGELOG.md +91 -24
- data/Dockerfile-2.3 +1 -7
- data/Dockerfile-2.7 +11 -0
- data/Dockerfile-3.0 +12 -0
- data/Guardfile +1 -1
- data/README.md +64 -15
- data/bin/rotp +1 -1
- data/docker-compose.yml +37 -0
- data/lib/rotp/arguments.rb +6 -5
- data/lib/rotp/base32.rb +56 -30
- data/lib/rotp/cli.rb +4 -5
- data/lib/rotp/hotp.rb +6 -13
- data/lib/rotp/otp/uri.rb +78 -0
- data/lib/rotp/otp.rb +26 -25
- data/lib/rotp/totp.rb +13 -33
- data/lib/rotp/version.rb +1 -1
- data/lib/rotp.rb +2 -4
- data/release-please-config.json +12 -0
- data/rotp.gemspec +13 -15
- data/spec/lib/rotp/arguments_spec.rb +5 -6
- data/spec/lib/rotp/base32_spec.rb +45 -19
- data/spec/lib/rotp/cli_spec.rb +21 -6
- data/spec/lib/rotp/hotp_spec.rb +38 -17
- data/spec/lib/rotp/otp/uri_spec.rb +99 -0
- data/spec/lib/rotp/totp_spec.rb +61 -98
- data/spec/spec_helper.rb +1 -2
- metadata +25 -43
- data/.travis.yml +0 -8
- data/Dockerfile-1.9 +0 -15
- data/Dockerfile-2.1 +0 -16
- 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,50 +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
|
-
|
12
|
-
|
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
|
13
27
|
end
|
14
|
-
|
28
|
+
result.pack('c*')
|
15
29
|
end
|
16
30
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
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])
|
21
53
|
end
|
22
|
-
|
54
|
+
return out
|
23
55
|
end
|
24
56
|
|
25
|
-
|
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
|
26
62
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
bytes[0] = (quints[0] << 3) + (quints[1] ? quints[1] >> 2 : 0)
|
32
|
-
return bytes if length < 3
|
33
|
-
bytes[1] = ((quints[1] & 3) << 6) + (quints[2] << 1) + (quints[3] ? quints[3] >> 4 : 0)
|
34
|
-
return bytes if length < 4
|
35
|
-
bytes[2] = ((quints[3] & 15) << 4) + (quints[4] ? quints[4] >> 1 : 0)
|
36
|
-
return bytes if length < 6
|
37
|
-
bytes[3] = ((quints[4] & 1) << 7) + (quints[5] << 2) + (quints[6] ? quints[6] >> 3 : 0)
|
38
|
-
return bytes if length < 7
|
39
|
-
bytes[4] = ((quints[6] & 7) << 5) + (quints[7] || 0)
|
40
|
-
bytes
|
63
|
+
# Prevent breaking changes
|
64
|
+
def random_base32(str_len = 32)
|
65
|
+
byte_length = str_len * 5/8
|
66
|
+
random(byte_length)
|
41
67
|
end
|
42
68
|
|
69
|
+
private
|
70
|
+
|
43
71
|
def decode_quint(q)
|
44
|
-
CHARS.index(q
|
72
|
+
CHARS.index(q) || raise(Base32Error, "Invalid Base32 Character - '#{q}'")
|
45
73
|
end
|
46
|
-
|
47
74
|
end
|
48
|
-
|
49
75
|
end
|
50
76
|
end
|
data/lib/rotp/cli.rb
CHANGED
@@ -16,10 +16,10 @@ module ROTP
|
|
16
16
|
# :nocov:
|
17
17
|
|
18
18
|
def errors
|
19
|
-
if [
|
19
|
+
if %i[time hmac].include?(options.mode)
|
20
20
|
if options.secret.to_s == ''
|
21
21
|
red 'You must also specify a --secret. Try --help for help.'
|
22
|
-
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? }
|
23
23
|
red 'Secret must be in RFC4648 Base32 format - http://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet'
|
24
24
|
end
|
25
25
|
end
|
@@ -31,9 +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
|
36
|
+
ROTP::HOTP.new(options.secret, options).at options.counter
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
@@ -48,6 +48,5 @@ module ROTP
|
|
48
48
|
def red(string)
|
49
49
|
"\033[31m#{string}\033[0m"
|
50
50
|
end
|
51
|
-
|
52
51
|
end
|
53
52
|
end
|
data/lib/rotp/hotp.rb
CHANGED
@@ -12,10 +12,10 @@ module ROTP
|
|
12
12
|
# @param counter [Integer] the counter of the OTP
|
13
13
|
# @param retries [Integer] number of counters to incrementally retry
|
14
14
|
def verify(otp, counter, retries: 0)
|
15
|
-
counters = (counter..counter+retries).to_a
|
16
|
-
counters.find
|
17
|
-
super(otp,
|
18
|
-
|
15
|
+
counters = (counter..counter + retries).to_a
|
16
|
+
counters.find do |c|
|
17
|
+
super(otp, at(c))
|
18
|
+
end
|
19
19
|
end
|
20
20
|
|
21
21
|
# Returns the provisioning URI for the OTP
|
@@ -24,15 +24,8 @@ module ROTP
|
|
24
24
|
# @param [String] name of the account
|
25
25
|
# @param [Integer] initial_count starting counter value, defaults to 0
|
26
26
|
# @return [String] provisioning uri
|
27
|
-
def provisioning_uri(name, initial_count=0)
|
28
|
-
|
29
|
-
secret: secret,
|
30
|
-
counter: initial_count,
|
31
|
-
digits: digits == DEFAULT_DIGITS ? nil : digits
|
32
|
-
}
|
33
|
-
encode_params("otpauth://hotp/#{URI.encode(name)}", params)
|
27
|
+
def provisioning_uri(name = nil, initial_count = 0)
|
28
|
+
OTP::URI.new(self, account_name: name || @name, counter: initial_count).to_s
|
34
29
|
end
|
35
|
-
|
36
30
|
end
|
37
|
-
|
38
31
|
end
|
data/lib/rotp/otp/uri.rb
ADDED
@@ -0,0 +1,78 @@
|
|
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: nil, 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
|
+
@otp.issuer&.strip&.tr(':', '_')
|
38
|
+
end
|
39
|
+
|
40
|
+
def label
|
41
|
+
[issuer, @account_name.rstrip]
|
42
|
+
.compact
|
43
|
+
.map { |s| s.tr(':', '_') }
|
44
|
+
.map { |s| ERB::Util.url_encode(s) }
|
45
|
+
.join(':')
|
46
|
+
end
|
47
|
+
|
48
|
+
def parameters
|
49
|
+
{
|
50
|
+
secret: @otp.secret,
|
51
|
+
issuer: issuer,
|
52
|
+
algorithm: algorithm,
|
53
|
+
digits: digits,
|
54
|
+
period: period,
|
55
|
+
counter: counter,
|
56
|
+
}
|
57
|
+
.merge(@otp.provisioning_params)
|
58
|
+
.reject { |_, v| v.nil? }
|
59
|
+
.map { |k, v| "#{k}=#{ERB::Util.url_encode(v)}" }
|
60
|
+
.join('&')
|
61
|
+
end
|
62
|
+
|
63
|
+
def period
|
64
|
+
return if @otp.is_a?(HOTP)
|
65
|
+
return if @otp.interval == DEFAULT_INTERVAL
|
66
|
+
|
67
|
+
@otp.interval
|
68
|
+
end
|
69
|
+
|
70
|
+
def type
|
71
|
+
case @otp
|
72
|
+
when TOTP then 'totp'
|
73
|
+
when HOTP then 'hotp'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/rotp/otp.rb
CHANGED
@@ -1,24 +1,35 @@
|
|
1
1
|
module ROTP
|
2
2
|
class OTP
|
3
|
-
attr_reader :secret, :digits, :digest
|
3
|
+
attr_reader :secret, :digits, :digest, :name, :issuer, :provisioning_params
|
4
4
|
DEFAULT_DIGITS = 6
|
5
5
|
|
6
6
|
# @param [String] secret in the form of base32
|
7
7
|
# @option options digits [Integer] (6)
|
8
|
-
# Number of integers in the OTP
|
8
|
+
# Number of integers in the OTP.
|
9
9
|
# Google Authenticate only supports 6 currently
|
10
10
|
# @option options digest [String] (sha1)
|
11
|
-
# Digest used in the HMAC
|
11
|
+
# Digest used in the HMAC.
|
12
12
|
# Google Authenticate only supports 'sha1' currently
|
13
|
+
# @option options name [String]
|
14
|
+
# The name of the account for the OTP.
|
15
|
+
# Used in the provisioning URL
|
16
|
+
# @option options issuer [String]
|
17
|
+
# The issuer of the OTP.
|
18
|
+
# Used in the provisioning URL
|
19
|
+
# @option options provisioning_params [Hash] ({})
|
20
|
+
# Additional non-standard params you may want appended to the
|
21
|
+
# provisioning URI. Ex. `image: 'https://example.com/icon.png'`
|
13
22
|
# @returns [OTP] OTP instantiation
|
14
23
|
def initialize(s, options = {})
|
15
24
|
@digits = options[:digits] || DEFAULT_DIGITS
|
16
|
-
@digest = options[:digest] ||
|
25
|
+
@digest = options[:digest] || 'sha1'
|
26
|
+
@name = options[:name]
|
27
|
+
@issuer = options[:issuer]
|
28
|
+
@provisioning_params = options[:provisioning_params] || {}
|
17
29
|
@secret = s
|
18
30
|
end
|
19
31
|
|
20
32
|
# @param [Integer] input the number used seed the HMAC
|
21
|
-
# @option padded [Boolean] (false) Output the otp as a 0 padded string
|
22
33
|
# Usually either the counter, or the computed integer
|
23
34
|
# based on the Unix timestamp
|
24
35
|
def generate_otp(input)
|
@@ -30,17 +41,19 @@ module ROTP
|
|
30
41
|
|
31
42
|
offset = hmac[-1].ord & 0xf
|
32
43
|
code = (hmac[offset].ord & 0x7f) << 24 |
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
(code % 10 ** digits).to_s
|
44
|
+
(hmac[offset + 1].ord & 0xff) << 16 |
|
45
|
+
(hmac[offset + 2].ord & 0xff) << 8 |
|
46
|
+
(hmac[offset + 3].ord & 0xff)
|
47
|
+
code_str = (10 ** digits + (code % 10 ** digits)).to_s
|
48
|
+
code_str[-digits..-1]
|
37
49
|
end
|
38
50
|
|
39
51
|
private
|
40
52
|
|
41
53
|
def verify(input, generated)
|
42
|
-
raise ArgumentError,
|
54
|
+
raise ArgumentError, '`otp` should be a String' unless
|
43
55
|
input.is_a?(String)
|
56
|
+
|
44
57
|
time_constant_compare(input, generated)
|
45
58
|
end
|
46
59
|
|
@@ -54,37 +67,25 @@ module ROTP
|
|
54
67
|
#
|
55
68
|
def int_to_bytestring(int, padding = 8)
|
56
69
|
unless int >= 0
|
57
|
-
raise ArgumentError,
|
70
|
+
raise ArgumentError, '#int_to_bytestring requires a positive number'
|
58
71
|
end
|
59
72
|
|
60
73
|
result = []
|
61
74
|
until int == 0
|
62
75
|
result << (int & 0xFF).chr
|
63
|
-
int >>=
|
76
|
+
int >>= 8
|
64
77
|
end
|
65
78
|
result.reverse.join.rjust(padding, 0.chr)
|
66
79
|
end
|
67
80
|
|
68
|
-
# A very simple param encoder
|
69
|
-
def encode_params(uri, params)
|
70
|
-
params_str = String.new("?")
|
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
81
|
# constant-time compare the strings
|
81
82
|
def time_constant_compare(a, b)
|
82
83
|
return false if a.empty? || b.empty? || a.bytesize != b.bytesize
|
84
|
+
|
83
85
|
l = a.unpack "C#{a.bytesize}"
|
84
86
|
res = 0
|
85
87
|
b.each_byte { |byte| res |= byte ^ l.shift }
|
86
88
|
res == 0
|
87
89
|
end
|
88
|
-
|
89
90
|
end
|
90
91
|
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
|
@@ -21,7 +20,7 @@ module ROTP
|
|
21
20
|
|
22
21
|
# Generate the current time OTP
|
23
22
|
# @return [Integer] the OTP as an integer
|
24
|
-
def now
|
23
|
+
def now
|
25
24
|
generate_otp(timecode(Time.now))
|
26
25
|
end
|
27
26
|
|
@@ -40,39 +39,22 @@ module ROTP
|
|
40
39
|
def verify(otp, drift_ahead: 0, drift_behind: 0, after: nil, at: Time.now)
|
41
40
|
timecodes = get_timecodes(at, drift_behind, drift_ahead)
|
42
41
|
|
43
|
-
if after
|
44
|
-
timecodes = timecodes.select { |t| t > timecode(after) }
|
45
|
-
end
|
42
|
+
timecodes = timecodes.select { |t| t > timecode(after) } if after
|
46
43
|
|
47
44
|
result = nil
|
48
|
-
timecodes.each
|
49
|
-
if
|
50
|
-
|
51
|
-
|
52
|
-
}
|
53
|
-
return result
|
45
|
+
timecodes.each do |t|
|
46
|
+
result = t * interval if super(otp, generate_otp(t))
|
47
|
+
end
|
48
|
+
result
|
54
49
|
end
|
55
50
|
|
56
|
-
|
57
51
|
# Returns the provisioning URI for the OTP
|
58
52
|
# This can then be encoded in a QR Code and used
|
59
53
|
# to provision the Google Authenticator app
|
60
54
|
# @param [String] name of the account
|
61
55
|
# @return [String] provisioning URI
|
62
|
-
def provisioning_uri(name)
|
63
|
-
|
64
|
-
# https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
65
|
-
# For compatibility the issuer appears both before that account name and also in the
|
66
|
-
# query string.
|
67
|
-
issuer_string = issuer.nil? ? "" : "#{URI.encode(issuer)}:"
|
68
|
-
params = {
|
69
|
-
secret: secret,
|
70
|
-
period: interval == 30 ? nil : interval,
|
71
|
-
issuer: issuer,
|
72
|
-
digits: digits == DEFAULT_DIGITS ? nil : digits,
|
73
|
-
algorithm: digest.upcase == 'SHA1' ? nil : digest.upcase,
|
74
|
-
}
|
75
|
-
encode_params("otpauth://totp/#{issuer_string}#{URI.encode(name)}", params)
|
56
|
+
def provisioning_uri(name = nil)
|
57
|
+
OTP::URI.new(self, account_name: name || @name).to_s
|
76
58
|
end
|
77
59
|
|
78
60
|
private
|
@@ -82,20 +64,18 @@ module ROTP
|
|
82
64
|
now = timeint(at)
|
83
65
|
timecode_start = timecode(now - drift_behind)
|
84
66
|
timecode_end = timecode(now + drift_ahead)
|
85
|
-
|
67
|
+
(timecode_start..timecode_end).step(1).to_a
|
86
68
|
end
|
87
69
|
|
88
70
|
# Ensure UTC int
|
89
71
|
def timeint(time)
|
90
|
-
unless time.class == Time
|
91
|
-
|
92
|
-
|
93
|
-
return time.utc.to_i
|
72
|
+
return time.to_i unless time.class == Time
|
73
|
+
|
74
|
+
time.utc.to_i
|
94
75
|
end
|
95
76
|
|
96
77
|
def timecode(time)
|
97
|
-
|
78
|
+
timeint(time) / interval
|
98
79
|
end
|
99
|
-
|
100
80
|
end
|
101
81
|
end
|
data/lib/rotp/version.rb
CHANGED
data/lib/rotp.rb
CHANGED
@@ -0,0 +1,12 @@
|
|
1
|
+
{
|
2
|
+
"packages": {
|
3
|
+
".": {
|
4
|
+
"changelog-path": "CHANGELOG.md",
|
5
|
+
"bump-minor-pre-major": false,
|
6
|
+
"bump-patch-for-minor-pre-major": false,
|
7
|
+
"draft": false,
|
8
|
+
"prerelease": false
|
9
|
+
}
|
10
|
+
},
|
11
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
12
|
+
}
|
data/rotp.gemspec
CHANGED
@@ -1,26 +1,24 @@
|
|
1
|
-
|
2
|
-
require "./lib/rotp/version"
|
1
|
+
require './lib/rotp/version'
|
3
2
|
|
4
3
|
Gem::Specification.new do |s|
|
5
|
-
s.name =
|
4
|
+
s.name = 'rotp'
|
6
5
|
s.version = ROTP::VERSION
|
7
6
|
s.platform = Gem::Platform::RUBY
|
8
|
-
s.
|
9
|
-
s.
|
10
|
-
s.
|
11
|
-
s.
|
12
|
-
s.
|
13
|
-
s.
|
14
|
-
|
15
|
-
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'
|
16
14
|
|
17
15
|
s.files = `git ls-files`.split("\n")
|
18
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
-
s.require_paths = [
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
18
|
+
s.require_paths = ['lib']
|
21
19
|
|
22
|
-
s.add_development_dependency
|
20
|
+
s.add_development_dependency "rake", "~> 13.0"
|
23
21
|
s.add_development_dependency 'rspec', '~> 3.5'
|
24
|
-
s.add_development_dependency 'timecop', '~> 0.8'
|
25
22
|
s.add_development_dependency 'simplecov', '~> 0.12'
|
23
|
+
s.add_development_dependency 'timecop', '~> 0.8'
|
26
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
|
@@ -72,7 +72,7 @@ RSpec.describe ROTP::Arguments do
|
|
72
72
|
end
|
73
73
|
|
74
74
|
context 'generating a counter based secret' do
|
75
|
-
let(:argv) { %w
|
75
|
+
let(:argv) { %w[--time --secret s3same] }
|
76
76
|
|
77
77
|
describe '#options' do
|
78
78
|
it 'is in hmac mode' do
|
@@ -86,7 +86,7 @@ RSpec.describe ROTP::Arguments do
|
|
86
86
|
end
|
87
87
|
|
88
88
|
context 'generating a time based secret' do
|
89
|
-
let(:argv) { %w
|
89
|
+
let(:argv) { %w[--secret s3same] }
|
90
90
|
|
91
91
|
describe '#options' do
|
92
92
|
it 'is in time mode' do
|
@@ -98,5 +98,4 @@ RSpec.describe ROTP::Arguments do
|
|
98
98
|
end
|
99
99
|
end
|
100
100
|
end
|
101
|
-
|
102
101
|
end
|
@@ -1,25 +1,33 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ROTP::Base32 do
|
4
|
-
|
5
|
-
describe '.random_base32' do
|
4
|
+
describe '.random' do
|
6
5
|
context 'without arguments' do
|
7
|
-
let(:base32) { ROTP::Base32.
|
6
|
+
let(:base32) { ROTP::Base32.random }
|
8
7
|
|
9
|
-
it 'is 32
|
8
|
+
it 'is 20 bytes (160 bits) long (resulting in a 32 character base32 code)' do
|
9
|
+
expect(ROTP::Base32.decode(base32).length).to eq 20
|
10
10
|
expect(base32.length).to eq 32
|
11
11
|
end
|
12
12
|
|
13
|
-
it 'is
|
14
|
-
expect(base32).to match
|
13
|
+
it 'is base32 charset' do
|
14
|
+
expect(base32).to match(/\A[A-Z2-7]+\z/)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
18
|
context 'with arguments' do
|
19
|
-
let(:base32) { ROTP::Base32.
|
19
|
+
let(:base32) { ROTP::Base32.random 48 }
|
20
20
|
|
21
|
-
it '
|
22
|
-
expect(base32.length).to eq
|
21
|
+
it 'returns the appropriate byte length code' do
|
22
|
+
expect(ROTP::Base32.decode(base32).length).to eq 48
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'alias to older random_base32' do
|
27
|
+
let(:base32) { ROTP::Base32.random_base32(36) }
|
28
|
+
it 'is base32 charset' do
|
29
|
+
expect(base32.length).to eq 36
|
30
|
+
expect(ROTP::Base32.decode(base32).length).to eq 22
|
23
31
|
end
|
24
32
|
end
|
25
33
|
end
|
@@ -34,22 +42,40 @@ RSpec.describe ROTP::Base32 do
|
|
34
42
|
|
35
43
|
context 'valid input data' do
|
36
44
|
it 'correctly decodes a string' do
|
37
|
-
expect(ROTP::Base32.decode('
|
38
|
-
expect(ROTP::Base32.decode('
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
expect(ROTP::Base32.decode('
|
45
|
-
|
45
|
+
expect(ROTP::Base32.decode('2EB7C66WC5TSO').unpack('H*').first).to eq 'd103f17bd6176727'
|
46
|
+
expect(ROTP::Base32.decode('Y6Y5ZCAC7NABCHSJ').unpack('H*').first).to eq 'c7b1dc8802fb40111e49'
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'correctly decode strings with trailing bits (not a multiple of 8)' do
|
50
|
+
# Dropbox style 26 characters (26*5==130 bits or 16.25 bytes, but chopped to 128)
|
51
|
+
# Matches the behavior of Google Authenticator, drops extra 2 empty bits
|
52
|
+
expect(ROTP::Base32.decode('YVT6Z2XF4BQJNBMTD7M6QBQCEM').unpack('H*').first).to eq 'c567eceae5e0609685931fd9e8060223'
|
53
|
+
|
54
|
+
# For completeness, test all the possibilities allowed by Google Authenticator
|
55
|
+
# Drop the incomplete empty extra 4 bits (28*5==140bits or 17.5 bytes, chopped to 136 bits)
|
56
|
+
expect(ROTP::Base32.decode('5GGZQB3WN6LD7V3L5HPDYTQUANEQ').unpack('H*').first).to eq 'e98d9807766f963fd76be9de3c4e140349'
|
57
|
+
|
46
58
|
end
|
47
59
|
|
48
60
|
context 'with padding' do
|
49
61
|
it 'correctly decodes a string' do
|
50
|
-
expect(ROTP::Base32.decode('
|
62
|
+
expect(ROTP::Base32.decode('234A===').unpack('H*').first).to eq 'd6f8'
|
51
63
|
end
|
52
64
|
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '.encode' do
|
70
|
+
context 'encode input data' do
|
71
|
+
it 'correctly encodes data' do
|
72
|
+
expect(ROTP::Base32.encode(hex_to_bin('3c204da94294ff82103ee34e96f74b48'))).to eq 'HQQE3KKCST7YEEB64NHJN52LJA'
|
73
|
+
end
|
53
74
|
end
|
54
75
|
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
def hex_to_bin(s)
|
80
|
+
s.scan(/../).map { |x| x.hex }.pack('c*')
|
55
81
|
end
|