rotp 4.0.2 → 6.2.0
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/.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
|
[](https://travis-ci.org/mdp/rotp)
|
4
4
|
[](https://rubygems.org/gems/rotp)
|
5
|
+
[](https://www.rubydoc.info/github/mdp/rotp/master)
|
5
6
|
[](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
|
-
|