1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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)"
|