rotp 4.0.0 → 4.0.2
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/CHANGELOG.md +9 -0
- data/Guardfile +1 -1
- data/README.md +1 -1
- data/Rakefile +2 -2
- data/bin/rotp +1 -1
- data/lib/rotp.rb +1 -2
- data/lib/rotp/arguments.rb +2 -5
- data/lib/rotp/base32.rb +11 -9
- data/lib/rotp/cli.rb +2 -3
- data/lib/rotp/hotp.rb +6 -8
- data/lib/rotp/otp.rb +13 -14
- data/lib/rotp/totp.rb +14 -22
- data/lib/rotp/version.rb +1 -1
- data/rotp.gemspec +14 -13
- data/spec/lib/rotp/arguments_spec.rb +5 -6
- data/spec/lib/rotp/base32_spec.rb +2 -3
- data/spec/lib/rotp/cli_spec.rb +6 -7
- data/spec/lib/rotp/hotp_spec.rb +9 -5
- data/spec/lib/rotp/totp_spec.rb +34 -43
- data/spec/spec_helper.rb +1 -2
- metadata +21 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72d869e33ff8ede2ef4233ed0398b730a1fc14e2b764295b090492bed133c4fc
|
4
|
+
data.tar.gz: 5ee5f47d3cee494762fcaaabb4dff1dfc247bab7faa0207fc6a3b7ff7b672f05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42c5bb89a97375204c3198dd9d07080f4728162307c9b99eb6f7c46152bfa8aa21d7e02699fc911c9991ef6ae17c9465f2c8e438bf223b2097cbc9b72766912b
|
7
|
+
data.tar.gz: 45dc82dd282328c5e3651a5b660ac327c1d10f07b7ca21025139fbe3edf418b661e1b67648081027e04ea9bce639195ec21d44ada7d6af9ac607984e8fcda47b
|
data/CHANGELOG.md
CHANGED
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -21,7 +21,7 @@ Many websites use this for [multi-factor authentication](https://www.youtube.com
|
|
21
21
|
- `verify` now takes options for `drift` and `after`
|
22
22
|
- `verify` returns a timestamp if true, nil if false
|
23
23
|
- Dropping support for Ruby < 2.0
|
24
|
-
- Docs for 3.x can be found [here](https://github.com/mdp/rotp/tree/v3.
|
24
|
+
- Docs for 3.x can be found [here](https://github.com/mdp/rotp/tree/v3.x)
|
25
25
|
|
26
26
|
## Installation
|
27
27
|
|
data/Rakefile
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'bundler'
|
2
2
|
Bundler::GemHelper.install_tasks
|
3
|
-
require
|
3
|
+
require 'rspec/core/rake_task'
|
4
4
|
|
5
5
|
RSpec::Core::RakeTask.new(:rspec) do |spec|
|
6
6
|
spec.pattern = 'spec/**/*_spec.rb'
|
7
7
|
end
|
8
8
|
|
9
|
-
task :
|
9
|
+
task default: :rspec
|
data/bin/rotp
CHANGED
data/lib/rotp.rb
CHANGED
data/lib/rotp/arguments.rb
CHANGED
@@ -3,7 +3,6 @@ require 'ostruct'
|
|
3
3
|
|
4
4
|
module ROTP
|
5
5
|
class Arguments
|
6
|
-
|
7
6
|
def initialize(filename, arguments)
|
8
7
|
@filename = filename
|
9
8
|
@arguments = Array(arguments)
|
@@ -32,11 +31,11 @@ module ROTP
|
|
32
31
|
|
33
32
|
def parse
|
34
33
|
return options!.mode = :help if arguments.empty?
|
35
|
-
parser.parse arguments
|
36
34
|
|
35
|
+
parser.parse arguments
|
37
36
|
rescue OptionParser::InvalidOption => exception
|
38
37
|
options!.mode = :help
|
39
|
-
options!.warnings = red(exception.message +
|
38
|
+
options!.warnings = red(exception.message + '. Try --help for help.')
|
40
39
|
end
|
41
40
|
|
42
41
|
def parser
|
@@ -83,7 +82,5 @@ module ROTP
|
|
83
82
|
def red(string)
|
84
83
|
"\033[31m#{string}\033[0m"
|
85
84
|
end
|
86
|
-
|
87
85
|
end
|
88
86
|
end
|
89
|
-
|
data/lib/rotp/base32.rb
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
module ROTP
|
2
2
|
class Base32
|
3
3
|
class Base32Error < RuntimeError; end
|
4
|
-
CHARS =
|
4
|
+
CHARS = 'abcdefghijklmnopqrstuvwxyz234567'.each_char.to_a
|
5
5
|
|
6
6
|
class << self
|
7
7
|
def decode(str)
|
8
|
-
str = str.tr('=','')
|
8
|
+
str = str.tr('=', '')
|
9
9
|
output = []
|
10
10
|
str.scan(/.{1,8}/).each do |block|
|
11
|
-
char_array = decode_block(block).map
|
11
|
+
char_array = decode_block(block).map(&:chr)
|
12
12
|
output << char_array
|
13
13
|
end
|
14
14
|
output.join
|
15
15
|
end
|
16
16
|
|
17
|
-
def random_base32(length=32)
|
18
|
-
b32 =
|
17
|
+
def random_base32(length = 32)
|
18
|
+
b32 = ''
|
19
19
|
SecureRandom.random_bytes(length).each_byte do |b|
|
20
20
|
b32 << CHARS[b % 32]
|
21
21
|
end
|
@@ -26,25 +26,27 @@ module ROTP
|
|
26
26
|
|
27
27
|
def decode_block(block)
|
28
28
|
length = block.scan(/[^=]/).length
|
29
|
-
quints = block.each_char.map {|c| decode_quint(c)}
|
29
|
+
quints = block.each_char.map { |c| decode_quint(c) }
|
30
30
|
bytes = []
|
31
31
|
bytes[0] = (quints[0] << 3) + (quints[1] ? quints[1] >> 2 : 0)
|
32
32
|
return bytes if length < 3
|
33
|
+
|
33
34
|
bytes[1] = ((quints[1] & 3) << 6) + (quints[2] << 1) + (quints[3] ? quints[3] >> 4 : 0)
|
34
35
|
return bytes if length < 4
|
36
|
+
|
35
37
|
bytes[2] = ((quints[3] & 15) << 4) + (quints[4] ? quints[4] >> 1 : 0)
|
36
38
|
return bytes if length < 6
|
39
|
+
|
37
40
|
bytes[3] = ((quints[4] & 1) << 7) + (quints[5] << 2) + (quints[6] ? quints[6] >> 3 : 0)
|
38
41
|
return bytes if length < 7
|
42
|
+
|
39
43
|
bytes[4] = ((quints[6] & 7) << 5) + (quints[7] || 0)
|
40
44
|
bytes
|
41
45
|
end
|
42
46
|
|
43
47
|
def decode_quint(q)
|
44
|
-
CHARS.index(q.downcase)
|
48
|
+
CHARS.index(q.downcase) || raise(Base32Error, "Invalid Base32 Character - '#{q}'")
|
45
49
|
end
|
46
|
-
|
47
50
|
end
|
48
|
-
|
49
51
|
end
|
50
52
|
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.downcase)
|
22
|
+
elsif options.secret.to_s.chars.any? { |c| ROTP::Base32::CHARS.index(c.downcase).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
|
@@ -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,13 @@ 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)
|
27
|
+
def provisioning_uri(name, initial_count = 0)
|
28
28
|
params = {
|
29
29
|
secret: secret,
|
30
30
|
counter: initial_count,
|
31
31
|
digits: digits == DEFAULT_DIGITS ? nil : digits
|
32
32
|
}
|
33
|
-
encode_params("otpauth://hotp/#{URI.
|
33
|
+
encode_params("otpauth://hotp/#{Addressable::URI.escape(name)}", params)
|
34
34
|
end
|
35
|
-
|
36
35
|
end
|
37
|
-
|
38
36
|
end
|
data/lib/rotp/otp.rb
CHANGED
@@ -13,7 +13,7 @@ module ROTP
|
|
13
13
|
# @returns [OTP] OTP instantiation
|
14
14
|
def initialize(s, options = {})
|
15
15
|
@digits = options[:digits] || DEFAULT_DIGITS
|
16
|
-
@digest = options[:digest] ||
|
16
|
+
@digest = options[:digest] || 'sha1'
|
17
17
|
@secret = s
|
18
18
|
end
|
19
19
|
|
@@ -30,17 +30,18 @@ module ROTP
|
|
30
30
|
|
31
31
|
offset = hmac[-1].ord & 0xf
|
32
32
|
code = (hmac[offset].ord & 0x7f) << 24 |
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
(code % 10
|
33
|
+
(hmac[offset + 1].ord & 0xff) << 16 |
|
34
|
+
(hmac[offset + 2].ord & 0xff) << 8 |
|
35
|
+
(hmac[offset + 3].ord & 0xff)
|
36
|
+
(code % 10**digits).to_s.rjust(digits, '0')
|
37
37
|
end
|
38
38
|
|
39
39
|
private
|
40
40
|
|
41
41
|
def verify(input, generated)
|
42
|
-
raise ArgumentError,
|
42
|
+
raise ArgumentError, '`otp` should be a String' unless
|
43
43
|
input.is_a?(String)
|
44
|
+
|
44
45
|
time_constant_compare(input, generated)
|
45
46
|
end
|
46
47
|
|
@@ -54,24 +55,22 @@ module ROTP
|
|
54
55
|
#
|
55
56
|
def int_to_bytestring(int, padding = 8)
|
56
57
|
unless int >= 0
|
57
|
-
raise ArgumentError,
|
58
|
+
raise ArgumentError, '#int_to_bytestring requires a positive number'
|
58
59
|
end
|
59
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
69
|
# A very simple param encoder
|
69
70
|
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
|
71
|
+
params_str = String.new('?')
|
72
|
+
params.each do |k, v|
|
73
|
+
params_str << "#{k}=#{CGI.escape(v.to_s)}&" if v
|
75
74
|
end
|
76
75
|
params_str.chop!
|
77
76
|
uri + params_str
|
@@ -80,11 +79,11 @@ module ROTP
|
|
80
79
|
# constant-time compare the strings
|
81
80
|
def time_constant_compare(a, b)
|
82
81
|
return false if a.empty? || b.empty? || a.bytesize != b.bytesize
|
82
|
+
|
83
83
|
l = a.unpack "C#{a.bytesize}"
|
84
84
|
res = 0
|
85
85
|
b.each_byte { |byte| res |= byte ^ l.shift }
|
86
86
|
res == 0
|
87
87
|
end
|
88
|
-
|
89
88
|
end
|
90
89
|
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,20 +39,15 @@ 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
|
@@ -64,15 +58,15 @@ module ROTP
|
|
64
58
|
# https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
65
59
|
# For compatibility the issuer appears both before that account name and also in the
|
66
60
|
# query string.
|
67
|
-
issuer_string = issuer.nil? ?
|
61
|
+
issuer_string = issuer.nil? ? '' : "#{Addressable::URI.escape(issuer)}:"
|
68
62
|
params = {
|
69
63
|
secret: secret,
|
70
64
|
period: interval == 30 ? nil : interval,
|
71
65
|
issuer: issuer,
|
72
66
|
digits: digits == DEFAULT_DIGITS ? nil : digits,
|
73
|
-
algorithm: digest.
|
67
|
+
algorithm: digest.casecmp('SHA1').zero? ? nil : digest.upcase
|
74
68
|
}
|
75
|
-
encode_params("otpauth://totp/#{issuer_string}#{URI.
|
69
|
+
encode_params("otpauth://totp/#{issuer_string}#{Addressable::URI.escape(name)}", params)
|
76
70
|
end
|
77
71
|
|
78
72
|
private
|
@@ -82,20 +76,18 @@ module ROTP
|
|
82
76
|
now = timeint(at)
|
83
77
|
timecode_start = timecode(now - drift_behind)
|
84
78
|
timecode_end = timecode(now + drift_ahead)
|
85
|
-
|
79
|
+
(timecode_start..timecode_end).step(1).to_a
|
86
80
|
end
|
87
81
|
|
88
82
|
# Ensure UTC int
|
89
83
|
def timeint(time)
|
90
|
-
unless time.class == Time
|
91
|
-
|
92
|
-
|
93
|
-
return time.utc.to_i
|
84
|
+
return time.to_i unless time.class == Time
|
85
|
+
|
86
|
+
time.utc.to_i
|
94
87
|
end
|
95
88
|
|
96
89
|
def timecode(time)
|
97
|
-
|
90
|
+
timeint(time) / interval
|
98
91
|
end
|
99
|
-
|
100
92
|
end
|
101
93
|
end
|
data/lib/rotp/version.rb
CHANGED
data/rotp.gemspec
CHANGED
@@ -1,26 +1,27 @@
|
|
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.license =
|
9
|
-
s.authors = [
|
10
|
-
s.email = [
|
11
|
-
s.homepage =
|
12
|
-
s.summary =
|
13
|
-
s.description =
|
7
|
+
s.license = 'MIT'
|
8
|
+
s.authors = ['Mark Percival']
|
9
|
+
s.email = ['mark@markpercival.us']
|
10
|
+
s.homepage = 'http://github.com/mdp/rotp'
|
11
|
+
s.summary = 'A Ruby library for generating and verifying one time passwords'
|
12
|
+
s.description = 'Works for both HOTP and TOTP, and includes QR Code provisioning'
|
14
13
|
|
15
|
-
s.rubyforge_project =
|
14
|
+
s.rubyforge_project = 'rotp'
|
16
15
|
|
17
16
|
s.files = `git ls-files`.split("\n")
|
18
17
|
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 = [
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
19
|
+
s.require_paths = ['lib']
|
20
|
+
|
21
|
+
s.add_runtime_dependency 'addressable', '~> 2.5'
|
21
22
|
|
22
23
|
s.add_development_dependency 'rake', '~> 10.5'
|
23
24
|
s.add_development_dependency 'rspec', '~> 3.5'
|
24
|
-
s.add_development_dependency 'timecop', '~> 0.8'
|
25
25
|
s.add_development_dependency 'simplecov', '~> 0.12'
|
26
|
+
s.add_development_dependency 'timecop', '~> 0.8'
|
26
27
|
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,7 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ROTP::Base32 do
|
4
|
-
|
5
4
|
describe '.random_base32' do
|
6
5
|
context 'without arguments' do
|
7
6
|
let(:base32) { ROTP::Base32.random_base32 }
|
@@ -10,8 +9,8 @@ RSpec.describe ROTP::Base32 do
|
|
10
9
|
expect(base32.length).to eq 32
|
11
10
|
end
|
12
11
|
|
13
|
-
it 'is
|
14
|
-
expect(base32).to match
|
12
|
+
it 'is base32 charset' do
|
13
|
+
expect(base32).to match(/\A[a-z2-7]+\z/)
|
15
14
|
end
|
16
15
|
end
|
17
16
|
|
data/spec/lib/rotp/cli_spec.rb
CHANGED
@@ -4,14 +4,14 @@ require 'rotp/cli'
|
|
4
4
|
RSpec.describe ROTP::CLI do
|
5
5
|
let(:cli) { described_class.new('executable', argv) }
|
6
6
|
let(:output) { cli.output }
|
7
|
-
let(:now) { Time.utc 2012,1,1 }
|
7
|
+
let(:now) { Time.utc 2012, 1, 1 }
|
8
8
|
|
9
9
|
before do
|
10
10
|
Timecop.freeze now
|
11
11
|
end
|
12
12
|
|
13
13
|
context 'generating a TOTP' do
|
14
|
-
let(:argv) { %w
|
14
|
+
let(:argv) { %w[--secret JBSWY3DPEHPK3PXP] }
|
15
15
|
|
16
16
|
it 'prints the corresponding token' do
|
17
17
|
expect(output).to eq '068212'
|
@@ -19,7 +19,7 @@ RSpec.describe ROTP::CLI do
|
|
19
19
|
end
|
20
20
|
|
21
21
|
context 'generating a TOTP with no secret' do
|
22
|
-
let(:argv) { %
|
22
|
+
let(:argv) { %w[--time --secret] }
|
23
23
|
|
24
24
|
it 'prints the corresponding token' do
|
25
25
|
expect(output).to match 'You must also specify a --secret'
|
@@ -27,7 +27,7 @@ RSpec.describe ROTP::CLI do
|
|
27
27
|
end
|
28
28
|
|
29
29
|
context 'generating a TOTP with bad base32 secret' do
|
30
|
-
let(:argv) { %W
|
30
|
+
let(:argv) { %W[--time --secret #{'1' * 32}] }
|
31
31
|
|
32
32
|
it 'prints the corresponding token' do
|
33
33
|
expect(output).to match 'Secret must be in RFC4648 Base32 format'
|
@@ -35,7 +35,7 @@ RSpec.describe ROTP::CLI do
|
|
35
35
|
end
|
36
36
|
|
37
37
|
context 'trying to generate an unsupport type' do
|
38
|
-
let(:argv) { %W
|
38
|
+
let(:argv) { %W[--notreal --secret #{'a' * 32}] }
|
39
39
|
|
40
40
|
it 'prints the corresponding token' do
|
41
41
|
expect(output).to match 'invalid option: --notreal'
|
@@ -43,11 +43,10 @@ RSpec.describe ROTP::CLI do
|
|
43
43
|
end
|
44
44
|
|
45
45
|
context 'generating a HOTP' do
|
46
|
-
let(:argv) { %W
|
46
|
+
let(:argv) { %W[--hmac --secret #{'a' * 32} --counter 1234] }
|
47
47
|
|
48
48
|
it 'prints the corresponding token' do
|
49
49
|
expect(output).to eq '161024'
|
50
50
|
end
|
51
51
|
end
|
52
|
-
|
53
52
|
end
|
data/spec/lib/rotp/hotp_spec.rb
CHANGED
@@ -14,6 +14,12 @@ RSpec.describe ROTP::HOTP do
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
context 'invalid counter' do
|
18
|
+
it 'raises an error' do
|
19
|
+
expect { hotp.at(-123_456) }.to raise_error(ArgumentError)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
17
23
|
context 'RFC compatibility' do
|
18
24
|
let(:hotp) { ROTP::HOTP.new('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') }
|
19
25
|
|
@@ -31,7 +37,6 @@ RSpec.describe ROTP::HOTP do
|
|
31
37
|
expect(hotp.at(8)).to eq '399871'
|
32
38
|
expect(hotp.at(9)).to eq '520489'
|
33
39
|
end
|
34
|
-
|
35
40
|
end
|
36
41
|
end
|
37
42
|
|
@@ -39,7 +44,7 @@ RSpec.describe ROTP::HOTP do
|
|
39
44
|
let(:verification) { hotp.verify token, counter }
|
40
45
|
|
41
46
|
context 'numeric token' do
|
42
|
-
let(:token) {
|
47
|
+
let(:token) { 161_024 }
|
43
48
|
|
44
49
|
it 'raises an error' do
|
45
50
|
expect { verification }.to raise_error(ArgumentError)
|
@@ -62,7 +67,7 @@ RSpec.describe ROTP::HOTP do
|
|
62
67
|
end
|
63
68
|
end
|
64
69
|
describe 'with retries' do
|
65
|
-
let(:verification) { hotp.verify token, counter, retries:retries }
|
70
|
+
let(:verification) { hotp.verify token, counter, retries: retries }
|
66
71
|
|
67
72
|
context 'counter outside than retries' do
|
68
73
|
let(:counter) { 1223 }
|
@@ -104,7 +109,7 @@ RSpec.describe ROTP::HOTP do
|
|
104
109
|
|
105
110
|
describe '#provisioning_uri' do
|
106
111
|
let(:uri) { hotp.provisioning_uri('mark@percival') }
|
107
|
-
let(:params) { CGI
|
112
|
+
let(:params) { CGI.parse URI.parse(uri).query }
|
108
113
|
|
109
114
|
it 'has the correct format' do
|
110
115
|
expect(uri).to match %r{\Aotpauth:\/\/hotp.+}
|
@@ -128,5 +133,4 @@ RSpec.describe ROTP::HOTP do
|
|
128
133
|
end
|
129
134
|
end
|
130
135
|
end
|
131
|
-
|
132
136
|
end
|
data/spec/lib/rotp/totp_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
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 =
|
3
|
+
TEST_TIME = Time.utc 2016, 9, 23, 9 # 2016-09-23 09:00:00 UTC
|
4
|
+
TEST_TOKEN = '082630'.freeze
|
5
5
|
|
6
6
|
RSpec.describe ROTP::TOTP do
|
7
7
|
let(:now) { TEST_TIME }
|
@@ -19,11 +19,10 @@ RSpec.describe ROTP::TOTP do
|
|
19
19
|
let(:totp) { ROTP::TOTP.new('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ') }
|
20
20
|
|
21
21
|
it 'matches the RFC documentation examples' do
|
22
|
-
expect(totp.at
|
23
|
-
expect(totp.at
|
24
|
-
expect(totp.at
|
22
|
+
expect(totp.at(1_111_111_111)).to eq '050471'
|
23
|
+
expect(totp.at(1_234_567_890)).to eq '005924'
|
24
|
+
expect(totp.at(2_000_000_000)).to eq '279037'
|
25
25
|
end
|
26
|
-
|
27
26
|
end
|
28
27
|
end
|
29
28
|
|
@@ -31,7 +30,7 @@ RSpec.describe ROTP::TOTP do
|
|
31
30
|
let(:verification) { totp.verify token, at: now }
|
32
31
|
|
33
32
|
context 'numeric token' do
|
34
|
-
let(:token) {
|
33
|
+
let(:token) { 82_630 }
|
35
34
|
|
36
35
|
it 'raises an error with an integer' do
|
37
36
|
expect { verification }.to raise_error(ArgumentError)
|
@@ -53,7 +52,7 @@ RSpec.describe ROTP::TOTP do
|
|
53
52
|
end
|
54
53
|
|
55
54
|
context 'RFC compatibility' do
|
56
|
-
let(:totp)
|
55
|
+
let(:totp) { ROTP::TOTP.new 'wrn3pqx5uqxqvnqr' }
|
57
56
|
|
58
57
|
before do
|
59
58
|
Timecop.freeze now
|
@@ -61,7 +60,7 @@ RSpec.describe ROTP::TOTP do
|
|
61
60
|
|
62
61
|
context 'correct time based OTP' do
|
63
62
|
let(:token) { '102705' }
|
64
|
-
let(:now) { Time.at
|
63
|
+
let(:now) { Time.at 1_297_553_958 }
|
65
64
|
|
66
65
|
it 'verifies' do
|
67
66
|
expect(totp.verify('102705')).to be_truthy
|
@@ -75,17 +74,17 @@ RSpec.describe ROTP::TOTP do
|
|
75
74
|
end
|
76
75
|
end
|
77
76
|
context 'invalidating reused tokens' do
|
78
|
-
let(:verification)
|
77
|
+
let(:verification) do
|
79
78
|
totp.verify token,
|
80
|
-
|
81
|
-
|
82
|
-
|
79
|
+
after: after,
|
80
|
+
at: now
|
81
|
+
end
|
83
82
|
let(:after) { nil }
|
84
83
|
|
85
84
|
context 'passing in the `after` timestamp' do
|
86
|
-
let(:after)
|
85
|
+
let(:after) do
|
87
86
|
totp.verify TEST_TOKEN, after: nil, at: now
|
88
|
-
|
87
|
+
end
|
89
88
|
|
90
89
|
it 'returns a timecode' do
|
91
90
|
expect(after).to be_kind_of(Integer)
|
@@ -106,23 +105,23 @@ RSpec.describe ROTP::TOTP do
|
|
106
105
|
totp.send('get_timecodes', at, b, a)
|
107
106
|
end
|
108
107
|
|
109
|
-
describe
|
108
|
+
describe 'drifting timecodes' do
|
110
109
|
it 'should get timecodes behind' do
|
111
|
-
expect(get_timecodes(TEST_TIME+15, 15, 0)).to eq([
|
112
|
-
expect(get_timecodes(TEST_TIME, 15, 0)).to eq([
|
113
|
-
expect(get_timecodes(TEST_TIME, 40, 0)).to eq([
|
114
|
-
expect(get_timecodes(TEST_TIME, 90, 0)).to eq([
|
110
|
+
expect(get_timecodes(TEST_TIME + 15, 15, 0)).to eq([49_154_040])
|
111
|
+
expect(get_timecodes(TEST_TIME, 15, 0)).to eq([49_154_039, 49_154_040])
|
112
|
+
expect(get_timecodes(TEST_TIME, 40, 0)).to eq([49_154_038, 49_154_039, 49_154_040])
|
113
|
+
expect(get_timecodes(TEST_TIME, 90, 0)).to eq([49_154_037, 49_154_038, 49_154_039, 49_154_040])
|
115
114
|
end
|
116
115
|
it 'should get timecodes ahead' do
|
117
|
-
expect(get_timecodes(TEST_TIME, 0, 15)).to eq([
|
118
|
-
expect(get_timecodes(TEST_TIME+15, 0, 15)).to eq([
|
119
|
-
expect(get_timecodes(TEST_TIME, 0, 30)).to eq([
|
120
|
-
expect(get_timecodes(TEST_TIME, 0, 70)).to eq([
|
121
|
-
expect(get_timecodes(TEST_TIME, 0, 90)).to eq([
|
116
|
+
expect(get_timecodes(TEST_TIME, 0, 15)).to eq([49_154_040])
|
117
|
+
expect(get_timecodes(TEST_TIME + 15, 0, 15)).to eq([49_154_040, 49_154_041])
|
118
|
+
expect(get_timecodes(TEST_TIME, 0, 30)).to eq([49_154_040, 49_154_041])
|
119
|
+
expect(get_timecodes(TEST_TIME, 0, 70)).to eq([49_154_040, 49_154_041, 49_154_042])
|
120
|
+
expect(get_timecodes(TEST_TIME, 0, 90)).to eq([49_154_040, 49_154_041, 49_154_042, 49_154_043])
|
122
121
|
end
|
123
122
|
it 'should get timecodes behind and ahead' do
|
124
|
-
expect(get_timecodes(TEST_TIME, 30, 30)).to eq([
|
125
|
-
expect(get_timecodes(TEST_TIME, 60, 60)).to eq([
|
123
|
+
expect(get_timecodes(TEST_TIME, 30, 30)).to eq([49_154_039, 49_154_040, 49_154_041])
|
124
|
+
expect(get_timecodes(TEST_TIME, 60, 60)).to eq([49_154_038, 49_154_039, 49_154_040, 49_154_041, 49_154_042])
|
126
125
|
end
|
127
126
|
end
|
128
127
|
|
@@ -131,7 +130,6 @@ RSpec.describe ROTP::TOTP do
|
|
131
130
|
let(:drift_ahead) { 0 }
|
132
131
|
let(:drift_behind) { 0 }
|
133
132
|
|
134
|
-
|
135
133
|
context 'with an old OTP' do
|
136
134
|
let(:token) { totp.at TEST_TIME - 30 } # Previous token at 2016-09-23 08:59:30 UTC
|
137
135
|
let(:drift_behind) { 15 }
|
@@ -151,7 +149,6 @@ RSpec.describe ROTP::TOTP do
|
|
151
149
|
expect(verification).to be_nil
|
152
150
|
end
|
153
151
|
end
|
154
|
-
|
155
152
|
end
|
156
153
|
|
157
154
|
context 'with a future OTP' do
|
@@ -166,14 +163,13 @@ RSpec.describe ROTP::TOTP do
|
|
166
163
|
# Tested at 2016-09-23 09:00:20 UTC, and with drift ahead to 2016-09-23 09:00:35 UTC
|
167
164
|
# This would therefore include 2 intervals
|
168
165
|
context 'inside of drift range' do
|
169
|
-
let(:now)
|
166
|
+
let(:now) { TEST_TIME + 20 }
|
170
167
|
|
171
168
|
it 'is true' do
|
172
169
|
expect(verification).to be_truthy
|
173
170
|
end
|
174
171
|
end
|
175
172
|
end
|
176
|
-
|
177
173
|
end
|
178
174
|
|
179
175
|
describe '#verify with drift and prevent token reuse' do
|
@@ -183,7 +179,6 @@ RSpec.describe ROTP::TOTP do
|
|
183
179
|
let(:after) { nil }
|
184
180
|
|
185
181
|
context 'with the `after` timestamp set' do
|
186
|
-
|
187
182
|
context 'older token' do
|
188
183
|
let(:token) { totp.at TEST_TIME - 30 }
|
189
184
|
let(:drift_behind) { 15 }
|
@@ -194,14 +189,13 @@ RSpec.describe ROTP::TOTP do
|
|
194
189
|
end
|
195
190
|
|
196
191
|
context 'after it has been used' do
|
197
|
-
let(:after)
|
192
|
+
let(:after) do
|
198
193
|
totp.verify token, after: nil, at: now, drift_behind: drift_behind
|
199
|
-
|
194
|
+
end
|
200
195
|
it 'is false' do
|
201
196
|
expect(verification).to be_falsey
|
202
197
|
end
|
203
198
|
end
|
204
|
-
|
205
199
|
end
|
206
200
|
|
207
201
|
context 'newer token' do
|
@@ -215,21 +209,20 @@ RSpec.describe ROTP::TOTP do
|
|
215
209
|
end
|
216
210
|
|
217
211
|
context 'after it has been used' do
|
218
|
-
let(:after)
|
212
|
+
let(:after) do
|
219
213
|
totp.verify token, after: nil, at: now, drift_ahead: drift_ahead
|
220
|
-
|
214
|
+
end
|
221
215
|
it 'is false' do
|
222
216
|
expect(verification).to be_falsey
|
223
217
|
end
|
224
218
|
end
|
225
|
-
|
226
219
|
end
|
227
220
|
end
|
228
221
|
end
|
229
222
|
|
230
223
|
describe '#provisioning_uri' do
|
231
224
|
let(:uri) { totp.provisioning_uri('mark@percival') }
|
232
|
-
let(:params) { CGI
|
225
|
+
let(:params) { CGI.parse URI.parse(uri).query }
|
233
226
|
|
234
227
|
context 'without issuer' do
|
235
228
|
it 'has the correct format' do
|
@@ -302,7 +295,6 @@ RSpec.describe ROTP::TOTP do
|
|
302
295
|
expect(params['algorithm'].first).to eq 'SHA256'
|
303
296
|
end
|
304
297
|
end
|
305
|
-
|
306
298
|
end
|
307
299
|
|
308
300
|
describe '#now' do
|
@@ -312,7 +304,7 @@ RSpec.describe ROTP::TOTP do
|
|
312
304
|
|
313
305
|
context 'Google Authenticator' do
|
314
306
|
let(:totp) { ROTP::TOTP.new 'wrn3pqx5uqxqvnqr' }
|
315
|
-
let(:now) { Time.at
|
307
|
+
let(:now) { Time.at 1_297_553_958 }
|
316
308
|
|
317
309
|
it 'matches the known output' do
|
318
310
|
expect(totp.now).to eq '102705'
|
@@ -321,12 +313,11 @@ RSpec.describe ROTP::TOTP do
|
|
321
313
|
|
322
314
|
context 'Dropbox 26 char secret output' do
|
323
315
|
let(:totp) { ROTP::TOTP.new 'tjtpqea6a42l56g5eym73go2oa' }
|
324
|
-
let(:now) { Time.at
|
316
|
+
let(:now) { Time.at 1_378_762_454 }
|
325
317
|
|
326
318
|
it 'matches the known output' do
|
327
319
|
expect(totp.now).to eq '747864'
|
328
320
|
end
|
329
321
|
end
|
330
322
|
end
|
331
|
-
|
332
323
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rotp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.0.
|
4
|
+
version: 4.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Percival
|
@@ -10,6 +10,20 @@ bindir: bin
|
|
10
10
|
cert_chain: []
|
11
11
|
date: 2018-11-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: addressable
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.5'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: rake
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -39,33 +53,33 @@ dependencies:
|
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '3.5'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
56
|
+
name: simplecov
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
59
|
- - "~>"
|
46
60
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0.
|
61
|
+
version: '0.12'
|
48
62
|
type: :development
|
49
63
|
prerelease: false
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
51
65
|
requirements:
|
52
66
|
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
|
-
version: '0.
|
68
|
+
version: '0.12'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
70
|
+
name: timecop
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
73
|
- - "~>"
|
60
74
|
- !ruby/object:Gem::Version
|
61
|
-
version: '0.
|
75
|
+
version: '0.8'
|
62
76
|
type: :development
|
63
77
|
prerelease: false
|
64
78
|
version_requirements: !ruby/object:Gem::Requirement
|
65
79
|
requirements:
|
66
80
|
- - "~>"
|
67
81
|
- !ruby/object:Gem::Version
|
68
|
-
version: '0.
|
82
|
+
version: '0.8'
|
69
83
|
description: Works for both HOTP and TOTP, and includes QR Code provisioning
|
70
84
|
email:
|
71
85
|
- mark@markpercival.us
|