class Tome::Command

Public Class Methods

run(tome_filename, args, stdout = $stdout, stderr = $stderr, stdin = $stdin) click to toggle source
# File lib/tome/command.rb, line 12
def self.run(tome_filename, args, stdout = $stdout, stderr = $stderr, stdin = $stdin)
  command = new()
  return command.send(:run, tome_filename, args, stdout, stderr, stdin)
end

Private Instance Methods

command_from_arg(arg) click to toggle source
# File lib/tome/command.rb, line 57
def command_from_arg(arg)
  commands = {
    /\A(help|-h|--help)\z/i => :help,
    /\A(version|ver|-v|--version)\z/i => :version,
    /\A(set|s|add)\z/i => :set,
    /\A(get|g|show)\z/i => :get,
    /\A(delete|del|rm|remove)\z/i => :delete,
    /\A(generate|gen)\z/i => :generate,
    /\A(copy|cp)\z/i => :copy,
    /\A(rename|ren|rn)\z/i => :rename,
    /\A(master)\z/i => :master,
    /\A(list|ls)\z/i => :list
  }

  commands.each { |pattern, command|
    return command if arg =~ pattern
  }

  return nil
end
copy(args) click to toggle source
# File lib/tome/command.rb, line 243
def copy(args)
  if args.length != 1
    raise CommandError, "Invalid arguments.\n\n#{$copy_usage}"
  end

  # tome cp bar.com
  # tome cp foo@bar.com
  pattern = args[0]

  tome = tome_connect()
  matches = tome.find(pattern)

  if matches.empty?
    raise CommandError, "No password found for #{pattern}."
  elsif matches.count > 1
    match = pick_match(pattern, matches)
  else
    match = matches.first
  end

  name = match.first
  password = match.last

  Clipboard.copy(password)
  if Clipboard.paste == password
    @out.puts "Password for #{name} copied to clipboard."
  else
    @err.puts "Failed to copy password for #{name} to clipboard."
  end
end
delete(args) click to toggle source
# File lib/tome/command.rb, line 185
def delete(args)
  if args.length != 1
    raise CommandError, "Invalid arguments.\n\n#{$delete_usage}"
  end

  tome = tome_connect()

  # tome del bar.com
  # tome del foo@bar.com
  id = args[0]

  exists = !tome.get(id).nil?
  if exists
    confirmed = prompt_confirm("Are you sure you want to delete the password for #{id} (y/n)? ")
    if !confirmed
      raise CommandError, 'Aborted.'
    end
  end

  deleted = tome.delete(id)

  if deleted
    @out.puts "Deleted password for #{id}."
  else
    @out.puts "No password found for #{id}."
  end
end
generate(args) click to toggle source
# File lib/tome/command.rb, line 213
def generate(args)
  if args.length != 1
    raise CommandError, "Invalid arguments.\n\n#{$generate_usage}"
  end
  
  created, tome = tome_create_connect()

  # tome gen bar.com
  # tome gen foo@bar.com
  id = args[0]
  password = generate_password()

  exists = !tome.get(id).nil?
  if exists
    confirm = prompt_confirm("A password already exists for #{id}. Overwrite (y/n)? ")
    if !confirm
      raise CommandError, 'Aborted.'
    end
  end

  created = tome.set(id, password)
  Clipboard.copy(password)

  if created
    @out.puts "Generated and copied password for #{id}."
  else
    @out.puts "Updated and copied password for #{id}."
  end
end
generate_password() click to toggle source
# File lib/tome/command.rb, line 356
def generate_password
  Passgen.generate(:length => 30, :symbols => true)
