rotp 1.4.6 → 1.5.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/README.markdown +10 -1
- data/lib/rotp/base32.rb +6 -1
- data/lib/rotp/hotp.rb +1 -1
- data/lib/rotp/otp.rb +12 -0
- data/lib/rotp/totp.rb +7 -3
- data/lib/rotp/version.rb +1 -1
- data/spec/base_spec.rb +3 -63
- data/spec/hotp_spec.rb +30 -0
- data/spec/totp_spec.rb +62 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a9c9c9d92e7df6e74c912f064f44aed0a2c6345
|
4
|
+
data.tar.gz: 664591b20546f52e2f216759bd0a0516ca1c6be8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72190c4a66d89ea2220550701fb2f7bb6b932ca93603cadad1b054b07e3e135cd752d61a87c78a87e59e71cea5cb89cae02784d508b8c51ba65797150e6d2421
|
7
|
+
data.tar.gz: 6d1a508e6379354f303338c7b42a393522602128c354fe2808ebe0b397a348588365921d48bf145ad6faed795b9428a48374aeddcfc3f9dc529855b84b2948ca
|
data/README.markdown
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# ROTP - The Ruby One Time Password Library
|
2
2
|
[](http://travis-ci.org/mdp/rotp)
|
3
3
|
|
4
|
-
A ruby library for generating one time passwords according to [ RFC 4226 ](http://tools.ietf.org/html/rfc4226) and
|
4
|
+
A ruby library for generating one time passwords (HOTP & TOTP) according to [ RFC 4226 ](http://tools.ietf.org/html/rfc4226) and [ RFC 6238 ](http://tools.ietf.org/html/rfc6238)
|
5
5
|
|
6
6
|
This is compatible with Google Authenticator apps available for Android and iPhone, and now in use on GMail
|
7
7
|
|
@@ -92,6 +92,15 @@ Now run the following and compare the output
|
|
92
92
|
|
93
93
|
### Changelog
|
94
94
|
|
95
|
+
#### 1.5.0
|
96
|
+
|
97
|
+
- Add support for "issuer" parameter on provisioning url
|
98
|
+
- Add support for "period/interval" parameter on provisioning url
|
99
|
+
|
100
|
+
#### 1.4.6
|
101
|
+
|
102
|
+
- Revert to previous Base32
|
103
|
+
|
95
104
|
#### 1.4.5
|
96
105
|
|
97
106
|
- Fix and test correct implementation of Base32
|
data/lib/rotp/base32.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module ROTP
|
2
2
|
class Base32
|
3
|
+
class Base32Error < RuntimeError; end
|
3
4
|
CHARS = "abcdefghijklmnopqrstuvwxyz234567".each_char.to_a
|
4
5
|
|
5
6
|
class << self
|
@@ -39,7 +40,11 @@ module ROTP
|
|
39
40
|
end
|
40
41
|
|
41
42
|
def decode_quint(q)
|
42
|
-
CHARS.index(q.downcase)
|
43
|
+
if d = CHARS.index(q.downcase)
|
44
|
+
d
|
45
|
+
else
|
46
|
+
raise Base32Error, "Invalid Base32 Character - '#{q}'"
|
47
|
+
end
|
43
48
|
end
|
44
49
|
|
45
50
|
end
|
data/lib/rotp/hotp.rb
CHANGED
@@ -22,7 +22,7 @@ module ROTP
|
|
22
22
|
# @param [Integer] initial_count starting counter value, defaults to 0
|
23
23
|
# @return [String] provisioning uri
|
24
24
|
def provisioning_uri(name, initial_count=0)
|
25
|
-
"otpauth://hotp/#{URI.encode(name)}
|
25
|
+
encode_params("otpauth://hotp/#{URI.encode(name)}", :secret=>secret, :counter=>initial_count)
|
26
26
|
end
|
27
27
|
|
28
28
|
end
|
data/lib/rotp/otp.rb
CHANGED
@@ -62,5 +62,17 @@ module ROTP
|
|
62
62
|
result.reverse.join.rjust(padding, 0.chr)
|
63
63
|
end
|
64
64
|
|
65
|
+
# A very simple param encoder
|
66
|
+
def encode_params(uri, params)
|
67
|
+
params_str = "?"
|
68
|
+
params.each do |k,v|
|
69
|
+
if v
|
70
|
+
params_str << "#{k}=#{CGI::escape(v.to_s)}&"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
params_str.chop!
|
74
|
+
uri + params_str
|
75
|
+
end
|
76
|
+
|
65
77
|
end
|
66
78
|
end
|
data/lib/rotp/totp.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
+
DEFAULT_INTERVAL = 30
|
2
|
+
|
1
3
|
module ROTP
|
2
4
|
class TOTP < OTP
|
3
5
|
|
4
|
-
attr_reader :interval
|
6
|
+
attr_reader :interval, :issuer
|
5
7
|
|
6
8
|
# @option options [Integer] interval (30) the time interval in seconds for OTP
|
7
9
|
# This defaults to 30 which is standard.
|
8
10
|
def initialize(s, options = {})
|
9
|
-
@interval = options[:interval] ||
|
11
|
+
@interval = options[:interval] || DEFAULT_INTERVAL
|
12
|
+
@issuer = options[:issuer]
|
10
13
|
super
|
11
14
|
end
|
12
15
|
|
@@ -51,7 +54,8 @@ module ROTP
|
|
51
54
|
# @param [String] name of the account
|
52
55
|
# @return [String] provisioning uri
|
53
56
|
def provisioning_uri(name)
|
54
|
-
"otpauth://totp/#{URI.encode(name)}
|
57
|
+
encode_params("otpauth://totp/#{URI.encode(name)}",
|
58
|
+
:period => (interval==30 ? nil : interval), :issuer => issuer, :secret => secret)
|
55
59
|
end
|
56
60
|
|
57
61
|
private
|
data/lib/rotp/version.rb
CHANGED
data/spec/base_spec.rb
CHANGED
@@ -8,6 +8,9 @@ describe "the Base32 implementation" do
|
|
8
8
|
it "should be allow a specific length" do
|
9
9
|
ROTP::Base32.random_base32(32).length.should == 32
|
10
10
|
end
|
11
|
+
it "raise a sane error on a bad decode" do
|
12
|
+
expect { ROTP::Base32.decode("4BCDEFG234BCDEF1") }.to raise_error(ROTP::Base32::Base32Error)
|
13
|
+
end
|
11
14
|
it "should correctly decode a string" do
|
12
15
|
ROTP::Base32.decode("F").unpack('H*').first.should == "28"
|
13
16
|
ROTP::Base32.decode("23").unpack('H*').first.should == "d6"
|
@@ -21,66 +24,3 @@ describe "the Base32 implementation" do
|
|
21
24
|
end
|
22
25
|
end
|
23
26
|
|
24
|
-
describe "HOTP example values from the rfc" do
|
25
|
-
it "should match the RFC" do
|
26
|
-
# 12345678901234567890 in Bas32
|
27
|
-
# GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
|
28
|
-
hotp = ROTP::HOTP.new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
|
29
|
-
hotp.at(0).should ==(755224)
|
30
|
-
hotp.at(1).should ==(287082)
|
31
|
-
hotp.at(2).should ==(359152)
|
32
|
-
hotp.at(3).should ==(969429)
|
33
|
-
hotp.at(4).should ==(338314)
|
34
|
-
hotp.at(5).should ==(254676)
|
35
|
-
hotp.at(6).should ==(287922)
|
36
|
-
hotp.at(7).should ==(162583)
|
37
|
-
hotp.at(8).should ==(399871)
|
38
|
-
hotp.at(9).should ==(520489)
|
39
|
-
end
|
40
|
-
it "should verify an OTP and now allow reuse" do
|
41
|
-
hotp = ROTP::HOTP.new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
|
42
|
-
hotp.verify(520489, 9).should be_true
|
43
|
-
hotp.verify(520489, 10).should be_false
|
44
|
-
end
|
45
|
-
it "should output its provisioning URI" do
|
46
|
-
hotp = ROTP::HOTP.new("wrn3pqx5uqxqvnqr")
|
47
|
-
hotp.provisioning_uri('mark@percival').should == "otpauth://hotp/mark@percival?secret=wrn3pqx5uqxqvnqr&counter=0"
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
describe "TOTP example values from the rfc" do
|
52
|
-
it "should match the RFC" do
|
53
|
-
totp = ROTP::TOTP.new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
|
54
|
-
totp.at(1111111111).should ==(50471)
|
55
|
-
totp.at(1234567890).should ==(5924)
|
56
|
-
totp.at(2000000000).should ==(279037)
|
57
|
-
end
|
58
|
-
|
59
|
-
it "should match the Google Authenticator output" do
|
60
|
-
totp = ROTP::TOTP.new("wrn3pqx5uqxqvnqr")
|
61
|
-
Timecop.freeze(Time.at(1297553958)) do
|
62
|
-
totp.now.should ==(102705)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
it "should match Dropbox 26 char secret output" do
|
66
|
-
totp = ROTP::TOTP.new("tjtpqea6a42l56g5eym73go2oa")
|
67
|
-
Timecop.freeze(Time.at(1378762454)) do
|
68
|
-
totp.now.should ==(747864)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
it "should validate a time based OTP" do
|
72
|
-
totp = ROTP::TOTP.new("wrn3pqx5uqxqvnqr")
|
73
|
-
Timecop.freeze(Time.at(1297553958)) do
|
74
|
-
totp.verify(102705).should be_true
|
75
|
-
end
|
76
|
-
Timecop.freeze(Time.at(1297553958 + 30)) do
|
77
|
-
totp.verify(102705).should be_false
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
|
82
|
-
it "should output its provisioning URI" do
|
83
|
-
totp = ROTP::TOTP.new("wrn3pqx5uqxqvnqr")
|
84
|
-
totp.provisioning_uri('mark@percival').should == "otpauth://totp/mark@percival?secret=wrn3pqx5uqxqvnqr"
|
85
|
-
end
|
86
|
-
end
|
data/spec/hotp_spec.rb
CHANGED
@@ -17,4 +17,34 @@ describe ROTP::HOTP do
|
|
17
17
|
it "should verify a string" do
|
18
18
|
subject.verify("161024", @counter).should be_true
|
19
19
|
end
|
20
|
+
it "should output its provisioning URI" do
|
21
|
+
url = subject.provisioning_uri('mark@percival')
|
22
|
+
params = CGI::parse(URI::parse(url).query)
|
23
|
+
url.should match(/otpauth:\/\/hotp.+/)
|
24
|
+
params["secret"].first.should == "a" * 32
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "HOTP example values from the rfc" do
|
30
|
+
it "should match the RFC" do
|
31
|
+
# 12345678901234567890 in Base32
|
32
|
+
# GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
|
33
|
+
hotp = ROTP::HOTP.new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
|
34
|
+
hotp.at(0).should ==(755224)
|
35
|
+
hotp.at(1).should ==(287082)
|
36
|
+
hotp.at(2).should ==(359152)
|
37
|
+
hotp.at(3).should ==(969429)
|
38
|
+
hotp.at(4).should ==(338314)
|
39
|
+
hotp.at(5).should ==(254676)
|
40
|
+
hotp.at(6).should ==(287922)
|
41
|
+
hotp.at(7).should ==(162583)
|
42
|
+
hotp.at(8).should ==(399871)
|
43
|
+
hotp.at(9).should ==(520489)
|
44
|
+
end
|
45
|
+
it "should verify an OTP and not allow reuse" do
|
46
|
+
hotp = ROTP::HOTP.new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
|
47
|
+
hotp.verify(520489, 9).should be_true
|
48
|
+
hotp.verify(520489, 10).should be_false
|
49
|
+
end
|
20
50
|
end
|
data/spec/totp_spec.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'cgi'
|
1
2
|
require 'spec_helper'
|
2
3
|
|
3
4
|
describe ROTP::TOTP do
|
@@ -18,6 +19,36 @@ describe ROTP::TOTP do
|
|
18
19
|
subject.verify("68212", @now).should be_true
|
19
20
|
end
|
20
21
|
|
22
|
+
it "should output its provisioning URI" do
|
23
|
+
url = subject.provisioning_uri('mark@percival')
|
24
|
+
params = CGI::parse(URI::parse(url).query)
|
25
|
+
url.should match(/otpauth:\/\/totp.+/)
|
26
|
+
params["secret"].first.should == "JBSWY3DPEHPK3PXP"
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with issuer" do
|
30
|
+
subject { ROTP::TOTP.new("JBSWY3DPEHPK3PXP", :issuer => "FooCo") }
|
31
|
+
it "should output its provisioning URI with issuer" do
|
32
|
+
url = subject.provisioning_uri('mark@percival')
|
33
|
+
params = CGI::parse(URI::parse(url).query)
|
34
|
+
url.should match(/otpauth:\/\/totp.+/)
|
35
|
+
params["secret"].first.should == "JBSWY3DPEHPK3PXP"
|
36
|
+
params["issuer"].first.should == "FooCo"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "with non default interval" do
|
41
|
+
subject { ROTP::TOTP.new("JBSWY3DPEHPK3PXP", :interval => 60) }
|
42
|
+
it "should output its provisioning URI with issuer" do
|
43
|
+
url = subject.provisioning_uri('mark@percival')
|
44
|
+
params = CGI::parse(URI::parse(url).query)
|
45
|
+
url.should match(/otpauth:\/\/totp.+/)
|
46
|
+
params["secret"].first.should == "JBSWY3DPEHPK3PXP"
|
47
|
+
params["period"].first.should == "60"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
21
52
|
context "with drift" do
|
22
53
|
it "should verify a number" do
|
23
54
|
subject.verify_with_drift(68212, 0, @now).should be_true
|
@@ -44,3 +75,34 @@ describe ROTP::TOTP do
|
|
44
75
|
end
|
45
76
|
end
|
46
77
|
end
|
78
|
+
|
79
|
+
describe "TOTP example values from the documented output" do
|
80
|
+
it "should match the RFC" do
|
81
|
+
totp = ROTP::TOTP.new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
|
82
|
+
totp.at(1111111111).should ==(50471)
|
83
|
+
totp.at(1234567890).should ==(5924)
|
84
|
+
totp.at(2000000000).should ==(279037)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should match the Google Authenticator output" do
|
88
|
+
totp = ROTP::TOTP.new("wrn3pqx5uqxqvnqr")
|
89
|
+
Timecop.freeze(Time.at(1297553958)) do
|
90
|
+
totp.now.should ==(102705)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
it "should match Dropbox 26 char secret output" do
|
94
|
+
totp = ROTP::TOTP.new("tjtpqea6a42l56g5eym73go2oa")
|
95
|
+
Timecop.freeze(Time.at(1378762454)) do
|
96
|
+
totp.now.should ==(747864)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
it "should validate a time based OTP" do
|
100
|
+
totp = ROTP::TOTP.new("wrn3pqx5uqxqvnqr")
|
101
|
+
Timecop.freeze(Time.at(1297553958)) do
|
102
|
+
totp.verify(102705).should be_true
|
103
|
+
end
|
104
|
+
Timecop.freeze(Time.at(1297553958 + 30)) do
|
105
|
+
totp.verify(102705).should be_false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rotp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Percival
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-11-
|
11
|
+
date: 2013-11-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|