From 5a27585bbb467802c579ae3a1d043e31cd8c315d Mon Sep 17 00:00:00 2001 From: Nat Lasseter Date: Thu, 29 Sep 2022 12:27:19 +0100 Subject: Added HOTP functionality --- Readme.textile | 18 +++++++++++--- twofa | 75 +++++++++++++++++++++++++++++++++++++++++++++------------- 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" -- cgit v1.2.1