end
get(args) click to toggle source
# File lib/tome/command.rb, line 159
def get(args)
  if args.length != 1
    raise CommandError, "Invalid arguments.\n\n#{$get_usage}"
  end
  
  # tome get bar.com
  # tome get foo@bar.com
  pattern = args[0]

  tome = tome_connect()
  matches = tome.find(pattern)

  if matches.empty?
    raise CommandError, "No password found for #{pattern}."
  elsif matches.count == 1
    match = matches.first
    @out.puts "Password for #{match.first}:"
    @out.puts match.last
  else
    @out.puts "Multiple matches for #{pattern}:"
    matches.each { |key, password|
      @out.puts "#{key}: #{password}"
    }
  end
end
handle_command(args) click to toggle source
# File lib/tome/command.rb, line 44
def handle_command(args)
  # TODO: Handle 'command --help', e.g. 'tome set --help'.

  command = command_from_arg(args[0])

  if command.nil?
    raise CommandError, "Unrecognized command: #{args[0]}.\n\n#{$usage}"
  end

  args.shift
  send(command, args) 
end
help(args) click to toggle source
# File lib/tome/command.rb, line 78
def help(args)
  if args.length > 1
    raise CommandError, "Invalid arguments.\n\n#{$usage}"
  end

  if args.empty?
    usage()
    return
  end

  command = command_from_arg(args[0])

  if command.nil?
    raise CommandError, "No help for unrecognized command: #{args[0]}.\n\n#{$usage}"
  end

  help = {
    :help => $help_usage,
    :set => $set_usage,
    :get => $get_usage,
    :delete => $delete_usage,
    :generate => $generate_usage,
    :copy => $copy_usage,
    :rename => $rename_usage,
    :master => $master_usage,
    :list => $list_usage
  }

  usage = help[command]
  if usage.nil?
    raise CommandError, "No help available for command: #{args[0]}."
  end

  @out.puts usage
end
input_password() click to toggle source
# File lib/tome/command.rb, line 384
def input_password
  input = proc { |stdin|
    raw = stdin.gets
    return nil if raw.nil?

    password = raw.strip
    @out.puts

    return password
  }

  begin
    @in.noecho { |stdin|
      input.call stdin
    }
  rescue Errno::EBADF
    # This can happen when stdin refers to a file or pipe.
    # In this case, we ignore 'no echo' and do normal input.
    input.call @in
  end
end
list(args) click to toggle source
# File lib/tome/command.rb, line 297
def list(args)
  if !args.empty?
    raise CommandError, "Invalid arguments.\n\n#{$list_usage}"
  end

  tome = tome_connect()

  count = 0
  tome.each_password { |id, password|
    @out.puts "#{id}: #{password}"
    count += 1
  }

  if count == 0
    @out.puts 'No passwords stored.'
  end
end
master(args) click to toggle source
# File lib/tome/command.rb, line 342
def master(args)
  if args.count > 0
    raise CommandError, "Invalid arguments.\n\n#{$master_usage}"
  end

  created, tome = tome_create_connect()

  if !created
    master_password = prompt_password('New master password')
    tome.master_password = master_password
    @out.puts 'Master password updated.'
  end
end
pick_match(pattern, matches) click to toggle source
# File lib/tome/command.rb, line 274
def pick_match(pattern, matches)
  matches = matches.to_a

  @out.puts "Found multiple matches for \"#{pattern}\":\n\n"
  matches.each_with_index { |match, i|
    @out.puts "\t#{i + 1}: #{match[0]}"
  }

  begin
    @out.print "\n> "

    index = @in.gets.to_i
    if index <= 0 || index > matches.length
      @out.puts 'Invalid option.'
      raise
    end

    return matches[index - 1]
  rescue
    retry
  end
end
prompt_confirm(prompt) click to toggle source
# File lib/tome/command.rb, line 406
def prompt_confirm(prompt)
  begin
    @out.print prompt

    confirm = @in.gets.strip

    if confirm =~ /\Ay/i
      return true
    elsif confirm =~ /\An/i
      return false
    end
  rescue
    retry
  end
end
prompt_password(prompt = 'Password') click to toggle source
# File lib/tome/command.rb, line 360
def prompt_password(prompt = 'Password')
  begin
    @err.print "#{prompt}: "
    password = input_password()

    if password.empty?
      @err.puts 'Password cannot be blank.'
      raise
    end

    @err.print "#{prompt} (verify): "
    verify = input_password()

    if verify != password
      @err.puts 'Passwords do not match.'
      raise
    end
  rescue
    retry
  end

  return password
end
rename(args) click to toggle source
# File lib/tome/command.rb, line 315
def rename(args)
  if args.count != 2
    raise CommandError, "Invalid arguments.\n\n#{$rename_usage}"
  end

  tome = tome_connect()

  old_id = args[0]
  new_id = args[1]

  overwriting = !tome.get(new_id).nil?
  if overwriting
    confirm = prompt_confirm("A password already exists for #{new_id}. Overwrite (y/n)? ")
    if !confirm
      raise CommandError, 'Aborted.'
    end
  end

  renamed = tome.rename(old_id, new_id)
  
  if !renamed
    raise CommandError, "#{old_id} does not exist."
  else
    @out.puts "#{old_id} renamed to #{new_id}."
  end
end
run(tome_filename, args, stdout, stderr, stdin) click to toggle source
# File lib/tome/command.rb, line 18
def run(tome_filename, args, stdout, stderr, stdin)
  @out = stdout
  @err = stderr
  @in = stdin
  @tome_filename = tome_filename
  
  if args.length < 1
    usage()
    return 1
  end

  begin
    handle_command(args)
  rescue CommandError => error
    @err.puts "Error: #{error.message}"
    return 1
  rescue FileFormatError => error
    # Fix file separators for Windows.
    filename = @tome_filename.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
    @err.puts "Error: Cannot read #{filename}: #{error.message}"
    return 1
  end

  return 0
end
set(args) click to toggle source
# File lib/tome/command.rb, line 118
def set(args)
  if args.length < 1 || args.length > 2
    raise CommandError, "Invalid arguments.\n\n#{$set_usage}"
  end
  
  created, tome = tome_create_connect()

  case args.length
    # TODO: Validate that first argument is in [username@]domain form.

    # tome set bar.com
    # tome set foo@bar.com
    when 1
      id = args[0]
      password = prompt_password()

    # tome set bar.com p4ssw0rd
    # tome set foo@bar.com p4ssw0rd
    when 2
      id = args[0]
      password = args[1]
  end

  exists = !tome.get(id).nil?
  if exists
    confirm = prompt_confirm("A password already exists for #{id}. Overwrite (y/n)? ")
    if !confirm
      raise CommandError, 'Aborted.'
    end
  end

  created = tome.set(id, password)
  if created
    @out.print 'Created '
  else
    @out.print 'Updated '
  end

  @out.puts "password for #{id}."
end
tome_connect() click to toggle source
# File lib/tome/command.rb, line 428
def tome_connect
  if !Tome.exists?(@tome_filename)
    raise CommandError, "Tome database does not exist. Use 'tome set' or 'tome generate' to create a password first."
  end

  begin
    @err.print 'Master password: '
    master_password = input_password()
    tome = Tome.new(@tome_filename, master_password)
  rescue MasterPasswordError
    @err.puts 'Incorrect master password.'

    if master_password.nil?
      raise CommandError, 'Authentication failed.'
    else
      retry
    end
  end

  return tome
end
tome_create_connect() click to toggle source
# File lib/tome/command.rb, line 450
def tome_create_connect
  if !Tome.exists?(@tome_filename)
    @out.puts 'Creating tome database.'
    master_password = prompt_password('Master password')
    return true, Tome.create!(@tome_filename, master_password)
  else
    return false, tome_connect()
  end
end
usage() click to toggle source
# File lib/tome/command.rb, line 422
def usage
  @err.puts "tome version #{VERSION}"
  @err.puts
  @err.puts $usage
end
version(args) click to toggle source
# File lib/tome/command.rb, line 114
def version(args)
  @out.puts "tome version #{VERSION}"
end