Google Authenticator CLI
#!/usr/bin/env ruby | |
# Licence : AGPLv3+ | |
require 'cgi' | |
require 'fileutils' | |
require 'optparse' | |
require 'rotp' | |
require 'tempfile' | |
require 'uri' | |
class GoogleAuthenticator | |
CONFIG_FILE = File.join Dir.home, '.google-authenticator' | |
FileUtils.touch CONFIG_FILE unless File.exists? CONFIG_FILE | |
class OTP | |
attr_reader :name, :issuer | |
def self.get(line) | |
uri = URI line | |
type = uri.host | |
name = uri.path.sub /^\//, '' | |
query = CGI::parse uri.query | |
secret = query['secret'].first | |
issuer = query['issuer'].first | |
algorithm = query['algorithm'].first || 'sha1' | |
digits = numeric_value query, 'digits', 6 | |
counter = numeric_value query, 'counter' | |
period = numeric_value query, 'period', 30 | |
case type | |
when 'hotp' then | |
HOTP.new line, name, secret, issuer, algorithm, digits, counter | |
when 'totp' then | |
TOTP.new line, name, secret, issuer, algorithm, digits, period | |
end | |
end | |
def qrcode | |
file = ::Tempfile.new ['qrcode', 'png'] | |
system 'qrencode', '-t', 'png', '-o', file.path, @line | |
file | |
end | |
protected | |
def initialize(otp, line, name, secret, issuer=nil, algorithm='sha1', digits=6) | |
@otp = otp | |
@line = line | |
@name = name | |
@secret = secret | |
@issuer = issuer | |
@algorithm = algorithm | |
@digits = digits | |
end | |
private_class_method | |
def self.numeric_value(query, name, default_value = nil) | |
value = query[name].first | |
return default_value if value.nil? | |
value.to_i | |
end | |
end | |
class HOTP < OTP | |
def initialize(line, name, secret, issuer=nil, algorithm='sha1', digits=6, counter=0) | |
super line, name, secret, issuer, algorithm, digits | |
@counter = counter | |
end | |
end | |
class TOTP < OTP | |
def initialize(line, name, secret, issuer=nil, algorithm='sha1', digits=6, period=30) | |
@period = period | |
otp = ::ROTP::TOTP.new secret, digits: digits, digest: algorithm, interval: period | |
super otp, line, name, secret, issuer, algorithm, digits | |
end | |
def code | |
@otp.now | |
end | |
def delay | |
@period - (Time.now.to_i % @period) | |
end | |
end | |
attr_reader :otps | |
def initialize | |
@otps = Hash[IO.readlines(CONFIG_FILE).collect do |line| | |
otp = OTP.get line.chomp() | |
[otp.name, otp] | |
end] | |
end | |
def add(secret) | |
puts "Adding secret : \"#{secret}\"" | |
File.open(CONFIG_FILE, 'a') do |f| | |
f.puts secret | |
end | |
end | |
def add_qrcode(data) | |
require 'zbar' | |
self.add ZBar::Image.from_jpeg(data).process[0].data | |
end | |
def otp(name) | |
raise 'No such OTP' unless @otps.has_key? name | |
@otps[name] | |
end | |
def otps | |
@otps.collect do |_, o| | |
s = o.name | |
s << %{ (#{o.issuer})} unless o.issuer.nil? | |
s | |
end | |
end | |
end | |
begin | |
authenticator = GoogleAuthenticator.new | |
options = {} | |
OptionParser.new do |opts| | |
opts.on('-a STRING', '--add STRING', 'Add secret from string') do |o| | |
authenticator.add o | |
exit | |
end | |
opts.on('-i[PATH]', '--img[=PATH]', 'Add secret from QR code') do |o| | |
o = o.nil? ? $stdin : File.read(o) | |
authenticator.add_qrcode o | |
exit | |
end | |
opts.on('-d NAME', '--delete NAME', 'Remove secret') do |o| | |
exit | |
end | |
opts.on('-q NAME', '--qrcode NAME', 'Display qrcode') do |o| | |
file = authenticator.otp(o).qrcode | |
system 'display', file.path | |
file.close | |
file.unlink | |
exit | |
end | |
end.parse! | |
name = ARGV[0] | |
if name.nil? | |
puts authenticator.otps | |
else | |
otp = authenticator.otp name | |
unless $stdout.tty? | |
puts otp.code | |
$stderr.puts "#{otp.code} (#{otp.delay}s)" | |
else | |
puts "#{otp.code} (#{otp.delay}s)" | |
end | |
end | |
rescue Exception => e | |
puts e.message | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment