otp 0.0.9 → 0.0.10
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/.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
|