otp 0.0.9 → 0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -2
- data/README.md +4 -4
- data/lib/otp/base.rb +24 -21
- data/lib/otp/hotp.rb +5 -6
- data/lib/otp/totp.rb +5 -6
- data/lib/otp/uri.rb +22 -46
- data/lib/otp/utils.rb +16 -3
- data/lib/otp/version.rb +1 -1
- data/test/test_base.rb +46 -11
- data/test/test_uri.rb +59 -3
- 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: 81f98da9bacd6750a761f89590463ed0777b2b1d
|
4
|
+
data.tar.gz: 710bb562bde9f46efcdf89f5e9777839ba8dbf37
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5921f7970174fafc13b4ce15de85f94772ae958338bab7645d9a5804e61b45a6e8ef1666e0a18075dfabb47e52d8356f6fe7a03575d6ac472b5471052a7c755
|
7
|
+
data.tar.gz: 85f05deafa16e5232185255572003d7c3c477ada47079dcd393262983694030f24cc7e540e54d503c4f19f132aee103b1cb5c3b277301efd771cb42f56adbd2c
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -47,11 +47,11 @@ You can use the last and post option parameters to verify several generations, i
|
|
47
47
|
|
48
48
|
TOTP and HOTP algorithm details can be referred at the following URLs.
|
49
49
|
|
50
|
-
* HOTP: An HMAC-Based One-Time Password Algorithm
|
51
|
-
* TOTP: Time-Based One-Time Password Algorithm
|
50
|
+
* [HOTP: An HMAC-Based One-Time Password Algorithm](http://tools.ietf.org/html/rfc4226)
|
51
|
+
* [TOTP: Time-Based One-Time Password Algorithm](http://tools.ietf.org/html/rfc6238)
|
52
52
|
|
53
53
|
In the OTP URI format, the value of "secret" is encoded with BASE32 algorithm.
|
54
54
|
The Format details are described in the document of Google Authenticator.
|
55
55
|
|
56
|
-
* The Base16, Base32, and Base64 Data Encodings
|
57
|
-
* Google Authenticator Key URI format
|
56
|
+
* [The Base16, Base32, and Base64 Data Encodings](http://tools.ietf.org/html/rfc4648)
|
57
|
+
* [Google Authenticator Key URI format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
|
data/lib/otp/base.rb
CHANGED
@@ -9,9 +9,7 @@ module OTP
|
|
9
9
|
DEFAULT_DIGITS = 6
|
10
10
|
DEFAULT_ALGORITHM = "SHA1"
|
11
11
|
|
12
|
-
attr_accessor :secret
|
13
|
-
attr_accessor :algorithm
|
14
|
-
attr_accessor :digits
|
12
|
+
attr_accessor :secret, :algorithm, :digits
|
15
13
|
attr_accessor :issuer, :accountname
|
16
14
|
|
17
15
|
def initialize(secret=nil, algorithm=nil, digits=nil)
|
@@ -21,24 +19,23 @@ module OTP
|
|
21
19
|
end
|
22
20
|
|
23
21
|
def new_secret(num_bytes=10)
|
24
|
-
|
25
|
-
self.secret = OTP::Base32.encode(s)
|
22
|
+
self.raw_secret = OpenSSL::Random.random_bytes(num_bytes)
|
26
23
|
end
|
27
24
|
|
28
|
-
def
|
29
|
-
|
25
|
+
def raw_secret=(bytes)
|
26
|
+
self.secret = OTP::Base32.encode(bytes)
|
30
27
|
end
|
31
28
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
def raw_secret
|
30
|
+
return OTP::Base32.decode(secret)
|
31
|
+
end
|
32
|
+
|
33
|
+
def moving_factor
|
34
|
+
raise NotImplementedError
|
36
35
|
end
|
37
36
|
|
38
37
|
def password(generation=0)
|
39
|
-
|
40
|
-
pw = "0" + pw while pw.length < digits
|
41
|
-
return pw
|
38
|
+
return otp(algorithm, raw_secret, moving_factor+generation, digits)
|
42
39
|
end
|
43
40
|
|
44
41
|
def verify(given_pw, last:0, post:0)
|
@@ -48,18 +45,24 @@ module OTP
|
|
48
45
|
return (-last..post).any?{|i| compare(password(i), given_pw) }
|
49
46
|
end
|
50
47
|
|
51
|
-
## URI related methods
|
52
|
-
|
53
48
|
def to_uri
|
54
|
-
OTP::URI.format(self)
|
49
|
+
return OTP::URI.format(self)
|
55
50
|
end
|
56
51
|
|
57
|
-
def
|
58
|
-
|
52
|
+
def uri_params
|
53
|
+
params = {}
|
54
|
+
params[:secret] = secret
|
55
|
+
params[:issuer] = issuer if issuer
|
56
|
+
params[:algorithm] = algorithm if algorithm != DEFAULT_ALGORITHM
|
57
|
+
params[:digits] = digits if digits != DEFAULT_DIGITS
|
58
|
+
return params
|
59
59
|
end
|
60
60
|
|
61
|
-
def
|
62
|
-
|
61
|
+
def extract_uri_params(params)
|
62
|
+
self.secret = params["secret"]
|
63
|
+
self.issuer = issuer || params["issuer"]
|
64
|
+
self.algorithm = params["algorithm"] || algorithm
|
65
|
+
self.digits = (params["digits"] || digits).to_i
|
63
66
|
end
|
64
67
|
end
|
65
68
|
end
|
data/lib/otp/hotp.rb
CHANGED
@@ -13,14 +13,13 @@ module OTP
|
|
13
13
|
return count
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
return
|
16
|
+
def uri_params
|
17
|
+
return super.merge(count: count)
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
end
|
20
|
+
def extract_uri_params(params)
|
21
|
+
super
|
22
|
+
self.count = (params["count"] || count).to_i
|
24
23
|
end
|
25
24
|
end
|
26
25
|
end
|
data/lib/otp/totp.rb
CHANGED
@@ -16,16 +16,15 @@ module OTP
|
|
16
16
|
return (time || Time.now).to_i / period
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
20
|
-
params =
|
19
|
+
def uri_params
|
20
|
+
params = super
|
21
21
|
params["period"] = period if period != DEFAULT_PERIOD
|
22
22
|
return params
|
23
23
|
end
|
24
24
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
end
|
25
|
+
def extract_uri_params(params)
|
26
|
+
super
|
27
|
+
self.period = (params["period"] || period).to_i
|
29
28
|
end
|
30
29
|
end
|
31
30
|
end
|
data/lib/otp/uri.rb
CHANGED
@@ -2,29 +2,23 @@ require "uri"
|
|
2
2
|
|
3
3
|
module OTP
|
4
4
|
module URI
|
5
|
-
|
5
|
+
SCHEME = "otpauth"
|
6
6
|
|
7
|
-
|
7
|
+
module_function
|
8
8
|
|
9
9
|
def parse(uri_string)
|
10
10
|
uri = ::URI.parse(uri_string)
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
if uri.scheme.downcase != SCHEME
|
12
|
+
raise "URI scheme not match: #{uri.scheme}"
|
13
|
+
end
|
14
|
+
otp = type_to_class(uri).new
|
15
|
+
unless m = %r{/(?:([^:]*): *)?(.+)}.match(::URI.decode(uri.path))
|
16
|
+
raise "account name must be present: #{uri_string}"
|
17
|
+
end
|
14
18
|
otp.issuer = m[1] if m[1]
|
15
19
|
otp.accountname = m[2]
|
16
20
|
query = Hash[::URI.decode_www_form(uri.query)]
|
17
|
-
otp.
|
18
|
-
if value = query["algorithm"]
|
19
|
-
otp.algorithm = value
|
20
|
-
end
|
21
|
-
if value = query["issuer"]
|
22
|
-
otp.issuer = value
|
23
|
-
end
|
24
|
-
if value = query["digits"]
|
25
|
-
otp.digits = value.to_i
|
26
|
-
end
|
27
|
-
otp.extract_type_specific_uri_params(query)
|
21
|
+
otp.extract_uri_params(query)
|
28
22
|
return otp
|
29
23
|
end
|
30
24
|
|
@@ -32,41 +26,23 @@ module OTP
|
|
32
26
|
raise "secret must be set" if otp.secret.nil?
|
33
27
|
raise "accountname must be set" if otp.accountname.nil?
|
34
28
|
typename = otp.class.name.split("::")[-1].downcase
|
35
|
-
label = otp.
|
36
|
-
|
37
|
-
return "
|
29
|
+
label = otp.accountname.dup
|
30
|
+
label.prepend("#{otp.issuer}:") if otp.issuer
|
31
|
+
return "%s://%s/%s?%s" % [
|
32
|
+
SCHEME,
|
38
33
|
::URI.encode(typename),
|
39
34
|
::URI.encode(label),
|
40
|
-
::URI.encode_www_form(
|
35
|
+
::URI.encode_www_form(otp.uri_params)
|
41
36
|
]
|
42
37
|
end
|
43
38
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
raise "unknown OTP type: #{uri.host}"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def pickup_params(otp)
|
56
|
-
param_spec = [
|
57
|
-
[:secret, nil],
|
58
|
-
[:issuer, nil],
|
59
|
-
[:algorithm, OTP::Base::DEFAULT_ALGORITHM],
|
60
|
-
[:digits, OTP::Base::DEFAULT_DIGITS],
|
61
|
-
]
|
62
|
-
params = param_spec.reduce({}) do |h, (name, default)|
|
63
|
-
value = otp.send(name)
|
64
|
-
if value && value != default
|
65
|
-
h[name] = value
|
66
|
-
end
|
67
|
-
h
|
68
|
-
end
|
69
|
-
return params.merge(otp.type_specific_uri_params)
|
39
|
+
def type_to_class(uri)
|
40
|
+
klass = OTP.const_get(uri.host.upcase)
|
41
|
+
raise unless klass.is_a?(Class)
|
42
|
+
raise unless klass.ancestors.include?(OTP::Base)
|
43
|
+
return klass
|
44
|
+
rescue
|
45
|
+
raise "unknown OTP type: #{uri.host}"
|
70
46
|
end
|
71
47
|
end
|
72
48
|
end
|
data/lib/otp/utils.rb
CHANGED
@@ -4,6 +4,13 @@ module OTP
|
|
4
4
|
module Utils
|
5
5
|
private
|
6
6
|
|
7
|
+
def otp(algorithm, secret, moving_factor, digits)
|
8
|
+
message = pack_int64(moving_factor)
|
9
|
+
digest = hmac(algorithm, secret, message)
|
10
|
+
num = pickup(digest)
|
11
|
+
return truncate(num, digits)
|
12
|
+
end
|
13
|
+
|
7
14
|
def pack_int64(i)
|
8
15
|
return [i >> 32 & 0xffffffff, i & 0xffffffff].pack("NN")
|
9
16
|
end
|
@@ -14,12 +21,18 @@ module OTP
|
|
14
21
|
return mac.digest
|
15
22
|
end
|
16
23
|
|
17
|
-
def
|
18
|
-
offset =
|
19
|
-
binary =
|
24
|
+
def pickup(digest)
|
25
|
+
offset = digest[-1].ord & 0xf
|
26
|
+
binary = digest[offset, 4]
|
20
27
|
return binary.unpack("N")[0] & 0x7fffffff
|
21
28
|
end
|
22
29
|
|
30
|
+
def truncate(num, digits)
|
31
|
+
pw = (num % (10 ** digits)).to_s
|
32
|
+
pw.prepend("0") while pw.length < digits
|
33
|
+
return pw
|
34
|
+
end
|
35
|
+
|
23
36
|
def compare(a, b)
|
24
37
|
return a.to_i == b.to_i
|
25
38
|
end
|
data/lib/otp/version.rb
CHANGED
data/test/test_base.rb
CHANGED
@@ -1,25 +1,60 @@
|
|
1
1
|
require_relative "helper"
|
2
2
|
|
3
3
|
class TestBase < Test::Unit::TestCase
|
4
|
-
def
|
4
|
+
def test_new_secret
|
5
5
|
otp = OTP::Base.new
|
6
|
+
|
6
7
|
otp.new_secret(20)
|
8
|
+
assert_equal(20, otp.raw_secret.length)
|
7
9
|
assert_equal(32, otp.secret.length)
|
10
|
+
|
8
11
|
otp.new_secret(40)
|
12
|
+
assert_equal(40, otp.raw_secret.length)
|
9
13
|
assert_equal(64, otp.secret.length)
|
10
14
|
end
|
11
15
|
|
12
|
-
def
|
16
|
+
def test_secret
|
17
|
+
otp = OTP::Base.new
|
18
|
+
|
19
|
+
otp.secret = nil
|
20
|
+
assert_nil(otp.secret)
|
21
|
+
assert_nil(otp.raw_secret)
|
22
|
+
|
23
|
+
otp.secret = ""
|
24
|
+
assert_equal("", otp.secret)
|
25
|
+
assert_equal("", otp.raw_secret)
|
26
|
+
|
27
|
+
otp.secret = "MZXW6YTBOI======"
|
28
|
+
assert_equal("MZXW6YTBOI======", otp.secret)
|
29
|
+
assert_equal("foobar", otp.raw_secret)
|
30
|
+
|
31
|
+
otp.secret = "MZXW6YTBOI"
|
32
|
+
assert_equal("MZXW6YTBOI", otp.secret)
|
33
|
+
assert_equal("foobar", otp.raw_secret)
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_raw_secret
|
37
|
+
otp = OTP::Base.new
|
38
|
+
|
39
|
+
otp.raw_secret = nil
|
40
|
+
assert_nil(otp.secret)
|
41
|
+
assert_nil(otp.raw_secret)
|
42
|
+
|
43
|
+
otp.raw_secret = ""
|
44
|
+
assert_equal("", otp.secret)
|
45
|
+
assert_equal("", otp.raw_secret)
|
46
|
+
|
47
|
+
otp.raw_secret = "foobarbaz"
|
48
|
+
assert_equal("MZXW6YTBOJRGC6Q=", otp.secret)
|
49
|
+
assert_equal("foobarbaz", otp.raw_secret)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_moving_factor
|
13
53
|
base = OTP::Base.new
|
54
|
+
hotp = OTP::HOTP.new
|
14
55
|
totp = OTP::TOTP.new
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
[:type_specific_uri_params, ],
|
19
|
-
[:extract_type_specific_uri_params, {}],
|
20
|
-
].each do |m, *args|
|
21
|
-
assert_raise(NotImplementedError){ base.send(m, *args) }
|
22
|
-
assert_nothing_raised{ totp.send(m, *args) }
|
23
|
-
end
|
56
|
+
assert_raise(NotImplementedError){ base.moving_factor }
|
57
|
+
assert_nothing_raised{ hotp.moving_factor }
|
58
|
+
assert_nothing_raised{ totp.moving_factor }
|
24
59
|
end
|
25
60
|
end
|
data/test/test_uri.rb
CHANGED
@@ -7,17 +7,45 @@ class TestURI < Test::Unit::TestCase
|
|
7
7
|
assert_equal("account@example.com", otp.accountname)
|
8
8
|
assert_equal(nil, otp.issuer)
|
9
9
|
|
10
|
+
uri = "otpauth://totp/account@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Foo"
|
11
|
+
otp = OTP::URI.parse(uri)
|
12
|
+
assert_equal("account@example.com", otp.accountname)
|
13
|
+
assert_equal("Foo", otp.issuer)
|
14
|
+
|
10
15
|
uri = "otpauth://totp/My%20Company:%20%20account@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
11
16
|
otp = OTP::URI.parse(uri)
|
12
17
|
assert_equal("account@example.com", otp.accountname)
|
13
18
|
assert_equal("My Company", otp.issuer)
|
14
19
|
|
15
|
-
uri = "otpauth://totp/My%20Company:%20%20account@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
20
|
+
uri = "otpauth://totp/My%20Company:%20%20account@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Foo"
|
16
21
|
otp = OTP::URI.parse(uri)
|
17
22
|
assert_equal("account@example.com", otp.accountname)
|
18
23
|
assert_equal("My Company", otp.issuer)
|
19
24
|
end
|
20
25
|
|
26
|
+
def test_totp_simple
|
27
|
+
secret = OTP::Base32.encode("12345678901234567890")
|
28
|
+
totp = OTP::TOTP.new
|
29
|
+
totp.secret = secret
|
30
|
+
totp.accountname = "account@example.com"
|
31
|
+
uri = totp.to_uri
|
32
|
+
assert_not_match(/algorithm=/, uri)
|
33
|
+
assert_not_match(/digits=/, uri)
|
34
|
+
assert_not_match(/issuer=/, uri)
|
35
|
+
assert_not_match(/period=/, uri)
|
36
|
+
|
37
|
+
otp = OTP::URI.parse(uri)
|
38
|
+
assert_equal(OTP::TOTP, otp.class)
|
39
|
+
assert_equal(secret, otp.secret)
|
40
|
+
assert_equal("SHA1", otp.algorithm)
|
41
|
+
assert_equal(6, otp.digits)
|
42
|
+
assert_equal(30, otp.period)
|
43
|
+
assert_equal("account@example.com", otp.accountname)
|
44
|
+
assert_equal(nil, otp.issuer)
|
45
|
+
totp.time = otp.time = Time.now
|
46
|
+
assert_equal(otp.password, totp.password)
|
47
|
+
end
|
48
|
+
|
21
49
|
def test_totp
|
22
50
|
secret = OTP::Base32.encode("12345678901234567890")
|
23
51
|
totp = OTP::TOTP.new
|
@@ -41,6 +69,28 @@ class TestURI < Test::Unit::TestCase
|
|
41
69
|
assert_equal(otp.password, totp.password)
|
42
70
|
end
|
43
71
|
|
72
|
+
def test_hotp_simple
|
73
|
+
secret = OTP::Base32.encode("12345678901234567890")
|
74
|
+
hotp = OTP::HOTP.new
|
75
|
+
hotp.secret = secret
|
76
|
+
hotp.accountname = "account@example.com"
|
77
|
+
uri = hotp.to_uri
|
78
|
+
assert_not_match(/algorithm=/, uri)
|
79
|
+
assert_not_match(/digits=/, uri)
|
80
|
+
assert_not_match(/issuer=/, uri)
|
81
|
+
assert_match(/count=0/, uri)
|
82
|
+
|
83
|
+
otp = OTP::URI.parse(uri)
|
84
|
+
assert_equal(OTP::HOTP, otp.class)
|
85
|
+
assert_equal(secret, otp.secret)
|
86
|
+
assert_equal("SHA1", otp.algorithm)
|
87
|
+
assert_equal(6, otp.digits)
|
88
|
+
assert_equal(0, otp.count)
|
89
|
+
assert_equal("account@example.com", otp.accountname)
|
90
|
+
assert_equal(nil, otp.issuer)
|
91
|
+
assert_equal(otp.password, hotp.password)
|
92
|
+
end
|
93
|
+
|
44
94
|
def test_hotp
|
45
95
|
secret = OTP::Base32.encode("12345678901234567890")
|
46
96
|
hotp = OTP::HOTP.new
|
@@ -64,7 +114,13 @@ class TestURI < Test::Unit::TestCase
|
|
64
114
|
end
|
65
115
|
|
66
116
|
def test_parse_invalid
|
67
|
-
assert_raise(RuntimeError){ OTP::URI.parse("http://www.netlab.jp") }
|
68
|
-
|
117
|
+
e = assert_raise(RuntimeError){ OTP::URI.parse("http://www.netlab.jp") }
|
118
|
+
assert_match(/URI scheme not match/, e.message)
|
119
|
+
e = assert_raise(RuntimeError){ OTP::URI.parse("otpauth://foo") }
|
120
|
+
assert_match(/unknown OTP type/, e.message)
|
121
|
+
e = assert_raise(RuntimeError){ OTP::URI.parse("otpauth://version") }
|
122
|
+
assert_match(/unknown OTP type/, e.message)
|
123
|
+
e = assert_raise(RuntimeError){ OTP::URI.parse("otpauth://totp/") }
|
124
|
+
assert_match(/account name must be present/, e.message)
|
69
125
|
end
|
70
126
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: otp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yuuzou Gotou
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-06-
|
11
|
+
date: 2015-06-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|