potp 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +52 -0
- data/README.md +142 -0
- data/bin/potp +84 -0
- data/lib/potp/base32.rb +140 -0
- data/lib/potp/foreign/supplement.rb +21 -0
- data/lib/potp/hotp.rb +29 -0
- data/lib/potp/otp.rb +93 -0
- data/lib/potp/random.rb +26 -0
- data/lib/potp/totp.rb +61 -0
- data/lib/potp/version.rb +11 -0
- data/lib/potp.rb +11 -0
- data/potp.gemspec +35 -0
- metadata +62 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 572944d75dff4240e25ec32bb91202acd04ce07b2dea52e585fb765bf579076d
|
4
|
+
data.tar.gz: 633ba7f0dac49a1c66d71680a030571b32185806adf6e9e9111bc5561d937865
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4e6b4d5efe9da1b692486e28a56fcf20f4c858a99bb7d870889c4cee21540fff2061589088ba01917333dcf83ec9d3293e4e9741d79d468fac039d5913b0bef2
|
7
|
+
data.tar.gz: 5ed1543a5214cb4be08e1b0b54d7ebff17833ed7442757bc7a5605356d52860967d136bf0ed2a0eb3629024797d65df313dd70ab198dd1b3397b412740744c5f
|
data/LICENSE
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# BSD-2-clause license, extended by language use conditions
|
2
|
+
|
3
|
+
Copyright (C) 2024, Bertram Scharpf <software@bertram-scharpf.de>.
|
4
|
+
All rights reserved.
|
5
|
+
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
7
|
+
modification, are permitted provided that the following conditions are
|
8
|
+
met:
|
9
|
+
|
10
|
+
* Redistributions of source code must retain the above copyright
|
11
|
+
notice, this list of conditions and the following disclaimer.
|
12
|
+
|
13
|
+
* Redistributions in binary form must reproduce the above copyright
|
14
|
+
notice, this list of conditions and the following disclaimer in
|
15
|
+
the documentation and/or other materials provided with the
|
16
|
+
distribution.
|
17
|
+
|
18
|
+
* Redistributions must not contain any clauses about anticipated
|
19
|
+
harassment or discrimination, nor must they be held in a so-called
|
20
|
+
"inclusive language". As far as German language is used, the
|
21
|
+
conditions mentioned below additionally apply.
|
22
|
+
|
23
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
24
|
+
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
25
|
+
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
26
|
+
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
27
|
+
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
28
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
29
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
30
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
31
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
32
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
33
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
34
|
+
|
35
|
+
|
36
|
+
## Use of the German Language
|
37
|
+
|
38
|
+
Beim Gebrauch deutscher Sprache sind Weiterentwicklungen und
|
39
|
+
-verbreitungen nur gestattet unter Einhaltung sowie abermaligen
|
40
|
+
Einforderns folgender zusätzlicher Bedingungen:
|
41
|
+
|
42
|
+
* Keine Verwendung von sogenannter „geschlechtergerechter Sprache“,
|
43
|
+
also Anfügen von weiblichen Endungen mit Binnen-I, Sternchen,
|
44
|
+
Doppelpunkt, Unterstrich oder ähnlichem, oder Konstruktionen, die
|
45
|
+
den Sachverhalt falsch wiedergeben („Radfahrende“, „Studierende“).
|
46
|
+
|
47
|
+
* Keine Verwendung der „reformierten Rechtschreibung“ von 1996,
|
48
|
+
insbesondere Doppel-S am Silbenende, „plazieren“ mit T, sowie
|
49
|
+
Großschreibung von Wendungen wie „des weiteren“.
|
50
|
+
|
51
|
+
|
52
|
+
<!-- vim:set ft=markdown : -->
|
data/README.md
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
# The Plain One Time Password Library
|
2
|
+
|
3
|
+
A ruby library for generating and validating one time passwords (HOTP & TOTP)
|
4
|
+
according to
|
5
|
+
[RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226)
|
6
|
+
and
|
7
|
+
[RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238).
|
8
|
+
|
9
|
+
POTP aims to be compatible with
|
10
|
+
[Google Authenticator](https://github.com/google/google-authenticator).
|
11
|
+
|
12
|
+
The Base32 format conforms to
|
13
|
+
[RFC 4648 Base32](http://en.wikipedia.org/wiki/Base32#Base_32_Encoding_per_§6)
|
14
|
+
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
```bash
|
19
|
+
sudo gem install potp
|
20
|
+
```
|
21
|
+
|
22
|
+
If you like to run the executable (instead of writing a one-liner for
|
23
|
+
yourself), you have to install the `appl` gem.
|
24
|
+
|
25
|
+
```bash
|
26
|
+
sudo gem install appl
|
27
|
+
```
|
28
|
+
|
29
|
+
|
30
|
+
## Library Usage
|
31
|
+
|
32
|
+
### Time based (TOTP)
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
require "potp"
|
36
|
+
|
37
|
+
totp = POTP::TOTP.new "GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A"
|
38
|
+
totp.now #=> "152201"
|
39
|
+
|
40
|
+
totp.verify "152201" #=> 1735417500 # ok, value is the timestamp
|
41
|
+
sleep 30
|
42
|
+
totp.verify "152201" #=> nil # not ok
|
43
|
+
```
|
44
|
+
|
45
|
+
### Counter based (HOTP)
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
hotp = POTP::HOTP.new "GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A"
|
49
|
+
hotp.at 0 #=> "178748"
|
50
|
+
hotp.at 1 #=> "584373"
|
51
|
+
hotp.at 73 #=> "309764"
|
52
|
+
|
53
|
+
# OTP verifying with a counter
|
54
|
+
hotp.verify "309764", 73 #=> 73
|
55
|
+
hotp.verify "309764", 74 #=> nil
|
56
|
+
hotp.verify "309764", 70, retries: 2 #=> nil
|
57
|
+
hotp.verify "309764", 70, retries: 3 #=> 73
|
58
|
+
```
|
59
|
+
|
60
|
+
|
61
|
+
### Avoiding reuse of TOTP
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
require "potp"
|
65
|
+
totp = POTP::TOTP.new "GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A"
|
66
|
+
code = totp.now #=> "054626"
|
67
|
+
last_verify = totp.verify code #=> 1735527390
|
68
|
+
totp.verify code, after: last_verify #=> nil
|
69
|
+
sleep 30
|
70
|
+
code = totp.now #=> "481150"
|
71
|
+
totp.verify code, after: last_verify #=> 1735527420
|
72
|
+
```
|
73
|
+
|
74
|
+
|
75
|
+
### Verifying a TOTP with drift
|
76
|
+
|
77
|
+
In case a user entered a code just after it has expired, you can allow
|
78
|
+
the token to remain valid.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
totp = POTP::TOTP.new "GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A"
|
82
|
+
now = Time.now - 30
|
83
|
+
code = totp.at now #=> "455335"
|
84
|
+
totp.verify code #=> nil
|
85
|
+
totp.verify code, drift_behind: 27 #=> 1735530510
|
86
|
+
```
|
87
|
+
|
88
|
+
|
89
|
+
### Generating a Base32 secret key
|
90
|
+
|
91
|
+
Returns a 160 bit (32 character) Base32 secret.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
require "potp/random"
|
95
|
+
POTP::Base32.random #=> "GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A"
|
96
|
+
```
|
97
|
+
|
98
|
+
|
99
|
+
### Generating QR codes for provisioning mobile apps
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
require "potp"
|
103
|
+
|
104
|
+
totp = POTP::TOTP.new "GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A"
|
105
|
+
uri = totp.provisioning_uri name: "jdoe@example.net", issuer: "ACME Service"
|
106
|
+
#=> "otpauth://totp/ACME%20Service:jdoe%40example.net?secret=GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A&issuer=ACME%20Service"
|
107
|
+
|
108
|
+
hotp = POTP::HOTP.new "GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A"
|
109
|
+
uri = hotp.provisioning_uri name: "jdoe@example.net", issuer: "ACME Service", counter: 0
|
110
|
+
#=> "otpauth://hotp/ACME%20Service:jdoe%40example.net?secret=GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A&issuer=ACME%20Service&counter=0"
|
111
|
+
|
112
|
+
# Then, do something like this:
|
113
|
+
system *%w(qrencode -t xpm -s 1 -o), "qr.xpm", uri
|
114
|
+
```
|
115
|
+
|
116
|
+
|
117
|
+
## Executable Usage
|
118
|
+
|
119
|
+
Generates a time-based one-time password:
|
120
|
+
|
121
|
+
```bash
|
122
|
+
potp --secret GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A
|
123
|
+
```
|
124
|
+
|
125
|
+
Generates a counter-based one-time password:
|
126
|
+
|
127
|
+
```bash
|
128
|
+
potp --hmac --secret GYS5L3N3E4AAYNMN562LW76TMWHQBJ4A --counter 42
|
129
|
+
```
|
130
|
+
|
131
|
+
What you expect:
|
132
|
+
|
133
|
+
```bash
|
134
|
+
potp --help
|
135
|
+
```
|
136
|
+
|
137
|
+
|
138
|
+
## Copyright
|
139
|
+
|
140
|
+
* (C) 2024 Bertram Scharpf <software@bertram-scharpf.de>
|
141
|
+
* License: [BSD-2-Clause+](./LICENSE)
|
142
|
+
|
data/bin/potp
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# potp -- The Plain One-Time Password Tool
|
5
|
+
#
|
6
|
+
|
7
|
+
begin
|
8
|
+
require "appl"
|
9
|
+
rescue LoadError
|
10
|
+
puts "This tool requires the `appl` gem."
|
11
|
+
exit 2
|
12
|
+
end
|
13
|
+
require "potp/foreign/supplement"
|
14
|
+
require "potp"
|
15
|
+
|
16
|
+
|
17
|
+
class POTP::Appl < Application
|
18
|
+
|
19
|
+
NAME = "potp"
|
20
|
+
VERSION = POTP::VERSION
|
21
|
+
SUMMARY = "Plain One Time Password Tool"
|
22
|
+
COPYRIGHT = "(C) 2024 Bertram Scharpf <software@bertram-scharpf.de>"
|
23
|
+
LICENSE = "BSD-2-Clause+"
|
24
|
+
AUTHOR = "Bertram Scharpf <software@bertram-scharpf.de>"
|
25
|
+
|
26
|
+
DESCRIPTION = <<~EOT
|
27
|
+
Generate and validate one time passwords (HOTP & TOTP)
|
28
|
+
according to [RFC 4226] and [RFC 6238].
|
29
|
+
|
30
|
+
Examples:
|
31
|
+
|
32
|
+
potp --secret p4ssword # Generates a time-based one-time password
|
33
|
+
potp --hmac --secret p4ssword --counter 42 # Generates a counter-based one-time password
|
34
|
+
|
35
|
+
EOT
|
36
|
+
|
37
|
+
attr_writer :secret, :counter, :digest
|
38
|
+
attr_bang :debug
|
39
|
+
def time! ; @mode = :time ; end
|
40
|
+
def hmac! ; @mode = :hmac ; end
|
41
|
+
|
42
|
+
define_option "t", :time!, true, "use time-based OTP according to RFC 6238"
|
43
|
+
alias_option "t", "time"
|
44
|
+
|
45
|
+
define_option "m", :hmac!, "use counter-based OTP according to RFC 4226"
|
46
|
+
alias_option "m", "hmac"
|
47
|
+
|
48
|
+
define_option "s", :secret=, "STR", "the shared secret"
|
49
|
+
alias_option "s", "secret"
|
50
|
+
|
51
|
+
define_option "d", :digest=, "ALG", "sha1", "algorithm for the digest"
|
52
|
+
alias_option "d", "digest"
|
53
|
+
|
54
|
+
define_option "c", :counter=, "NUM", 0,
|
55
|
+
"the counter for counter-based hmac OTP"
|
56
|
+
alias_option "c", "counter"
|
57
|
+
|
58
|
+
define_option "g", :debug!, "full Ruby error messages and backtrace"
|
59
|
+
alias_option "g", "debug"
|
60
|
+
define_option "h", :help, "show options"
|
61
|
+
alias_option "h", "help"
|
62
|
+
define_option "V", :version, "show version"
|
63
|
+
alias_option "V", "version"
|
64
|
+
|
65
|
+
def run
|
66
|
+
puts generate_output
|
67
|
+
rescue POTP::Base32::Invalid
|
68
|
+
raise "Secret must be in RFC4648 Base32 format - http://en.wikipedia.org/wiki/Base32#Base_32_Encoding_per_§6"
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def generate_output
|
74
|
+
@secret.notempty? or raise "You must specify a --secret. See --help."
|
75
|
+
case @mode
|
76
|
+
when :time then (POTP::TOTP.new @secret, digest: @digest).now
|
77
|
+
when :hmac then (POTP::HOTP.new @secret, digest: @digest).at @counter
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
POTP::Appl.run
|
84
|
+
|
data/lib/potp/base32.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
#
|
2
|
+
# potp/base32.rb -- Base32 Encoding
|
3
|
+
#
|
4
|
+
|
5
|
+
require "potp/foreign/supplement"
|
6
|
+
|
7
|
+
|
8
|
+
module POTP
|
9
|
+
|
10
|
+
class Base32
|
11
|
+
|
12
|
+
ORD_A, ORD_Z, ORD_a, ORD_z, ORD_2, ORD_7 = %w(A Z a z 2 7).map { |c| c.ord }
|
13
|
+
LAP = 26
|
14
|
+
DIFF_2 = ORD_2 - LAP
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
def encode bytes, width: nil, equals: true
|
19
|
+
res = [""]
|
20
|
+
scan_bufs bytes do |buf,r|
|
21
|
+
chunk = []
|
22
|
+
8.times {
|
23
|
+
chunk.unshift buf & 0x1f
|
24
|
+
buf >>= 5
|
25
|
+
}
|
26
|
+
loop do
|
27
|
+
r -= 5
|
28
|
+
break unless r > 0
|
29
|
+
chunk.pop
|
30
|
+
end
|
31
|
+
chunk = chunk.map { |c| (c + (c < LAP ? ORD_A : DIFF_2)).chr }
|
32
|
+
if equals then
|
33
|
+
chunk.push "=" until chunk.length >= 8
|
34
|
+
end
|
35
|
+
res.last << chunk.join
|
36
|
+
if width and res.last.length > width then
|
37
|
+
res.push res.last.slice! width, 8
|
38
|
+
end
|
39
|
+
end
|
40
|
+
new res.join "\n"
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def scan_bufs bytes
|
46
|
+
scan_blocks bytes do |s|
|
47
|
+
buf, r = 0, 40
|
48
|
+
5.times {
|
49
|
+
buf <<= 8
|
50
|
+
if (b = s.shift) then
|
51
|
+
buf |= b
|
52
|
+
r -= 8
|
53
|
+
end
|
54
|
+
}
|
55
|
+
yield buf, r
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def scan_blocks bytes
|
60
|
+
i = 0
|
61
|
+
loop do
|
62
|
+
s = bytes.byteslice i, 5
|
63
|
+
break unless s.notempty?
|
64
|
+
yield s.unpack "C*"
|
65
|
+
i += 5
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
class Invalid < ArgumentError ; end
|
73
|
+
|
74
|
+
attr_reader :data
|
75
|
+
|
76
|
+
def initialize data
|
77
|
+
@data = data
|
78
|
+
end
|
79
|
+
|
80
|
+
def decode encoding: nil
|
81
|
+
res = ""
|
82
|
+
each_block { |buf,n|
|
83
|
+
chunk = []
|
84
|
+
5.times {
|
85
|
+
chunk.unshift buf & 0xff
|
86
|
+
buf >>= 8
|
87
|
+
}
|
88
|
+
until n >= 40 do
|
89
|
+
chunk.pop
|
90
|
+
n += 8
|
91
|
+
end
|
92
|
+
res << (chunk.pack "C*")
|
93
|
+
}
|
94
|
+
encoding = Encoding.default_external if encoding == :default
|
95
|
+
res.force_encoding encoding if encoding
|
96
|
+
res
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def each_block
|
102
|
+
buf, n = 0, 0
|
103
|
+
each_char do |c|
|
104
|
+
c -= case c
|
105
|
+
when ORD_A..ORD_Z then ORD_A
|
106
|
+
when ORD_a..ORD_z then ORD_a
|
107
|
+
when ORD_2..ORD_7 then DIFF_2
|
108
|
+
else raise Invalid, "Character '#{c}'."
|
109
|
+
end
|
110
|
+
buf <<= 5
|
111
|
+
buf |= c
|
112
|
+
n += 5
|
113
|
+
if n == 40 then
|
114
|
+
yield buf, n
|
115
|
+
buf, n = 0, 0
|
116
|
+
end
|
117
|
+
end
|
118
|
+
if n.nonzero? then
|
119
|
+
buf <<= 40 - n
|
120
|
+
yield buf, n
|
121
|
+
end
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def each_char
|
126
|
+
done = false
|
127
|
+
@data.each_char { |c|
|
128
|
+
case c
|
129
|
+
when "=" then done = true ; next
|
130
|
+
when "\n" then next
|
131
|
+
else raise Invalid, "Material after '=': '#{c}'" if done
|
132
|
+
end
|
133
|
+
yield c.ord
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#
|
2
|
+
# potp/foreign/supplement.rb -- Addition usefull Ruby functions
|
3
|
+
#
|
4
|
+
|
5
|
+
# The purpose of this is simply to reduce dependencies.
|
6
|
+
|
7
|
+
begin
|
8
|
+
require "supplement____"
|
9
|
+
rescue LoadError
|
10
|
+
class NilClass ; def notempty? ; end ; end
|
11
|
+
class String ; def notempty? ; self unless empty? ; end ; end
|
12
|
+
class Array ; def notempty? ; self unless empty? ; end ; end
|
13
|
+
class <<Struct ; alias [] new ; end
|
14
|
+
class String
|
15
|
+
def starts_with? oth ; o = oth.to_str ; o.length if start_with? o ; end
|
16
|
+
def ends_with? oth ; o = oth.to_str ; length - o.length if end_with? o ; end
|
17
|
+
alias starts_with starts_with?
|
18
|
+
alias ends_with ends_with?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
data/lib/potp/hotp.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#
|
2
|
+
# potp/hotp.rb -- HOTP class
|
3
|
+
#
|
4
|
+
|
5
|
+
require "potp/otp"
|
6
|
+
|
7
|
+
|
8
|
+
module POTP
|
9
|
+
|
10
|
+
class HOTP < OTP
|
11
|
+
|
12
|
+
SCHEME = "hotp"
|
13
|
+
|
14
|
+
def verify input, counter, retries: 0
|
15
|
+
while retries >= 0 do
|
16
|
+
return counter if super input, counter
|
17
|
+
counter += 1
|
18
|
+
retries -= 1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def provisioning_uri counter: 0, **kwargs
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
data/lib/potp/otp.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
#
|
2
|
+
# potp/otp.rb -- OTP class
|
3
|
+
#
|
4
|
+
|
5
|
+
require "potp/foreign/supplement"
|
6
|
+
require "openssl"
|
7
|
+
require "potp/base32"
|
8
|
+
|
9
|
+
|
10
|
+
module POTP
|
11
|
+
|
12
|
+
class OTP
|
13
|
+
|
14
|
+
attr_reader :secret, :digits, :digest
|
15
|
+
|
16
|
+
DEFAULT_DIGITS = 6
|
17
|
+
|
18
|
+
def initialize secret, digits: nil, digest: nil, **kwargs
|
19
|
+
@secret = secret
|
20
|
+
@digits = digits || DEFAULT_DIGITS # Google Authenticate only supports 6 currently
|
21
|
+
@digest = digest || "sha1" # Google Authenticate only supports SHA1 currently
|
22
|
+
end
|
23
|
+
|
24
|
+
def at data
|
25
|
+
hmac = build_digest data
|
26
|
+
code = hmac[ (hmac.last & 0x0f), 4].inject do |c,e| c <<= 8 ; c |= e end
|
27
|
+
code &= 0x7fffffff
|
28
|
+
s = ""
|
29
|
+
@digits.times { code, d = code.divmod 10 ; s << d.to_s }
|
30
|
+
s.reverse!
|
31
|
+
s
|
32
|
+
end
|
33
|
+
|
34
|
+
def verify input, data
|
35
|
+
String === input or raise ArgumentError, "`otp` has to be a String"
|
36
|
+
time_constant_compare input, (at data)
|
37
|
+
end
|
38
|
+
|
39
|
+
# https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
40
|
+
# Example additional parameter: image: "https://example.com/icon.png"
|
41
|
+
def provisioning_uri name:, issuer: nil, **kwargs
|
42
|
+
label = [ issuer, name||""].map { |x| x&.tr ":", "_" }
|
43
|
+
parameters = {
|
44
|
+
**kwargs,
|
45
|
+
digits: (@digits unless @digits == DEFAULT_DIGITS),
|
46
|
+
algorithm: (@digest.upcase unless @digest.downcase == "sha1"),
|
47
|
+
issuer: issuer,
|
48
|
+
secret: @secret,
|
49
|
+
}
|
50
|
+
build_uri label, parameters
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def int_to_bytestring i, padding = 8
|
56
|
+
i >= 0 or raise ArgumentError, "#int_to_bytestring requires a positive number"
|
57
|
+
result = []
|
58
|
+
while i != 0 or padding > 0 do
|
59
|
+
c = (i & 0xff).chr
|
60
|
+
result.unshift c
|
61
|
+
i >>= 8
|
62
|
+
padding -= 1
|
63
|
+
end
|
64
|
+
result.join
|
65
|
+
end
|
66
|
+
|
67
|
+
def time_constant_compare a, b
|
68
|
+
a.notempty? and b.notempty? and a == b
|
69
|
+
end
|
70
|
+
|
71
|
+
def build_digest input
|
72
|
+
d = OpenSSL::Digest.new @digest
|
73
|
+
s = (Base32.new @secret).decode
|
74
|
+
i = int_to_bytestring input.to_i
|
75
|
+
(OpenSSL::HMAC.digest d, s, i).bytes
|
76
|
+
end
|
77
|
+
|
78
|
+
def build_uri label, parameters
|
79
|
+
label.compact!
|
80
|
+
label = label.map! { |s| url_encode s }.join ":"
|
81
|
+
parameters.reject! { |_,v| v.nil? }
|
82
|
+
parameters = parameters.keys.reverse.map { |k| "#{k}=#{url_encode parameters[ k]}" }.join "&"
|
83
|
+
"otpauth://#{self.class::SCHEME}/#{label}?#{parameters}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def url_encode str
|
87
|
+
str.to_s.gsub %r/([^a-zA-Z0-9_.-])/ do |c| "%%%02X" % c.ord end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
data/lib/potp/random.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#
|
2
|
+
# potp/random.rb -- Generate random secrets
|
3
|
+
#
|
4
|
+
|
5
|
+
require "potp/base32"
|
6
|
+
require "securerandom"
|
7
|
+
|
8
|
+
|
9
|
+
module POTP
|
10
|
+
|
11
|
+
class Base32
|
12
|
+
|
13
|
+
class <<self
|
14
|
+
|
15
|
+
# A byte length of 20 means 160 bits and results in 32 character long base32 value.
|
16
|
+
def random byte_length = 20
|
17
|
+
rand_bytes = SecureRandom.random_bytes byte_length
|
18
|
+
(encode rand_bytes).data
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
data/lib/potp/totp.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#
|
2
|
+
# potp/totp.rb -- TOTP class
|
3
|
+
#
|
4
|
+
|
5
|
+
require "potp/otp"
|
6
|
+
|
7
|
+
|
8
|
+
module POTP
|
9
|
+
|
10
|
+
class TOTP < OTP
|
11
|
+
|
12
|
+
SCHEME = "totp"
|
13
|
+
|
14
|
+
DEFAULT_INTERVAL = 30
|
15
|
+
|
16
|
+
attr_reader :interval
|
17
|
+
|
18
|
+
# @option options [Integer] interval (30) the time interval in seconds for OTP
|
19
|
+
# This defaults to 30 which is standard.
|
20
|
+
def initialize secret, interval: nil, **kwargs
|
21
|
+
@interval = interval || DEFAULT_INTERVAL
|
22
|
+
super secret, **kwargs
|
23
|
+
end
|
24
|
+
|
25
|
+
def at time ; super (timeint time) / @interval ; end
|
26
|
+
def now ; at Time.now ; end
|
27
|
+
|
28
|
+
def verify input, drift_ahead: nil, drift_behind: nil, after: nil, at: nil
|
29
|
+
fin = now = timeint at||Time.now
|
30
|
+
now -= drift_behind if drift_behind
|
31
|
+
fin += drift_ahead if drift_ahead
|
32
|
+
if after then
|
33
|
+
after += @interval
|
34
|
+
now = after if now < after
|
35
|
+
end
|
36
|
+
now -= now % @interval
|
37
|
+
while now < fin do
|
38
|
+
return now if super input, now
|
39
|
+
now += @interval
|
40
|
+
end
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def provisioning_uri **kwargs
|
45
|
+
super **kwargs, period: (@interval unless @interval == TOTP::DEFAULT_INTERVAL)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def timeint time
|
51
|
+
case time
|
52
|
+
when Integer then time
|
53
|
+
when Time then time.utc.to_i
|
54
|
+
else time.to_i
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
data/lib/potp/version.rb
ADDED
data/lib/potp.rb
ADDED
data/potp.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#
|
2
|
+
# potp.gemspec -- Gem Specification
|
3
|
+
#
|
4
|
+
|
5
|
+
require "./lib/potp/version"
|
6
|
+
|
7
|
+
|
8
|
+
Gem::Specification.new do |s|
|
9
|
+
s.name = "potp"
|
10
|
+
s.version = POTP::VERSION
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.required_ruby_version = ">= 3.1"
|
13
|
+
s.summary = "Plain One Time Password Tool"
|
14
|
+
s.description = <<~EOT
|
15
|
+
A Ruby library for generating and verifying one time passwords,
|
16
|
+
both HOTP and TOTP, and includes QR Code provisioning.
|
17
|
+
EOT
|
18
|
+
s.license = "LicenseRef-LICENSE"
|
19
|
+
s.authors = ["Bertram Scharpf"]
|
20
|
+
s.email = ["<software@bertram-scharpf.de>"]
|
21
|
+
s.homepage = "https://github.com/BertramScharpf/ruby-potp"
|
22
|
+
|
23
|
+
s.requirements = "Just Ruby and some more if you like"
|
24
|
+
unless :full_dependecies then
|
25
|
+
s.add_dependency "supplement", "~>2", ">=2.10"
|
26
|
+
s.add_dependency "appl", "~>1"
|
27
|
+
end
|
28
|
+
|
29
|
+
s.require_paths = %w(lib)
|
30
|
+
s.extensions = %w()
|
31
|
+
s.files = Dir[ "lib/**/*.rb", "bin/*", ]
|
32
|
+
s.executables = %w(potp)
|
33
|
+
s.extra_rdoc_files = %w(LICENSE README.md potp.gemspec)
|
34
|
+
end
|
35
|
+
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: potp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bertram Scharpf
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-31 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |
|
14
|
+
A Ruby library for generating and verifying one time passwords,
|
15
|
+
both HOTP and TOTP, and includes QR Code provisioning.
|
16
|
+
email:
|
17
|
+
- "<software@bertram-scharpf.de>"
|
18
|
+
executables:
|
19
|
+
- potp
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files:
|
22
|
+
- LICENSE
|
23
|
+
- README.md
|
24
|
+
- potp.gemspec
|
25
|
+
files:
|
26
|
+
- LICENSE
|
27
|
+
- README.md
|
28
|
+
- bin/potp
|
29
|
+
- lib/potp.rb
|
30
|
+
- lib/potp/base32.rb
|
31
|
+
- lib/potp/foreign/supplement.rb
|
32
|
+
- lib/potp/hotp.rb
|
33
|
+
- lib/potp/otp.rb
|
34
|
+
- lib/potp/random.rb
|
35
|
+
- lib/potp/totp.rb
|
36
|
+
- lib/potp/version.rb
|
37
|
+
- potp.gemspec
|
38
|
+
homepage: https://github.com/BertramScharpf/ruby-potp
|
39
|
+
licenses:
|
40
|
+
- LicenseRef-LICENSE
|
41
|
+
metadata: {}
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '3.1'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements:
|
57
|
+
- Just Ruby and some more if you like
|
58
|
+
rubygems_version: 3.5.23
|
59
|
+
signing_key:
|
60
|
+
specification_version: 4
|
61
|
+
summary: Plain One Time Password Tool
|
62
|
+
test_files: []
|