rotp 1.4.6 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://secure.travis-ci.org/mdp/rotp.png)](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
|