rotp 4.0.0 → 4.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|