Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@aeris
Created November 24, 2015 22:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aeris/fd59fd49205cb085374c to your computer and use it in GitHub Desktop.
Save aeris/fd59fd49205cb085374c to your computer and use it in GitHub Desktop.
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