aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNat Lasseter <nat.lasseter@york.ac.uk>2022-09-29 12:27:19 +0100
committerNat Lasseter <nat.lasseter@york.ac.uk>2022-09-29 12:27:19 +0100
commit5a27585bbb467802c579ae3a1d043e31cd8c315d (patch)
tree5c321d5ee8dd8dd8118d2afb6705cef8e34c94cb
parent16f7d3f1e3ef999c9515b93f0525d0e2db215633 (diff)
Added HOTP functionality
-rw-r--r--Readme.textile18
-rwxr-xr-xtwofa75
2 files changed, 74 insertions, 19 deletions
diff --git a/Readme.textile b/Readme.textile
index 3051c29..8997f39 100644
--- a/Readme.textile
+++ b/Readme.textile
@@ -13,7 +13,7 @@ p. For clipboard tasks, twofa requires the _clipboard_ gem, and either the _xcli
h2. Usage
bc.. $ twofa -h
-twofa is a command line TOTP code generator.
+twofa is a command line OTP code generator.
Usage:
twofa [opts] ISSUER
@@ -26,6 +26,18 @@ where [opts] are:
h3. twofa secrets file
-p. The twofa secrets file is normally found at @~./twofa@, and has the format:
+p. The twofa secrets file is normally found at @~./twofa@, and each record has the following fields. If any of the optional fields are given, all preceeding optional fields must also be given.
-bc. ISSUER SECRET [interval | default(30)] [length | default(6)] [hashing algorithm | default(sha1)]
+p. If a HOTP code is requested, the file will be canoncalised.
+
+h4. Required fields
+
+- Method := The OTP type, currently @totp@ or @hotp@
+- Issuer := Human name for this issuer
+- Secret := The BASE32 shared secret
+
+h4. Optional fields
+
+- Time interval or Counter := Either the time interval (for TOTP) or counter (for HOTP). The former defaults to 30 if not given, the latter to 0.
+- Length := The output code length. Defaults to 6 digits.
+- Hashing algorithm := The algorithm used to generate the HMAS hash. Defaults to @sha1@.
diff --git a/twofa b/twofa
index a63c37f..487b094 100755
--- a/twofa
+++ b/twofa
@@ -1,5 +1,7 @@
#!/usr/bin/env ruby
+#!DESCRIBE: CLI based TOTP 2FA authenticator
+
require "openssl"
require "base32"
require "optimist"
@@ -8,8 +10,8 @@ def db32(str)
Base32.decode(str)
end
-def hs1(key, td = 30, tx = Time.now.to_i, hsh = "sha1")
- OpenSSL::HMAC.hexdigest(hsh, key.to_s, [tx.to_i/td.to_i].pack("Q>"))
+def hs1(key, counter, hsh = "sha1")
+ OpenSSL::HMAC.hexdigest(hsh, key.to_s, [counter].pack("Q>"))
end
def dt(str)
@@ -30,43 +32,77 @@ end
class Secrets
class Secret
- def initialize(secret, td, dig, hsh)
+ def initialize(method, secret, tdc, dig, hsh)
+ @method = method
@secret = secret
- @td = td
+ @tdc = tdc
@dig = dig
@hsh = hsh
end
+ attr_reader :method
+
def verify(tx = Time.now.to_i)
- decoded = db32(@secret)
- hmac = hs1(decoded, @td, tx, @hsh)
- trunc = dt(hmac)
- code = hotp(trunc, @dig)
- "%0#{@dig}d" % code
+ case @method
+ when 'totp'
+ decoded = db32(@secret)
+ hmac = hs1(decoded, tx.to_i/@tdc.to_i, @hsh)
+ trunc = dt(hmac)
+ code = hotp(trunc, @dig)
+ "%0#{@dig}d" % code
+ when 'hotp'
+ decoded = db32(@secret)
+ hmac = hs1(decoded, @tdc, @hsh)
+ trunc = dt(hmac)
+ code = hotp(trunc, @dig)
+ @tdc += 1
+ "%0#{@dig}d" % code
+ else
+ fatal("I don't know how to #{@method}")
+ end
end
def time_remaining(tx = Time.now.to_i)
- (tx.to_i/@td.to_i + 1) * @td - tx
+ case @method
+ when 'totp'
+ (tx.to_i/@tdc.to_i + 1) * @tdc - tx
+ end
+ end
+
+ def puts
+ "#{@secret} #{@tdc} #{@dig} #{@hsh}"
end
end
def initialize(arr)
@secrets = {}
arr.each do |secretline|
- i, s, t, d, h = secretline.split
- @secrets[i] = Secret.new(s, t&.to_i || 30, d&.to_i || 6, h || "sha1")
+ m, i, s, tc, d, h = secretline.split
+ case m
+ when 'totp'
+ tc = tc&.to_i || 30
+ when 'hotp'
+ tc = tc&.to_i || 0
+ end
+ @secrets[i] = Secret.new(m, s, tc, d&.to_i || 6, h || "sha1")
end
end
def [](issuer)
@secrets[issuer]
end
+
+ def puts
+ @secrets.map do |i, s|
+ "#{s.method} #{i} #{s.puts}"
+ end
+ end
end
opts = Optimist::options do
version "twofa (c) 2019 Nat Lasseter"
banner <<-EOS
-twofa is a command line TOTP code generator.
+twofa is a command line OTP code generator.
Usage:
twofa [opts] ISSUER
@@ -90,9 +126,16 @@ fatal("Specify issuer") if ISSUER.nil?
sec = SECRETS[ISSUER]
fatal("No such issuer") if sec.nil?
-code = sec.verify
-time = sec.time_remaining
-puts "#{code} (for #{time} more seconds)"
+case sec.method
+when 'totp'
+ code = sec.verify
+ time = sec.time_remaining
+ puts "#{code} (for #{time} more seconds)"
+when 'hotp'
+ code = sec.verify
+ puts "#{code} (rolled counter)"
+ File.open(TWOFAFILE, ?w).puts SECRETS.puts
+end
unless opts[:no_clip] then
require "clipboard"