class Authentic::CLI

Constants

CLOCKS

Public Instance Methods

add(name, secret_key, service = nil) click to toggle source
# File lib/authentic.rb, line 18
def add(name, secret_key, service = nil)
  params = {
    label: "authentic gem",
    account:  name,
    service:  service || name,
    password: secret_key,
  }

  begin
    item = Keychain.generic_passwords.create(params)
  rescue => e
    message = e.message
  end

  unless item.nil?
    say "\u2713".colorize(:green) + " Service #{name.colorize(:green)} added to keychain"
  else
    say "\u2717".colorize(:red) + " Couldn't add service #{name.colorize(:red)}..."
    say "Error: #{message}" unless message.nil?
  end
end
delete(name) click to toggle source
# File lib/authentic.rb, line 42
def delete(name)
  item = Keychain.generic_passwords.where(label: "authentic gem", account: name).first
  unless item
    return say "\u2717".colorize(:red) + " Couldn't find service #{name.colorize(:red)}..."
  end
  if options[:force] || yes?("Do you want to permanently delete #{name.colorize(:green)}?")
    item.delete
    say "\u2713".colorize(:green) + " Service #{name.colorize(:green)} deleted from keychain"
  else
    say "Leaving service #{name.colorize(:green)} in keychain"
  end
end
export() click to toggle source
# File lib/authentic.rb, line 65
def export
  keys = Keychain
    .generic_passwords
    .where(label: "authentic gem")
    .all.map do |key|
      secret = key.password.gsub(/=*$/, '')
      totp = ROTP::TOTP.new(secret)
      OpenStruct.new(
        secret:  secret,
        name:    key.attributes[:account],
        service: key.attributes[:service]
      )
    end.sort_by { |k| [k.service, k.name] }

  if options['qr']
    keys.each do |key|
      puts "#{key.service} - #{key.name}\n"
      puts `qrencode 'otpauth://totp/#{key.name}?issuer=#{key.service}&secret=#{key.secret}' -s 5 -o - | ~/.iterm2/imgcat`
    end
  else
    data = keys.map(&:to_h).to_json

    password = ask "Please enter a password for exported data:", echo: false
    return if password.empty?

    salt = OpenSSL::Random.random_bytes(32)
    cipher = OpenSSL::Cipher::AES256.new :CBC
    cipher.encrypt
    cipher.key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, salt, 20000, 32)
    cipher.iv = salt

    cipher_text = cipher.update(data)
    cipher_text << cipher.final

    puts [salt, cipher_text].map { |part| Base64.strict_encode64(part) }.join(':')
  end
end
generate() click to toggle source
# File lib/authentic.rb, line 127
def generate
  now  = Time.now
  keys = Keychain
          .generic_passwords
          .where(label: "authentic gem")
          .all.map do |key|
            secret = key.password.gsub(/=*$/, '')
            totp = ROTP::TOTP.new(secret)
            OpenStruct.new(
              secret:  secret,
              code:    totp.at(now),
              name:    key.attributes[:account],
              service: key.attributes[:service],
              remain:  now.utc.to_i % totp.interval
            )
          end.sort_by { |k| [k.service, k.name] }

  table = keys.each_with_index.map do |key, idx|
    number = (idx + 1).to_s.rjust(keys.size.to_s.size, ' ')
    service_prefix = "#{key.service} - " if key.service && key.service != key.name
    [
      number.colorize(:red),
      key.code.colorize(:green),
      "#{service_prefix}#{key.name}",
      CLOCKS[7 * (key.remain / 7)].colorize(:blue)
    ]
  end

  print_table(table.to_a)

  unless options['skip-copy'] || keys.size == 0
    if keys.size > 1
      prompt = "\nWhich key should I copy?"
      prompt += " [1-#{keys.size}, leave empty to exit]"
      response = ask prompt
      return if response.empty?
      idx = response.to_i - 1
      key = keys[idx]
    else
      key = keys.first
    end
    Clipboard.pbcopy key.code
    say "\nKey for account #{key.name.colorize(:green)} copied to clipboard"
  end
end
import(data) click to toggle source
# File lib/authentic.rb, line 104
def import(data)
  password = ask "Please enter a password for exported data:", echo: false
  return if password.empty?

  salt, cipher_text = data.split(':').map { |part| Base64.strict_decode64(part) }

  cipher = OpenSSL::Cipher::AES256.new :CBC
  cipher.decrypt
  cipher.iv = salt
  cipher.key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, salt, 20000, 32)

  text = cipher.update(cipher_text)
  text << cipher.final

  keys = JSON.parse(text)

  keys.each do |key|
    add(key['name'], key['secret'], key['service'])
  end
end