rotp 4.0.2 → 6.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -4
- data/CHANGELOG.md +36 -0
- data/Dockerfile-2.3 +1 -7
- data/Dockerfile-2.5 +10 -0
- data/Dockerfile-2.6 +11 -0
- data/Dockerfile-2.7 +11 -0
- data/Dockerfile-3.0-rc +12 -0
- data/README.md +46 -10
- data/docker-compose.yml +37 -0
- data/lib/rotp/arguments.rb +4 -0
- data/lib/rotp/base32.rb +56 -32
- data/lib/rotp/cli.rb +3 -3
- data/lib/rotp/hotp.rb +1 -6
- data/lib/rotp/otp/uri.rb +79 -0
- data/lib/rotp/otp.rb +2 -12
- data/lib/rotp/totp.rb +1 -13
- data/lib/rotp/version.rb +1 -1
- data/lib/rotp.rb +2 -3
- data/rotp.gemspec +3 -6
- data/spec/lib/rotp/base32_spec.rb +44 -17
- data/spec/lib/rotp/cli_spec.rb +16 -0
- data/spec/lib/rotp/hotp_spec.rb +6 -21
- data/spec/lib/rotp/otp/uri_spec.rb +99 -0
- data/spec/lib/rotp/totp_spec.rb +3 -73
- metadata +15 -32
- data/Dockerfile-1.9 +0 -15
- data/Dockerfile-2.1 +0 -16
- data/Rakefile +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd976bfa6985075f5e2b76607256d0afbbdf88a82c38cd094d0eaffbb5bce4f2
|
4
|
+
data.tar.gz: 70df660f1eca3dd9efc7baa1f53061ba9af1bbb49e4bb6ead507509f6e845d38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7fb326cc887a1a5614c90c492ac43b72188f75caa90fcc50c3338d129abe2efe4f67af88d018c378379806f1bef0c1d0e40fc6c683f4427f40ad411326729022
|
7
|
+
data.tar.gz: 4f913bf0693c1cead926bfe625e226fe8277323f93a552459447a792cd27b9189860ab26d3907baafff0e217430b04fb9e8b1829b0795e4c96e83062fac409fb
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,41 @@
|
|
1
1
|
### Changelog
|
2
2
|
|
3
|
+
### 6.2.0
|
4
|
+
|
5
|
+
- Update to expand compatibility with Ruby 3. This was only a change to the
|
6
|
+
gemspec, no code changes were necessary.
|
7
|
+
|
8
|
+
### 6.1.0
|
9
|
+
|
10
|
+
- Fixing URI encoding issues again, breaking out into it's own module
|
11
|
+
due to the complexity - closes #100 (@atcruice)
|
12
|
+
- Add docker-compose.yml to help with easier testing
|
13
|
+
|
14
|
+
### 6.0.0
|
15
|
+
|
16
|
+
- Dropping support for Ruby <2.3 (Major version bump)
|
17
|
+
- Fix issue when using --enable-frozen-string-literal Ruby option #95 (jeremyevans)
|
18
|
+
- URI Encoding fix #94 (ksuh90)
|
19
|
+
- Update gems (rake, addressable)
|
20
|
+
- Update Travis tests to include Ruby 2.7
|
21
|
+
|
22
|
+
### 5.1.0
|
23
|
+
|
24
|
+
- Create `random_base32` to perform `random` to avoid breaking changes
|
25
|
+
- Still needed to bump to 5.x due to Base32 cleanup
|
26
|
+
|
27
|
+
### 5.0.0
|
28
|
+
|
29
|
+
- Clean up base32 implementation to match Google Autheticator
|
30
|
+
- BREAKING `Base32.random_base32` renamed to random
|
31
|
+
- The argument is now byte length vs output string length for more precise bit strengths
|
32
|
+
|
33
|
+
### 4.1.0
|
34
|
+
|
35
|
+
- Add a digest option to the CLI #83
|
36
|
+
- Fix provisioning URI is README #82
|
37
|
+
- Improvements to docs
|
38
|
+
|
3
39
|
### 4.0.2
|
4
40
|
|
5
41
|
- Fix gemspec requirment for Addressable
|
data/Dockerfile-2.3
CHANGED
@@ -1,16 +1,10 @@
|
|
1
1
|
FROM ruby:2.3
|
2
2
|
|
3
|
-
# throw errors if Gemfile has been modified since Gemfile.lock
|
4
|
-
RUN bundle config --global frozen 1
|
5
|
-
|
6
3
|
RUN mkdir -p /usr/src/app
|
7
4
|
WORKDIR /usr/src/app
|
8
5
|
|
9
6
|
COPY Gemfile /usr/src/app/
|
10
|
-
COPY Gemfile.lock /usr/src/app/
|
11
7
|
COPY . /usr/src/app
|
12
8
|
RUN bundle install
|
13
9
|
|
14
|
-
|
15
|
-
CMD ["bundler", "exec", "rspec"]
|
16
|
-
|
10
|
+
CMD ["bundle", "exec", "rspec"]
|
data/Dockerfile-2.5
ADDED
data/Dockerfile-2.6
ADDED
data/Dockerfile-2.7
ADDED
data/Dockerfile-3.0-rc
ADDED
data/README.md
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/mdp/rotp.svg?branch=master)](https://travis-ci.org/mdp/rotp)
|
4
4
|
[![Gem Version](https://badge.fury.io/rb/rotp.svg)](https://rubygems.org/gems/rotp)
|
5
|
+
[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](https://www.rubydoc.info/github/mdp/rotp/master)
|
5
6
|
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/mdp/rotp/blob/master/LICENSE)
|
6
7
|
|
7
8
|
A ruby library for generating and validating one time passwords (HOTP & TOTP) according to [RFC 4226](http://tools.ietf.org/html/rfc4226) and [RFC 6238](http://tools.ietf.org/html/rfc6238).
|
@@ -15,7 +16,20 @@ Many websites use this for [multi-factor authentication](https://www.youtube.com
|
|
15
16
|
* OpenSSL
|
16
17
|
* Ruby 2.0 or higher
|
17
18
|
|
18
|
-
## Breaking changes
|
19
|
+
## Breaking changes
|
20
|
+
|
21
|
+
### Breaking changes in >= 6.0
|
22
|
+
|
23
|
+
- Dropping support for Ruby <2.3
|
24
|
+
|
25
|
+
### Breaking changes in >= 5.0
|
26
|
+
|
27
|
+
- `ROTP::Base32.random_base32` is now `ROTP::Base32.random` and the argument
|
28
|
+
has changed from secret string length to byte length to allow for more
|
29
|
+
precision. There is an alias to allow for `random_base32` for the time being.
|
30
|
+
- Cleaned up the Base32 implementation to match Google Authenticator's version.
|
31
|
+
|
32
|
+
### Breaking changes in >= 4.0
|
19
33
|
|
20
34
|
- Simplified API
|
21
35
|
- `verify` now takes options for `drift` and `after`
|
@@ -56,8 +70,8 @@ hotp.at(1) # => "595254"
|
|
56
70
|
hotp.at(1401) # => "259769"
|
57
71
|
|
58
72
|
# OTP verified with a counter
|
59
|
-
hotp.verify("
|
60
|
-
hotp.verify("
|
73
|
+
hotp.verify("259769", 1401) # => 1401
|
74
|
+
hotp.verify("259769", 1402) # => nil
|
61
75
|
```
|
62
76
|
|
63
77
|
### Preventing reuse of Time based OTP's
|
@@ -68,18 +82,21 @@ the interval window (default 30 seconds)
|
|
68
82
|
The following is an example of this in action:
|
69
83
|
|
70
84
|
```ruby
|
71
|
-
User.find(someUserID)
|
85
|
+
user = User.find(someUserID)
|
72
86
|
totp = ROTP::TOTP.new(user.otp_secret)
|
73
87
|
totp.now # => "492039"
|
74
88
|
|
89
|
+
# Let's take a look at the last time the user authenticated with an OTP
|
75
90
|
user.last_otp_at # => 1432703530
|
76
91
|
|
77
92
|
# Verify the OTP
|
78
93
|
last_otp_at = totp.verify("492039", after: user.last_otp_at) #=> 1472145760
|
79
94
|
# ROTP returns the timestamp(int) of the current period
|
95
|
+
|
80
96
|
# Store this on the user's account
|
81
97
|
user.update(last_otp_at: last_otp_at)
|
82
|
-
|
98
|
+
|
99
|
+
# Someone attempts to reuse the OTP inside the 30s window
|
83
100
|
last_otp_at = totp.verify("492039", after: user.last_otp_at) #=> nil
|
84
101
|
# It fails to verify because we are still in the same 30s interval window
|
85
102
|
```
|
@@ -104,7 +121,7 @@ totp.verify("250939", drift_behind: 15, at: now + 45) # => nil
|
|
104
121
|
### Generating a Base32 Secret key
|
105
122
|
|
106
123
|
```ruby
|
107
|
-
ROTP::Base32.
|
124
|
+
ROTP::Base32.random # returns a 160 bit (32 character) base32 secret. Compatible with Google Authenticator
|
108
125
|
```
|
109
126
|
|
110
127
|
Note: The Base32 format conforms to [RFC 4648 Base32](http://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet)
|
@@ -115,8 +132,11 @@ Provisioning URI's generated by ROTP are compatible with most One Time Password
|
|
115
132
|
Google Authenticator.
|
116
133
|
|
117
134
|
```ruby
|
118
|
-
totp.
|
119
|
-
|
135
|
+
totp = ROTP::TOTP.new("base32secret3232", issuer: "My Service")
|
136
|
+
totp.provisioning_uri("alice@google.com") # => 'otpauth://totp/My%20Service:alice%40google.com?secret=base32secret3232&issuer=My%20Service'
|
137
|
+
|
138
|
+
hotp = ROTP::HOTP.new("base32secret3232", issuer: "My Service")
|
139
|
+
hotp.provisioning_uri("alice@google.com", 0) # => 'otpauth://hotp/alice%40google.com?secret=base32secret3232&counter=0'
|
120
140
|
```
|
121
141
|
|
122
142
|
This can then be rendered as a QR Code which the user can scan using their mobile phone and the appropriate application.
|
@@ -143,9 +163,25 @@ bundle install
|
|
143
163
|
bundle exec rspec
|
144
164
|
```
|
145
165
|
|
166
|
+
### Testing with Docker
|
167
|
+
|
168
|
+
In order to make it easier to test against different ruby version, ROTP comes
|
169
|
+
with a set of Dockerfiles for each version that we test against in Travis
|
170
|
+
|
171
|
+
```bash
|
172
|
+
docker build -f Dockerfile-2.6 -t rotp_2.6 .
|
173
|
+
docker run --rm -v $(pwd):/usr/src/app rotp_2.6
|
174
|
+
```
|
175
|
+
|
176
|
+
Alternately, you may use docker-compose to run all the tests:
|
177
|
+
|
178
|
+
```
|
179
|
+
docker-compose up
|
180
|
+
```
|
181
|
+
|
146
182
|
## Executable Usage
|
147
183
|
|
148
|
-
The rotp rubygem includes
|
184
|
+
The rotp rubygem includes CLI version to help with testing and debugging
|
149
185
|
|
150
186
|
```bash
|
151
187
|
# Try this to get an overview of the commands
|
@@ -162,7 +198,7 @@ Have a look at the [contributors graph](https://github.com/mdp/rotp/graphs/contr
|
|
162
198
|
|
163
199
|
## License
|
164
200
|
|
165
|
-
MIT Copyright (C)
|
201
|
+
MIT Copyright (C) 2019 by Mark Percival, see [LICENSE](https://github.com/mdp/rotp/blob/master/LICENSE) for details.
|
166
202
|
|
167
203
|
## Other implementations
|
168
204
|
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
version: "3.8"
|
2
|
+
services:
|
3
|
+
ruby_2_3:
|
4
|
+
build:
|
5
|
+
context: .
|
6
|
+
dockerfile: Dockerfile-2.3
|
7
|
+
volumes:
|
8
|
+
- "./lib:/usr/src/app/lib"
|
9
|
+
- "./spec:/usr/src/app/spec"
|
10
|
+
ruby_2_5:
|
11
|
+
build:
|
12
|
+
context: .
|
13
|
+
dockerfile: Dockerfile-2.5
|
14
|
+
volumes:
|
15
|
+
- "./lib:/usr/src/app/lib"
|
16
|
+
- "./spec:/usr/src/app/spec"
|
17
|
+
ruby_2_6:
|
18
|
+
build:
|
19
|
+
context: .
|
20
|
+
dockerfile: Dockerfile-2.6
|
21
|
+
volumes:
|
22
|
+
- "./lib:/usr/src/app/lib"
|
23
|
+
- "./spec:/usr/src/app/spec"
|
24
|
+
ruby_2_7:
|
25
|
+
build:
|
26
|
+
context: .
|
27
|
+
dockerfile: Dockerfile-2.7
|
28
|
+
volumes:
|
29
|
+
- "./lib:/usr/src/app/lib"
|
30
|
+
- "./spec:/usr/src/app/spec"
|
31
|
+
ruby_3_0_rc:
|
32
|
+
build:
|
33
|
+
context: .
|
34
|
+
dockerfile: Dockerfile-3.0-rc
|
35
|
+
volumes:
|
36
|
+
- "./lib:/usr/src/app/lib"
|
37
|
+
- "./spec:/usr/src/app/spec"
|
data/lib/rotp/arguments.rb
CHANGED
@@ -68,6 +68,10 @@ module ROTP
|
|
68
68
|
parser.on_tail('-h', '--help', 'Show this message') do
|
69
69
|
options!.mode = :help
|
70
70
|
end
|
71
|
+
|
72
|
+
parser.on('-d', '--digest [ALGORITHM]', 'Use algorithm for the digest (default sha1)') do |digest|
|
73
|
+
options!.digest = digest
|
74
|
+
end
|
71
75
|
end
|
72
76
|
end
|
73
77
|
|
data/lib/rotp/base32.rb
CHANGED
@@ -1,51 +1,75 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
1
3
|
module ROTP
|
2
4
|
class Base32
|
3
5
|
class Base32Error < RuntimeError; end
|
4
|
-
CHARS = '
|
6
|
+
CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.each_char.to_a
|
7
|
+
SHIFT = 5
|
8
|
+
MASK = 31
|
5
9
|
|
6
10
|
class << self
|
11
|
+
|
7
12
|
def decode(str)
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
+
buffer = 0
|
14
|
+
idx = 0
|
15
|
+
bits_left = 0
|
16
|
+
str = str.tr('=', '').upcase
|
17
|
+
result = []
|
18
|
+
str.split('').each do |char|
|
19
|
+
buffer = buffer << SHIFT
|
20
|
+
buffer = buffer | (decode_quint(char) & MASK)
|
21
|
+
bits_left = bits_left + SHIFT
|
22
|
+
if bits_left >= 8
|
23
|
+
result[idx] = (buffer >> (bits_left - 8)) & 255
|
24
|
+
idx = idx + 1
|
25
|
+
bits_left = bits_left - 8
|
26
|
+
end
|
13
27
|
end
|
14
|
-
|
28
|
+
result.pack('c*')
|
15
29
|
end
|
16
30
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
31
|
+
def encode(b)
|
32
|
+
data = b.unpack('c*')
|
33
|
+
out = String.new
|
34
|
+
buffer = data[0]
|
35
|
+
idx = 1
|
36
|
+
bits_left = 8
|
37
|
+
while bits_left > 0 || idx < data.length
|
38
|
+
if bits_left < SHIFT
|
39
|
+
if idx < data.length
|
40
|
+
buffer = buffer << 8
|
41
|
+
buffer = buffer | (data[idx] & 255)
|
42
|
+
bits_left = bits_left + 8
|
43
|
+
idx = idx + 1
|
44
|
+
else
|
45
|
+
pad = SHIFT - bits_left
|
46
|
+
buffer = buffer << pad
|
47
|
+
bits_left = bits_left + pad
|
48
|
+
end
|
49
|
+
end
|
50
|
+
val = MASK & (buffer >> (bits_left - SHIFT))
|
51
|
+
bits_left = bits_left - SHIFT
|
52
|
+
out.concat(CHARS[val])
|
21
53
|
end
|
22
|
-
|
54
|
+
return out
|
23
55
|
end
|
24
56
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
bytes = []
|
31
|
-
bytes[0] = (quints[0] << 3) + (quints[1] ? quints[1] >> 2 : 0)
|
32
|
-
return bytes if length < 3
|
33
|
-
|
34
|
-
bytes[1] = ((quints[1] & 3) << 6) + (quints[2] << 1) + (quints[3] ? quints[3] >> 4 : 0)
|
35
|
-
return bytes if length < 4
|
36
|
-
|
37
|
-
bytes[2] = ((quints[3] & 15) << 4) + (quints[4] ? quints[4] >> 1 : 0)
|
38
|
-
return bytes if length < 6
|
39
|
-
|
40
|
-
bytes[3] = ((quints[4] & 1) << 7) + (quints[5] << 2) + (quints[6] ? quints[6] >> 3 : 0)
|
41
|
-
return bytes if length < 7
|
57
|
+
# Defaults to 160 bit long secret (meaning a 32 character long base32 secret)
|
58
|
+
def random(byte_length = 20)
|
59
|
+
rand_bytes = SecureRandom.random_bytes(byte_length)
|
60
|
+
self.encode(rand_bytes)
|
61
|
+
end
|
42
62
|
|
43
|
-
|
44
|
-
|
63
|
+
# Prevent breaking changes
|
64
|
+
def random_base32(str_len = 32)
|
65
|
+
byte_length = str_len * 5/8
|
66
|
+
random(byte_length)
|
45
67
|
end
|
46
68
|
|
69
|
+
private
|
70
|
+
|
47
71
|
def decode_quint(q)
|
48
|
-
CHARS.index(q
|
72
|
+
CHARS.index(q) || raise(Base32Error, "Invalid Base32 Character - '#{q}'")
|
49
73
|
end
|
50
74
|
end
|
51
75
|
end
|
data/lib/rotp/cli.rb
CHANGED
@@ -19,7 +19,7 @@ module ROTP
|
|
19
19
|
if %i[time hmac].include?(options.mode)
|
20
20
|
if options.secret.to_s == ''
|
21
21
|
red 'You must also specify a --secret. Try --help for help.'
|
22
|
-
elsif options.secret.to_s.chars.any? { |c| ROTP::Base32::CHARS.index(c.
|
22
|
+
elsif options.secret.to_s.chars.any? { |c| ROTP::Base32::CHARS.index(c.upcase).nil? }
|
23
23
|
red 'Secret must be in RFC4648 Base32 format - http://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet'
|
24
24
|
end
|
25
25
|
end
|
@@ -31,9 +31,9 @@ module ROTP
|
|
31
31
|
return arguments.to_s if options.mode == :help
|
32
32
|
|
33
33
|
if options.mode == :time
|
34
|
-
ROTP::TOTP.new(options.secret).now
|
34
|
+
ROTP::TOTP.new(options.secret, options).now
|
35
35
|
elsif options.mode == :hmac
|
36
|
-
ROTP::HOTP.new(options.secret).at options.counter
|
36
|
+
ROTP::HOTP.new(options.secret, options).at options.counter
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
data/lib/rotp/hotp.rb
CHANGED
@@ -25,12 +25,7 @@ module ROTP
|
|
25
25
|
# @param [Integer] initial_count starting counter value, defaults to 0
|
26
26
|
# @return [String] provisioning uri
|
27
27
|
def provisioning_uri(name, initial_count = 0)
|
28
|
-
|
29
|
-
secret: secret,
|
30
|
-
counter: initial_count,
|
31
|
-
digits: digits == DEFAULT_DIGITS ? nil : digits
|
32
|
-
}
|
33
|
-
encode_params("otpauth://hotp/#{Addressable::URI.escape(name)}", params)
|
28
|
+
OTP::URI.new(self, account_name: name, counter: initial_count).to_s
|
34
29
|
end
|
35
30
|
end
|
36
31
|
end
|
data/lib/rotp/otp/uri.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
module ROTP
|
2
|
+
class OTP
|
3
|
+
# https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
4
|
+
class URI
|
5
|
+
def initialize(otp, account_name:, counter: nil)
|
6
|
+
@otp = otp
|
7
|
+
@account_name = account_name
|
8
|
+
@counter = counter
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
"otpauth://#{type}/#{label}?#{parameters}"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def algorithm
|
18
|
+
return unless %w[sha256 sha512].include?(@otp.digest)
|
19
|
+
|
20
|
+
@otp.digest.upcase
|
21
|
+
end
|
22
|
+
|
23
|
+
def counter
|
24
|
+
return if @otp.is_a?(TOTP)
|
25
|
+
fail if @counter.nil?
|
26
|
+
|
27
|
+
@counter
|
28
|
+
end
|
29
|
+
|
30
|
+
def digits
|
31
|
+
return if @otp.digits == DEFAULT_DIGITS
|
32
|
+
|
33
|
+
@otp.digits
|
34
|
+
end
|
35
|
+
|
36
|
+
def issuer
|
37
|
+
return if @otp.is_a?(HOTP)
|
38
|
+
|
39
|
+
@otp.issuer&.strip&.tr(':', '_')
|
40
|
+
end
|
41
|
+
|
42
|
+
def label
|
43
|
+
[issuer, @account_name.rstrip]
|
44
|
+
.compact
|
45
|
+
.map { |s| s.tr(':', '_') }
|
46
|
+
.map { |s| ERB::Util.url_encode(s) }
|
47
|
+
.join(':')
|
48
|
+
end
|
49
|
+
|
50
|
+
def parameters
|
51
|
+
{
|
52
|
+
secret: @otp.secret,
|
53
|
+
issuer: issuer,
|
54
|
+
algorithm: algorithm,
|
55
|
+
digits: digits,
|
56
|
+
period: period,
|
57
|
+
counter: counter,
|
58
|
+
}
|
59
|
+
.reject { |_, v| v.nil? }
|
60
|
+
.map { |k, v| "#{k}=#{ERB::Util.url_encode(v)}" }
|
61
|
+
.join('&')
|
62
|
+
end
|
63
|
+
|
64
|
+
def period
|
65
|
+
return if @otp.is_a?(HOTP)
|
66
|
+
return if @otp.interval == DEFAULT_INTERVAL
|
67
|
+
|
68
|
+
@otp.interval
|
69
|
+
end
|
70
|
+
|
71
|
+
def type
|
72
|
+
case @otp
|
73
|
+
when TOTP then 'totp'
|
74
|
+
when HOTP then 'hotp'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/rotp/otp.rb
CHANGED
@@ -5,10 +5,10 @@ module ROTP
|
|
5
5
|
|
6
6
|
# @param [String] secret in the form of base32
|
7
7
|
# @option options digits [Integer] (6)
|
8
|
-
# Number of integers in the OTP
|
8
|
+
# Number of integers in the OTP.
|
9
9
|
# Google Authenticate only supports 6 currently
|
10
10
|
# @option options digest [String] (sha1)
|
11
|
-
# Digest used in the HMAC
|
11
|
+
# Digest used in the HMAC.
|
12
12
|
# Google Authenticate only supports 'sha1' currently
|
13
13
|
# @returns [OTP] OTP instantiation
|
14
14
|
def initialize(s, options = {})
|
@@ -66,16 +66,6 @@ module ROTP
|
|
66
66
|
result.reverse.join.rjust(padding, 0.chr)
|
67
67
|
end
|
68
68
|
|
69
|
-
# A very simple param encoder
|
70
|
-
def encode_params(uri, params)
|
71
|
-
params_str = String.new('?')
|
72
|
-
params.each do |k, v|
|
73
|
-
params_str << "#{k}=#{CGI.escape(v.to_s)}&" if v
|
74
|
-
end
|
75
|
-
params_str.chop!
|
76
|
-
uri + params_str
|
77
|
-
end
|
78
|
-
|
79
69
|
# constant-time compare the strings
|
80
70
|
def time_constant_compare(a, b)
|
81
71
|
return false if a.empty? || b.empty? || a.bytesize != b.bytesize
|
data/lib/rotp/totp.rb
CHANGED
@@ -54,19 +54,7 @@ module ROTP
|
|
54
54
|
# @param [String] name of the account
|
55
55
|
# @return [String] provisioning URI
|
56
56
|
def provisioning_uri(name)
|
57
|
-
|
58
|
-
# https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
59
|
-
# For compatibility the issuer appears both before that account name and also in the
|
60
|
-
# query string.
|
61
|
-
issuer_string = issuer.nil? ? '' : "#{Addressable::URI.escape(issuer)}:"
|
62
|
-
params = {
|
63
|
-
secret: secret,
|
64
|
-
period: interval == 30 ? nil : interval,
|
65
|
-
issuer: issuer,
|
66
|
-
digits: digits == DEFAULT_DIGITS ? nil : digits,
|
67
|
-
algorithm: digest.casecmp('SHA1').zero? ? nil : digest.upcase
|
68
|
-
}
|
69
|
-
encode_params("otpauth://totp/#{issuer_string}#{Addressable::URI.escape(name)}", params)
|
57
|
+
OTP::URI.new(self, account_name: name).to_s
|
70
58
|
end
|
71
59
|
|
72
60
|
private
|
data/lib/rotp/version.rb
CHANGED
data/lib/rotp.rb
CHANGED
data/rotp.gemspec
CHANGED
@@ -4,23 +4,20 @@ Gem::Specification.new do |s|
|
|
4
4
|
s.name = 'rotp'
|
5
5
|
s.version = ROTP::VERSION
|
6
6
|
s.platform = Gem::Platform::RUBY
|
7
|
+
s.required_ruby_version = '>= 2.3'
|
7
8
|
s.license = 'MIT'
|
8
9
|
s.authors = ['Mark Percival']
|
9
10
|
s.email = ['mark@markpercival.us']
|
10
|
-
s.homepage = '
|
11
|
+
s.homepage = 'https://github.com/mdp/rotp'
|
11
12
|
s.summary = 'A Ruby library for generating and verifying one time passwords'
|
12
13
|
s.description = 'Works for both HOTP and TOTP, and includes QR Code provisioning'
|
13
14
|
|
14
|
-
s.rubyforge_project = 'rotp'
|
15
|
-
|
16
15
|
s.files = `git ls-files`.split("\n")
|
17
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
19
18
|
s.require_paths = ['lib']
|
20
19
|
|
21
|
-
s.
|
22
|
-
|
23
|
-
s.add_development_dependency 'rake', '~> 10.5'
|
20
|
+
s.add_development_dependency "rake", "~> 13.0"
|
24
21
|
s.add_development_dependency 'rspec', '~> 3.5'
|
25
22
|
s.add_development_dependency 'simplecov', '~> 0.12'
|
26
23
|
s.add_development_dependency 'timecop', '~> 0.8'
|
@@ -1,24 +1,33 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ROTP::Base32 do
|
4
|
-
describe '.
|
4
|
+
describe '.random' do
|
5
5
|
context 'without arguments' do
|
6
|
-
let(:base32) { ROTP::Base32.
|
6
|
+
let(:base32) { ROTP::Base32.random }
|
7
7
|
|
8
|
-
it 'is 32
|
8
|
+
it 'is 20 bytes (160 bits) long (resulting in a 32 character base32 code)' do
|
9
|
+
expect(ROTP::Base32.decode(base32).length).to eq 20
|
9
10
|
expect(base32.length).to eq 32
|
10
11
|
end
|
11
12
|
|
12
13
|
it 'is base32 charset' do
|
13
|
-
expect(base32).to match(/\A[
|
14
|
+
expect(base32).to match(/\A[A-Z2-7]+\z/)
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
18
|
context 'with arguments' do
|
18
|
-
let(:base32) { ROTP::Base32.
|
19
|
+
let(:base32) { ROTP::Base32.random 48 }
|
19
20
|
|
20
|
-
it '
|
21
|
-
expect(base32.length).to eq
|
21
|
+
it 'returns the appropriate byte length code' do
|
22
|
+
expect(ROTP::Base32.decode(base32).length).to eq 48
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'alias to older random_base32' do
|
27
|
+
let(:base32) { ROTP::Base32.random_base32(36) }
|
28
|
+
it 'is base32 charset' do
|
29
|
+
expect(base32.length).to eq 36
|
30
|
+
expect(ROTP::Base32.decode(base32).length).to eq 22
|
22
31
|
end
|
23
32
|
end
|
24
33
|
end
|
@@ -33,22 +42,40 @@ RSpec.describe ROTP::Base32 do
|
|
33
42
|
|
34
43
|
context 'valid input data' do
|
35
44
|
it 'correctly decodes a string' do
|
36
|
-
expect(ROTP::Base32.decode('
|
37
|
-
expect(ROTP::Base32.decode('
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
expect(ROTP::Base32.decode('
|
44
|
-
|
45
|
+
expect(ROTP::Base32.decode('2EB7C66WC5TSO').unpack('H*').first).to eq 'd103f17bd6176727'
|
46
|
+
expect(ROTP::Base32.decode('Y6Y5ZCAC7NABCHSJ').unpack('H*').first).to eq 'c7b1dc8802fb40111e49'
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'correctly decode strings with trailing bits (not a multiple of 8)' do
|
50
|
+
# Dropbox style 26 characters (26*5==130 bits or 16.25 bytes, but chopped to 128)
|
51
|
+
# Matches the behavior of Google Authenticator, drops extra 2 empty bits
|
52
|
+
expect(ROTP::Base32.decode('YVT6Z2XF4BQJNBMTD7M6QBQCEM').unpack('H*').first).to eq 'c567eceae5e0609685931fd9e8060223'
|
53
|
+
|
54
|
+
# For completeness, test all the possibilities allowed by Google Authenticator
|
55
|
+
# Drop the incomplete empty extra 4 bits (28*5==140bits or 17.5 bytes, chopped to 136 bits)
|
56
|
+
expect(ROTP::Base32.decode('5GGZQB3WN6LD7V3L5HPDYTQUANEQ').unpack('H*').first).to eq 'e98d9807766f963fd76be9de3c4e140349'
|
57
|
+
|
45
58
|
end
|
46
59
|
|
47
60
|
context 'with padding' do
|
48
61
|
it 'correctly decodes a string' do
|
49
|
-
expect(ROTP::Base32.decode('
|
62
|
+
expect(ROTP::Base32.decode('234A===').unpack('H*').first).to eq 'd6f8'
|
50
63
|
end
|
51
64
|
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '.encode' do
|
70
|
+
context 'encode input data' do
|
71
|
+
it 'correctly encodes data' do
|
72
|
+
expect(ROTP::Base32.encode(hex_to_bin('3c204da94294ff82103ee34e96f74b48'))).to eq 'HQQE3KKCST7YEEB64NHJN52LJA'
|
73
|
+
end
|
52
74
|
end
|
53
75
|
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
def hex_to_bin(s)
|
80
|
+
s.scan(/../).map { |x| x.hex }.pack('c*')
|
54
81
|
end
|
data/spec/lib/rotp/cli_spec.rb
CHANGED
@@ -18,6 +18,14 @@ RSpec.describe ROTP::CLI do
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
context 'generating a TOTP with sha256 digest' do
|
22
|
+
let(:argv) { %w[--secret JBSWY3DPEHPK3PXP --digest sha256] }
|
23
|
+
|
24
|
+
it 'prints the corresponding token' do
|
25
|
+
expect(output).to eq '544902'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
21
29
|
context 'generating a TOTP with no secret' do
|
22
30
|
let(:argv) { %w[--time --secret] }
|
23
31
|
|
@@ -49,4 +57,12 @@ RSpec.describe ROTP::CLI do
|
|
49
57
|
expect(output).to eq '161024'
|
50
58
|
end
|
51
59
|
end
|
60
|
+
|
61
|
+
context 'generating a HOTP' do
|
62
|
+
let(:argv) { %W[--hmac --secret #{'a' * 32} --counter 1234 --digest sha256] }
|
63
|
+
|
64
|
+
it 'prints the corresponding token' do
|
65
|
+
expect(output).to eq '325941'
|
66
|
+
end
|
67
|
+
end
|
52
68
|
end
|
data/spec/lib/rotp/hotp_spec.rb
CHANGED
@@ -108,29 +108,14 @@ RSpec.describe ROTP::HOTP do
|
|
108
108
|
end
|
109
109
|
|
110
110
|
describe '#provisioning_uri' do
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
it 'has the correct format' do
|
115
|
-
expect(uri).to match %r{\Aotpauth:\/\/hotp.+}
|
116
|
-
end
|
117
|
-
|
118
|
-
it 'includes the secret as parameter' do
|
119
|
-
expect(params['secret'].first).to eq 'a' * 32
|
120
|
-
end
|
121
|
-
|
122
|
-
context 'with default digits' do
|
123
|
-
it 'does not include digits parameter with default digits' do
|
124
|
-
expect(params['digits'].first).to be_nil
|
125
|
-
end
|
111
|
+
it 'accepts the account name' do
|
112
|
+
expect(hotp.provisioning_uri('mark@percival'))
|
113
|
+
.to eq 'otpauth://hotp/mark%40percival?secret=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&counter=0'
|
126
114
|
end
|
127
115
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
it 'includes digits parameter' do
|
132
|
-
expect(params['digits'].first).to eq '8'
|
133
|
-
end
|
116
|
+
it 'also accepts a custom counter value' do
|
117
|
+
expect(hotp.provisioning_uri('mark@percival', 17))
|
118
|
+
.to eq 'otpauth://hotp/mark%40percival?secret=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&counter=17'
|
134
119
|
end
|
135
120
|
end
|
136
121
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ROTP::OTP::URI do
|
4
|
+
it 'meets basic functionality' do
|
5
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
|
6
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
7
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'includes issuer' do
|
11
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Example')
|
12
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
13
|
+
expect(uri.to_s).to eq 'otpauth://totp/Example:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example'
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'encodes the account name' do
|
17
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Provider1')
|
18
|
+
uri = described_class.new(otp, account_name: 'Alice Smith')
|
19
|
+
expect(uri.to_s).to eq 'otpauth://totp/Provider1:Alice%20Smith?secret=JBSWY3DPEHPK3PXP&issuer=Provider1'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'encodes the issuer' do
|
23
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Big Corporation')
|
24
|
+
uri = described_class.new(otp, account_name: ' alice@bigco.com')
|
25
|
+
expect(uri.to_s).to eq 'otpauth://totp/Big%20Corporation:%20alice%40bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big%20Corporation'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'includes non-default SHA256 algorithm' do
|
29
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digest: 'sha256')
|
30
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
31
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256'
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'includes non-default SHA512 algorithm' do
|
35
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digest: 'sha512')
|
36
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
37
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'includes non-default 8 digits' do
|
41
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', digits: 8)
|
42
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
43
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&digits=8'
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'includes non-default period for TOTP' do
|
47
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', interval: 35)
|
48
|
+
uri = described_class.new(otp, account_name: 'alice@google.com')
|
49
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&period=35'
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'includes non-default counter for HOTP' do
|
53
|
+
otp = ROTP::HOTP.new('JBSWY3DPEHPK3PXP')
|
54
|
+
uri = described_class.new(otp, account_name: 'alice@google.com', counter: 17)
|
55
|
+
expect(uri.to_s).to eq 'otpauth://hotp/alice%40google.com?secret=JBSWY3DPEHPK3PXP&counter=17'
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'can include all parameters' do
|
59
|
+
otp = ROTP::TOTP.new(
|
60
|
+
'HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ',
|
61
|
+
digest: 'sha512',
|
62
|
+
digits: 8,
|
63
|
+
interval: 60,
|
64
|
+
issuer: 'ACME Co',
|
65
|
+
)
|
66
|
+
uri = described_class.new(otp, account_name: 'john.doe@email.com')
|
67
|
+
expect(uri.to_s).to eq'otpauth://totp/ACME%20Co:john.doe%40email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA512&digits=8&period=60'
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'strips leading and trailing whitespace from the issuer' do
|
71
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: ' Big Corporation ')
|
72
|
+
uri = described_class.new(otp, account_name: ' alice@bigco.com')
|
73
|
+
expect(uri.to_s).to eq 'otpauth://totp/Big%20Corporation:%20alice%40bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big%20Corporation'
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'strips trailing whitespace from the account name' do
|
77
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
|
78
|
+
uri = described_class.new(otp, account_name: ' alice@google.com ')
|
79
|
+
expect(uri.to_s).to eq 'otpauth://totp/%20%20alice%40google.com?secret=JBSWY3DPEHPK3PXP'
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'replaces colons in the issuer with underscores' do
|
83
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP', issuer: 'Big:Corporation')
|
84
|
+
uri = described_class.new(otp, account_name: 'alice@bigco.com')
|
85
|
+
expect(uri.to_s).to eq 'otpauth://totp/Big_Corporation:alice%40bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big_Corporation'
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'replaces colons in the account name with underscores' do
|
89
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
|
90
|
+
uri = described_class.new(otp, account_name: 'Alice:Smith')
|
91
|
+
expect(uri.to_s).to eq 'otpauth://totp/Alice_Smith?secret=JBSWY3DPEHPK3PXP'
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'handles email account names with sub-addressing' do
|
95
|
+
otp = ROTP::TOTP.new('JBSWY3DPEHPK3PXP')
|
96
|
+
uri = described_class.new(otp, account_name: 'alice+1234@google.com')
|
97
|
+
expect(uri.to_s).to eq 'otpauth://totp/alice%2B1234%40google.com?secret=JBSWY3DPEHPK3PXP'
|
98
|
+
end
|
99
|
+
end
|
data/spec/lib/rotp/totp_spec.rb
CHANGED
@@ -221,79 +221,9 @@ RSpec.describe ROTP::TOTP do
|
|
221
221
|
end
|
222
222
|
|
223
223
|
describe '#provisioning_uri' do
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
context 'without issuer' do
|
228
|
-
it 'has the correct format' do
|
229
|
-
expect(uri).to match %r{\Aotpauth:\/\/totp.+}
|
230
|
-
end
|
231
|
-
|
232
|
-
it 'includes the secret as parameter' do
|
233
|
-
expect(params['secret'].first).to eq 'JBSWY3DPEHPK3PXP'
|
234
|
-
end
|
235
|
-
end
|
236
|
-
|
237
|
-
context 'with default digits' do
|
238
|
-
it 'does does not include digits parameter' do
|
239
|
-
expect(params['digits'].first).to be_nil
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
|
-
context 'with non-default digits' do
|
244
|
-
let(:totp) { ROTP::TOTP.new 'JBSWY3DPEHPK3PXP', digits: 8 }
|
245
|
-
|
246
|
-
it 'does does not include digits parameter' do
|
247
|
-
expect(params['digits'].first).to eq '8'
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
context 'with issuer' do
|
252
|
-
let(:totp) { ROTP::TOTP.new 'JBSWY3DPEHPK3PXP', issuer: 'FooCo' }
|
253
|
-
|
254
|
-
it 'has the correct format' do
|
255
|
-
expect(uri).to match %r{\Aotpauth:\/\/totp/FooCo:.+}
|
256
|
-
end
|
257
|
-
|
258
|
-
it 'includes the secret as parameter' do
|
259
|
-
expect(params['secret'].first).to eq 'JBSWY3DPEHPK3PXP'
|
260
|
-
end
|
261
|
-
|
262
|
-
it 'includes the issuer as parameter' do
|
263
|
-
expect(params['issuer'].first).to eq 'FooCo'
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
context 'with custom interval' do
|
268
|
-
let(:totp) { ROTP::TOTP.new 'JBSWY3DPEHPK3PXP', interval: 60 }
|
269
|
-
|
270
|
-
it 'has the correct format' do
|
271
|
-
expect(uri).to match %r{\Aotpauth:\/\/totp.+}
|
272
|
-
end
|
273
|
-
|
274
|
-
it 'includes the secret as parameter' do
|
275
|
-
expect(params['secret'].first).to eq 'JBSWY3DPEHPK3PXP'
|
276
|
-
end
|
277
|
-
|
278
|
-
it 'includes the interval as period parameter' do
|
279
|
-
expect(params['period'].first).to eq '60'
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
context 'with custom digest' do
|
284
|
-
let(:totp) { ROTP::TOTP.new 'JBSWY3DPEHPK3PXP', digest: 'sha256' }
|
285
|
-
|
286
|
-
it 'has the correct format' do
|
287
|
-
expect(uri).to match %r{\Aotpauth:\/\/totp.+}
|
288
|
-
end
|
289
|
-
|
290
|
-
it 'includes the secret as parameter' do
|
291
|
-
expect(params['secret'].first).to eq 'JBSWY3DPEHPK3PXP'
|
292
|
-
end
|
293
|
-
|
294
|
-
it 'includes the digest as algorithm parameter' do
|
295
|
-
expect(params['algorithm'].first).to eq 'SHA256'
|
296
|
-
end
|
224
|
+
it 'accepts the account name' do
|
225
|
+
expect(totp.provisioning_uri('mark@percival'))
|
226
|
+
.to eq 'otpauth://totp/mark%40percival?secret=JBSWY3DPEHPK3PXP'
|
297
227
|
end
|
298
228
|
end
|
299
229
|
|
metadata
CHANGED
@@ -1,43 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rotp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 6.2.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:
|
11
|
+
date: 2020-09-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: addressable
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '2.5'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '2.5'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: rake
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
30
16
|
requirements:
|
31
17
|
- - "~>"
|
32
18
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
19
|
+
version: '13.0'
|
34
20
|
type: :development
|
35
21
|
prerelease: false
|
36
22
|
version_requirements: !ruby/object:Gem::Requirement
|
37
23
|
requirements:
|
38
24
|
- - "~>"
|
39
25
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
26
|
+
version: '13.0'
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
28
|
name: rspec
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -92,14 +78,15 @@ files:
|
|
92
78
|
- ".gitignore"
|
93
79
|
- ".travis.yml"
|
94
80
|
- CHANGELOG.md
|
95
|
-
- Dockerfile-1.9
|
96
|
-
- Dockerfile-2.1
|
97
81
|
- Dockerfile-2.3
|
82
|
+
- Dockerfile-2.5
|
83
|
+
- Dockerfile-2.6
|
84
|
+
- Dockerfile-2.7
|
85
|
+
- Dockerfile-3.0-rc
|
98
86
|
- Gemfile
|
99
87
|
- Guardfile
|
100
88
|
- LICENSE
|
101
89
|
- README.md
|
102
|
-
- Rakefile
|
103
90
|
- bin/rotp
|
104
91
|
- doc/ROTP/HOTP.html
|
105
92
|
- doc/ROTP/OTP.html
|
@@ -119,12 +106,14 @@ files:
|
|
119
106
|
- doc/js/jquery.js
|
120
107
|
- doc/method_list.html
|
121
108
|
- doc/top-level-namespace.html
|
109
|
+
- docker-compose.yml
|
122
110
|
- lib/rotp.rb
|
123
111
|
- lib/rotp/arguments.rb
|
124
112
|
- lib/rotp/base32.rb
|
125
113
|
- lib/rotp/cli.rb
|
126
114
|
- lib/rotp/hotp.rb
|
127
115
|
- lib/rotp/otp.rb
|
116
|
+
- lib/rotp/otp/uri.rb
|
128
117
|
- lib/rotp/totp.rb
|
129
118
|
- lib/rotp/version.rb
|
130
119
|
- rotp.gemspec
|
@@ -132,9 +121,10 @@ files:
|
|
132
121
|
- spec/lib/rotp/base32_spec.rb
|
133
122
|
- spec/lib/rotp/cli_spec.rb
|
134
123
|
- spec/lib/rotp/hotp_spec.rb
|
124
|
+
- spec/lib/rotp/otp/uri_spec.rb
|
135
125
|
- spec/lib/rotp/totp_spec.rb
|
136
126
|
- spec/spec_helper.rb
|
137
|
-
homepage:
|
127
|
+
homepage: https://github.com/mdp/rotp
|
138
128
|
licenses:
|
139
129
|
- MIT
|
140
130
|
metadata: {}
|
@@ -146,22 +136,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
146
136
|
requirements:
|
147
137
|
- - ">="
|
148
138
|
- !ruby/object:Gem::Version
|
149
|
-
version: '
|
139
|
+
version: '2.3'
|
150
140
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
141
|
requirements:
|
152
142
|
- - ">="
|
153
143
|
- !ruby/object:Gem::Version
|
154
144
|
version: '0'
|
155
145
|
requirements: []
|
156
|
-
|
157
|
-
rubygems_version: 2.7.6
|
146
|
+
rubygems_version: 3.1.2
|
158
147
|
signing_key:
|
159
148
|
specification_version: 4
|
160
149
|
summary: A Ruby library for generating and verifying one time passwords
|
161
|
-
test_files:
|
162
|
-
- spec/lib/rotp/arguments_spec.rb
|
163
|
-
- spec/lib/rotp/base32_spec.rb
|
164
|
-
- spec/lib/rotp/cli_spec.rb
|
165
|
-
- spec/lib/rotp/hotp_spec.rb
|
166
|
-
- spec/lib/rotp/totp_spec.rb
|
167
|
-
- spec/spec_helper.rb
|
150
|
+
test_files: []
|
data/Dockerfile-1.9
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
FROM ruby:1.9
|
2
|
-
|
3
|
-
# throw errors if Gemfile has been modified since Gemfile.lock
|
4
|
-
RUN bundle config --global frozen 1
|
5
|
-
|
6
|
-
RUN mkdir -p /usr/src/app
|
7
|
-
WORKDIR /usr/src/app
|
8
|
-
|
9
|
-
COPY Gemfile /usr/src/app/
|
10
|
-
COPY Gemfile.lock /usr/src/app/
|
11
|
-
COPY . /usr/src/app
|
12
|
-
RUN bundle install
|
13
|
-
|
14
|
-
CMD ["bundler", "exec", "rspec"]
|
15
|
-
|
data/Dockerfile-2.1
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
FROM ruby:2.1
|
2
|
-
|
3
|
-
# throw errors if Gemfile has been modified since Gemfile.lock
|
4
|
-
RUN bundle config --global frozen 1
|
5
|
-
|
6
|
-
RUN mkdir -p /usr/src/app
|
7
|
-
WORKDIR /usr/src/app
|
8
|
-
|
9
|
-
COPY Gemfile /usr/src/app/
|
10
|
-
COPY Gemfile.lock /usr/src/app/
|
11
|
-
COPY . /usr/src/app
|
12
|
-
RUN bundle install
|
13
|
-
|
14
|
-
|
15
|
-
CMD ["bundler", "exec", "rspec"]
|
16
|
-
|