aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNat Lasseter <user@4574.co.uk>2019-12-17 09:44:22 +0000
committerNat Lasseter <user@4574.co.uk>2019-12-17 09:44:22 +0000
commit9529d2a2807f1c7a2c100d3de39e6e9e9c6b3b0d (patch)
treeebad36bd4c2b536a5a8a7ac63b297769a5755d4a
Initial commit
-rw-r--r--LICENSE25
-rw-r--r--Readme.textile13
-rwxr-xr-xtwofa76
3 files changed, 114 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..70c63f6
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+BSD 2-Clause License
+
+Copyright (c) 2019, Nat Lasseter
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Readme.textile b/Readme.textile
new file mode 100644
index 0000000..123c0d6
--- /dev/null
+++ b/Readme.textile
@@ -0,0 +1,13 @@
+h1. twofa
+
+p. A terrible, should-not-be-used, wrote-it-for-fun-and-learning 2fa authenticator.
+
+h2. Usage
+
+bc. $ twofa ISSUER
+
+h3. @~/.twofa@
+
+p. The twofa rc file has the format:
+
+bc. ISSUER SECRET [interval | default(30)] [length | default(6)] [hashing algorithm | default(sha1)]
diff --git a/twofa b/twofa
new file mode 100755
index 0000000..01c6e0c
--- /dev/null
+++ b/twofa
@@ -0,0 +1,76 @@
+#!/usr/bin/env ruby
+
+require "openssl"
+require "base32"
+
+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>"))
+end
+
+def dt(str)
+ offset = str[-1].to_i(16)
+ p = str[(offset*2)...((offset+4)*2)]
+ p[0] = (p[0].to_i(16) & 0x7).to_s(16)
+ p.to_i(16)
+end
+
+def hotp(num, dig = 6)
+ num % (10 ** dig)
+end
+
+def fatal(msg)
+ $stderr.puts(msg)
+ exit 1
+end
+
+class Secrets
+ class Secret
+ def initialize(secret, td, dig, hsh)
+ @secret = secret
+ @td = td
+ @dig = dig
+ @hsh = hsh
+ end
+
+ 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
+ end
+
+ def time_remaining(tx = Time.now.to_i)
+ (tx.to_i/@td.to_i + 1) * @td - tx
+ 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")
+ end
+ end
+
+ def [](issuer)
+ @secrets[issuer]
+ end
+end
+
+TWOFAFILE = File.join(ENV["HOME"], ".twofa")
+fatal("No 2fa issuers file at ~/.twofa") unless File.exist?(TWOFAFILE)
+
+SECRETS = Secrets.new(File.readlines(TWOFAFILE).map(&:strip))
+
+ISSUER = ARGV.shift&.strip&.downcase
+fatal("Specify issuer") if ISSUER.nil?
+
+sec = SECRETS[ISSUER]
+fatal("No such issuer") if sec.nil?
+
+puts "#{sec.verify} (for #{sec.time_remaining} more seconds)"