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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 032b0bd99d66d97d8281cd5006409b1f482c2314
4
- data.tar.gz: ca0320a12fe5d73c907e8cd445dac6fd0219112f
3
+ metadata.gz: 81f98da9bacd6750a761f89590463ed0777b2b1d
4
+ data.tar.gz: 710bb562bde9f46efcdf89f5e9777839ba8dbf37
5
5
  SHA512:
6
- metadata.gz: f60da46723a92855571fcabbf0b8e0ad264093822516ebdd9d152181cb102280c92a3ce97a499b9bf661aac9adf6ff2694e2ae02294eb39d2e6d1be70d3eb8ed
7
- data.tar.gz: 8d90aba69fd87db84b94a3b0a374f817a8566d970be4820e868f16a1098b81e8de188247bc9b29c4f811a0e4779bb1c8cc43907aef5fc1b4fd974a7fd68e28bb
6
+ metadata.gz: a5921f7970174fafc13b4ce15de85f94772ae958338bab7645d9a5804e61b45a6e8ef1666e0a18075dfabb47e52d8356f6fe7a03575d6ac472b5471052a7c755
7
+ data.tar.gz: 85f05deafa16e5232185255572003d7c3c477ada47079dcd393262983694030f24cc7e540e54d503c4f19f132aee103b1cb5c3b277301efd771cb42f56adbd2c
@@ -39,8 +39,8 @@ Style/FormatString:
39
39
  Style/EachWithObject:
40
40
  Enabled: false
41
41
 
42
- Metrics/AbcSize:
43
- Max: 20
42
+ #Metrics/AbcSize:
43
+ # Max: 20
44
44
 
45
45
  Metrics/MethodLength:
46
46
  Max: 30
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 - http://tools.ietf.org/html/rfc4226
51
- * TOTP: Time-Based One-Time Password Algorithm - http://tools.ietf.org/html/rfc6238
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 - http://tools.ietf.org/html/rfc4648
57
- * Google Authenticator Key URI format - https://github.com/google/google-authenticator/wiki/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)
@@ -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
- s = (0...num_bytes).map{ Random.rand(256).chr }.join
25
- self.secret = OTP::Base32.encode(s)
22
+ self.raw_secret = OpenSSL::Random.random_bytes(num_bytes)
26
23
  end
27
24
 
28
- def moving_factor
29
- raise NotImplementedError
25
+ def raw_secret=(bytes)
26
+ self.secret = OTP::Base32.encode(bytes)
30
27
  end
31
28
 
32
- def otp(generation=0)
33
- hash = hmac(algorithm, OTP::Base32.decode(secret),
34
- pack_int64(moving_factor+generation))
35
- return truncate(hash)
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
- pw = (otp(generation) % (10 ** digits)).to_s
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 type_specific_uri_params
58
- raise NotImplementedError
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 extract_type_specific_uri_params(query)
62
- raise NotImplementedError
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
@@ -13,14 +13,13 @@ module OTP
13
13
  return count
14
14
  end
15
15
 
16
- def type_specific_uri_params
17
- return {count: count}
16
+ def uri_params
17
+ return super.merge(count: count)
18
18
  end
19
19
 
20
- def extract_type_specific_uri_params(query)
21
- if value = query["count"]
22
- self.count = value.to_i
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
@@ -16,16 +16,15 @@ module OTP
16
16
  return (time || Time.now).to_i / period
17
17
  end
18
18
 
19
- def type_specific_uri_params
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 extract_type_specific_uri_params(query)
26
- if value = query["period"]
27
- self.period = value.to_i
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
@@ -2,29 +2,23 @@ require "uri"
2
2
 
3
3
  module OTP
4
4
  module URI
5
- module_function
5
+ SCHEME = "otpauth"
6
6
 
7
- SCHEME = "othauth"
7
+ module_function
8
8
 
9
9
  def parse(uri_string)
10
10
  uri = ::URI.parse(uri_string)
11
- raise "URI scheme not match: #{uri.scheme}" unless uri.scheme != SCHEME
12
- otp = otp_class(uri).new
13
- m = %r{/(?:([^:]*): *)?(.+)}.match(::URI.decode(uri.path))
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.secret = query["secret"]
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.issuer ? "#{otp.issuer}:#{otp.accountname}" : otp.accountname
36
- params = pickup_params(otp)
37
- return "otpauth://%s/%s?%s" % [
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(params)
35
+ ::URI.encode_www_form(otp.uri_params)
41
36
  ]
42
37
  end
43
38
 
44
- def otp_class(uri)
45
- case uri.host.upcase
46
- when "HOTP"
47
- OTP::HOTP
48
- when "TOTP"
49
- OTP::TOTP
50
- else
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
@@ -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 truncate(hash)
18
- offset = hash[-1].ord & 0xf
19
- binary = hash[offset, 4]
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
@@ -1,3 +1,3 @@
1
1
  module OTP
2
- VERSION = "0.0.9"
2
+ VERSION = "0.0.10"
3
3
  end
@@ -1,25 +1,60 @@
1
1
  require_relative "helper"
2
2
 
3
3
  class TestBase < Test::Unit::TestCase
4
- def test_base
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 test_methods_expected_to_be_override
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
- [:moving_factor, ],
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
@@ -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
- assert_raise(RuntimeError){ OTP::URI.parse("otpauth://foo") }
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.9
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-08 00:00:00.000000000 Z
11
+ date: 2015-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler