keybox 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGES +14 -0
- data/COPYING +22 -0
- data/README +132 -0
- data/bin/keybox +18 -0
- data/bin/kpg +19 -0
- data/data/chargrams.txt +8432 -0
- data/lib/keybox.rb +29 -0
- data/lib/keybox/application/base.rb +114 -0
- data/lib/keybox/application/password_generator.rb +131 -0
- data/lib/keybox/application/password_safe.rb +410 -0
- data/lib/keybox/cipher.rb +6 -0
- data/lib/keybox/convert.rb +1 -0
- data/lib/keybox/convert/csv.rb +96 -0
- data/lib/keybox/digest.rb +13 -0
- data/lib/keybox/entry.rb +200 -0
- data/lib/keybox/error.rb +5 -0
- data/lib/keybox/password_hash.rb +33 -0
- data/lib/keybox/randomizer.rb +193 -0
- data/lib/keybox/storage.rb +2 -0
- data/lib/keybox/storage/container.rb +307 -0
- data/lib/keybox/storage/record.rb +103 -0
- data/lib/keybox/string_generator.rb +194 -0
- data/lib/keybox/term_io.rb +163 -0
- data/lib/keybox/uuid.rb +86 -0
- data/spec/base_app_spec.rb +56 -0
- data/spec/convert_csv_spec.rb +46 -0
- data/spec/entry_spec.rb +63 -0
- data/spec/keybox_app_spec.rb +268 -0
- data/spec/kpg_app_spec.rb +132 -0
- data/spec/password_hash_spec.rb +11 -0
- data/spec/randomizer_spec.rb +116 -0
- data/spec/storage_container_spec.rb +99 -0
- data/spec/storage_record_spec.rb +63 -0
- data/spec/string_generator_spec.rb +114 -0
- data/spec/uuid_spec.rb +74 -0
- metadata +83 -0
data/lib/keybox.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Keybox
|
2
|
+
APP_ROOT_DIR = File.dirname(File.expand_path(File.join(__FILE__,".."))).freeze
|
3
|
+
APP_LIB_DIR = File.join(APP_ROOT_DIR,"lib").freeze
|
4
|
+
APP_DATA_DIR = File.join(APP_ROOT_DIR,"data").freeze
|
5
|
+
|
6
|
+
VERSION = [1,0,0].freeze
|
7
|
+
AUTHOR = "Jeremy Hinegardner".freeze
|
8
|
+
AUTHOR_EMAIL= "jeremy@hinegardner.org".freeze
|
9
|
+
HOMEPAGE = "http://keybox.rubyforge.org".freeze
|
10
|
+
COPYRIGHT = "2006, 2007 #{AUTHOR}".freeze
|
11
|
+
DESCRIPTION = <<DESC
|
12
|
+
Keybox is a set of command line applications and ruby libraries for
|
13
|
+
secure password storage and password generation.
|
14
|
+
DESC
|
15
|
+
end
|
16
|
+
|
17
|
+
$: << Keybox::APP_LIB_DIR
|
18
|
+
|
19
|
+
require 'keybox/cipher'
|
20
|
+
require 'keybox/digest'
|
21
|
+
require 'keybox/entry'
|
22
|
+
require 'keybox/error'
|
23
|
+
require 'keybox/password_hash'
|
24
|
+
require 'keybox/randomizer'
|
25
|
+
require 'keybox/storage'
|
26
|
+
require 'keybox/string_generator'
|
27
|
+
require 'keybox/uuid'
|
28
|
+
require 'keybox/term_io'
|
29
|
+
require 'keybox/convert'
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
#------------------------------------------------------------------------
|
5
|
+
# Base class for applications in keybox
|
6
|
+
#
|
7
|
+
# All of they keybox base applications are instantiated and then the
|
8
|
+
# .run method is called on the instance. In otherwards
|
9
|
+
#
|
10
|
+
# kapp = Keybox::Application::KeyApp.new(ARGV)
|
11
|
+
# kapp.run
|
12
|
+
#
|
13
|
+
#------------------------------------------------------------------------
|
14
|
+
|
15
|
+
module Keybox
|
16
|
+
module Application
|
17
|
+
class Base
|
18
|
+
# all applications have options and an error message
|
19
|
+
attr_accessor :options
|
20
|
+
attr_accessor :parsed_options
|
21
|
+
attr_accessor :error_message
|
22
|
+
|
23
|
+
# these allow for testing instrumentation
|
24
|
+
attr_accessor :stdout
|
25
|
+
attr_accessor :stderr
|
26
|
+
attr_accessor :stdin
|
27
|
+
|
28
|
+
def initialize(argv = [])
|
29
|
+
# make sure we have an empty array, we could be
|
30
|
+
# initially passed nil explicitly
|
31
|
+
argv ||= []
|
32
|
+
|
33
|
+
# for testing instrumentation
|
34
|
+
@stdin = $stdin
|
35
|
+
@stdout = $stdout
|
36
|
+
@stderr = $stderr
|
37
|
+
|
38
|
+
@options = self.default_options
|
39
|
+
@parsed_options = self.default_options
|
40
|
+
@parser = self.option_parser
|
41
|
+
@error_message = nil
|
42
|
+
|
43
|
+
begin
|
44
|
+
@parser.parse!(argv)
|
45
|
+
rescue OptionParser::ParseError => pe
|
46
|
+
msg = ["#{@parser.program_name}: #{pe}",
|
47
|
+
"Try `#{@parser.program_name} --help` for more information"]
|
48
|
+
@error_message = msg.join("\n")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def option_parser
|
53
|
+
OptionParser.new do |op|
|
54
|
+
op.separator ""
|
55
|
+
op.separator "Options:"
|
56
|
+
|
57
|
+
op.on("-h", "--help") do
|
58
|
+
@parsed_options.show_help = true
|
59
|
+
end
|
60
|
+
|
61
|
+
op.on("-v", "--version", "Show version information") do
|
62
|
+
@parsed_options.show_version = true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def default_options
|
68
|
+
options = OpenStruct.new
|
69
|
+
options.debug = 0
|
70
|
+
options.show_version = false
|
71
|
+
options.show_help = false
|
72
|
+
return options
|
73
|
+
end
|
74
|
+
|
75
|
+
def configuration_file_options
|
76
|
+
Hash.new
|
77
|
+
end
|
78
|
+
|
79
|
+
# load the default options, layer on the file options and
|
80
|
+
# then merge in the command line options
|
81
|
+
def merge_options
|
82
|
+
options = default_options.marshal_dump
|
83
|
+
configuration_file_options.each_pair do |key,value|
|
84
|
+
options[key] = value
|
85
|
+
end
|
86
|
+
|
87
|
+
@parsed_options.marshal_dump.each_pair do |key,value|
|
88
|
+
options[key] = value
|
89
|
+
end
|
90
|
+
|
91
|
+
@options = OpenStruct.new(options)
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def error_version_help
|
96
|
+
if @error_message then
|
97
|
+
@stderr.puts @error_message
|
98
|
+
exit 1
|
99
|
+
elsif @parsed_options.show_version then
|
100
|
+
@stdout.puts "#{@parser.program_name}: version #{Keybox::VERSION.join(".")}"
|
101
|
+
exit 0
|
102
|
+
elsif @parsed_options.show_help then
|
103
|
+
@stdout.puts @parser
|
104
|
+
exit 0
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def run
|
109
|
+
error_version_help
|
110
|
+
@stdout.puts "Keybox Base Application. Doing nothing but output this line."
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'keybox/application/base'
|
2
|
+
require 'keybox/string_generator'
|
3
|
+
require 'optparse'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
#----------------------------------------------------------------------
|
7
|
+
# The Password Generation application
|
8
|
+
#----------------------------------------------------------------------
|
9
|
+
module Keybox
|
10
|
+
module Application
|
11
|
+
class PasswordGenerator < Base
|
12
|
+
|
13
|
+
ALGORITHMS = { "random" => :random,
|
14
|
+
"pronounceable" => :pronounceable }
|
15
|
+
SYMBOL_SETS = Keybox::SymbolSet::MAPPING.keys
|
16
|
+
|
17
|
+
def option_parser
|
18
|
+
OptionParser.new do |op|
|
19
|
+
op.separator ""
|
20
|
+
|
21
|
+
op.separator "Options:"
|
22
|
+
|
23
|
+
op.on("-aALGORITHM", "--algorithm ALGORITHM", ALGORITHMS.keys,
|
24
|
+
"Select the algorithm for password ",
|
25
|
+
" generation (#{ALGORITHMS.keys.join(', ')})") do |alg|
|
26
|
+
key = ALGORITHMS.keys.find { |x| x =~ /^#{alg}/ }
|
27
|
+
@parsed_options.algorithm = ALGORITHMS[key]
|
28
|
+
end
|
29
|
+
|
30
|
+
op.on("-h", "--help") do
|
31
|
+
@parsed_options.show_help = true
|
32
|
+
end
|
33
|
+
|
34
|
+
op.on("-mLENGTH ", "--min-length LENGTH", Integer,
|
35
|
+
"Minimum LENGTH of the new password"," in letters") do |len|
|
36
|
+
@parsed_options.min_length = len
|
37
|
+
end
|
38
|
+
|
39
|
+
op.on("-xLENGTH ", "--max-length LENGTH", Integer,
|
40
|
+
"Maximum LENGTH of the new password"," in letters") do |len|
|
41
|
+
@parsed_options.max_length = len
|
42
|
+
end
|
43
|
+
|
44
|
+
op.on("-nNUMER", "--number NUMBER", Integer,
|
45
|
+
"Generate NUMBER of passwords (default 6)") do |n|
|
46
|
+
@parsed_options.number_to_generate = n
|
47
|
+
end
|
48
|
+
|
49
|
+
op.on("-uLIST", "--use symbol,set,list", Array,
|
50
|
+
"Use only one ore more of the following", " symbol sets:",
|
51
|
+
" [#{SYMBOL_SETS.join(', ')}]") do |list|
|
52
|
+
list.each do |symbol_set|
|
53
|
+
sym = SYMBOL_SETS.find { |s| s =~ /^#{symbol_set}/ }
|
54
|
+
raise OptionParser::InvalidArgument, ": #{symbol_set} does not match any of #{SYMBOL_SETS.join(', ')}" if sym.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
@parsed_options.use_symbols = options_to_symbol_sets(list)
|
58
|
+
end
|
59
|
+
|
60
|
+
op.on("-rLIST","--require symbol,set,list", Array,
|
61
|
+
"Require passwords to have letters from", " one or more of the following",
|
62
|
+
" symbol sets:", " [#{SYMBOL_SETS.join(', ')}]") do |list|
|
63
|
+
list.each do |symbol_set|
|
64
|
+
sym = SYMBOL_SETS.find { |s| s =~ /^#{symbol_set}/ }
|
65
|
+
raise OptionParser::InvalidArgument, ": #{symbol_set} does not match any of #{SYMBOL_SETS.join(', ')}" if sym.nil?
|
66
|
+
end
|
67
|
+
@parsed_options.require_symbols = options_to_symbol_sets(list)
|
68
|
+
end
|
69
|
+
|
70
|
+
op.on("-v", "--version", "Show version information") do
|
71
|
+
@parsed_options.show_version = true
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def default_options
|
78
|
+
options = OpenStruct.new
|
79
|
+
options.debug = 0
|
80
|
+
options.show_version = false
|
81
|
+
options.show_help = false
|
82
|
+
options.algorithm = :random
|
83
|
+
options.number_to_generate = 6
|
84
|
+
options.min_length = 8
|
85
|
+
options.max_length = 10
|
86
|
+
options.use_symbols = options_to_symbol_sets(["all"])
|
87
|
+
options.require_symbols = options_to_symbol_sets([])
|
88
|
+
return options
|
89
|
+
end
|
90
|
+
|
91
|
+
def options_to_symbol_sets(args)
|
92
|
+
sets = []
|
93
|
+
args.each do |a|
|
94
|
+
sym = SYMBOL_SETS.find { |s| s =~ /^#{a}/ }
|
95
|
+
sets << Keybox::SymbolSet::MAPPING[sym]
|
96
|
+
end
|
97
|
+
sets
|
98
|
+
end
|
99
|
+
|
100
|
+
def create_generator
|
101
|
+
case @options.algorithm
|
102
|
+
when :pronounceable
|
103
|
+
generator = Keybox::CharGramGenerator.new
|
104
|
+
when :random
|
105
|
+
generator = Keybox::SymbolSetGenerator.new(@options.use_symbols)
|
106
|
+
@options.require_symbols.each do |req|
|
107
|
+
generator.required_sets << req
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
generator.max_length = [@options.min_length,@options.max_length].max
|
112
|
+
generator.min_length = [@options.min_length,@options.max_length].min
|
113
|
+
|
114
|
+
# record what we set the generator to
|
115
|
+
@options.max_length = generator.max_length
|
116
|
+
@options.min_length = generator.min_length
|
117
|
+
|
118
|
+
return generator
|
119
|
+
end
|
120
|
+
|
121
|
+
def run
|
122
|
+
error_version_help
|
123
|
+
merge_options
|
124
|
+
generator = create_generator
|
125
|
+
@options.number_to_generate.times do
|
126
|
+
@stdout.puts generator.generate
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,410 @@
|
|
1
|
+
require 'keybox/storage'
|
2
|
+
require 'keybox/application/base'
|
3
|
+
require 'optparse'
|
4
|
+
require 'ostruct'
|
5
|
+
require 'uri'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
#-----------------------------------------------------------------------
|
9
|
+
# The Password Safe application
|
10
|
+
#-----------------------------------------------------------------------
|
11
|
+
module Keybox
|
12
|
+
module Application
|
13
|
+
class PasswordSafe < Base
|
14
|
+
include Keybox::TermIO
|
15
|
+
|
16
|
+
attr_accessor :actions
|
17
|
+
attr_reader :db
|
18
|
+
|
19
|
+
DEFAULT_DIRECTORY = File.join(ENV["HOME"],".keybox")
|
20
|
+
DEFAULT_DB = File.join(DEFAULT_DIRECTORY,"database.yaml")
|
21
|
+
DEFAULT_CONFIG = File.join(DEFAULT_DIRECTORY,"config.yaml")
|
22
|
+
|
23
|
+
ACTION_LIST = %w(add delete edit show list master-password)
|
24
|
+
|
25
|
+
def initialize(argv = [])
|
26
|
+
@actions = Array.new
|
27
|
+
super(argv)
|
28
|
+
|
29
|
+
# only one of the actions is allowed
|
30
|
+
if actions.size > 1 then
|
31
|
+
@error_message = [ "ERROR: Only one of #{ACTION_LIST.join(",")} is allowed at a time",
|
32
|
+
"Try `#{@parser.program_name} --help` for more information"].join("\n")
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def option_parser
|
38
|
+
OptionParser.new do |op|
|
39
|
+
|
40
|
+
op.separator ""
|
41
|
+
op.separator "General Options:"
|
42
|
+
|
43
|
+
op.on("-f", "--file DATABASE_FILE", "The Database File to use") do |db_file|
|
44
|
+
@parsed_options.db_file = db_file
|
45
|
+
end
|
46
|
+
|
47
|
+
op.on("-c", "--config CONFIG_FILE", "The Configuration file to use") do |cfile|
|
48
|
+
@parsed_options.config_file = cfile
|
49
|
+
end
|
50
|
+
|
51
|
+
op.on("-D", "--debug", "Ouput debug information to STDERR") do
|
52
|
+
@parsed_options.debug = true
|
53
|
+
end
|
54
|
+
|
55
|
+
op.on("--[no-]use-hash-for-url", "Use the password hash algorithm ", " for URL accounts") do |r|
|
56
|
+
@parsed_options.use_password_hash_for_url = r
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
op.separator ""
|
61
|
+
op.separator "Commands, one and only one of these is required:"
|
62
|
+
|
63
|
+
op.on("-h", "--help") do
|
64
|
+
@parsed_options.show_help = true
|
65
|
+
end
|
66
|
+
|
67
|
+
op.on("-a", "--add ACCOUNT", "Create a new account in keybox") do |account|
|
68
|
+
@actions << [:add, account]
|
69
|
+
end
|
70
|
+
|
71
|
+
op.on("-d", "--delete ACCOUNT", "Delete the account from keybox") do |account|
|
72
|
+
@actions << [:delete, account]
|
73
|
+
end
|
74
|
+
|
75
|
+
op.on("-e", "--edit ACCOUNT", "Edit the account in keybox") do |account|
|
76
|
+
@actions << [:edit, account]
|
77
|
+
end
|
78
|
+
|
79
|
+
op.on("-l", "--list [REGEX]", "List the matching accounts", " (no argument will list all)") do |regex|
|
80
|
+
regex = regex || ".*"
|
81
|
+
@actions << [:list, regex]
|
82
|
+
end
|
83
|
+
|
84
|
+
op.on("-m", "--master-password", "Change the master password") do
|
85
|
+
@actions << [:master_password, nil]
|
86
|
+
end
|
87
|
+
|
88
|
+
op.on("-s", "--show [REGEX]", "Show the given account(s)") do |regex|
|
89
|
+
regex = regex || ".*"
|
90
|
+
@actions << [:show, regex]
|
91
|
+
end
|
92
|
+
|
93
|
+
op.on("-v", "--version", "Show version information") do
|
94
|
+
@parsed_options.show_version = true
|
95
|
+
end
|
96
|
+
|
97
|
+
op.separator ""
|
98
|
+
op.separator "Import / Export from other data formats:"
|
99
|
+
|
100
|
+
op.on("-i", "--import-from-csv FILE", "Import from a CSV file") do |file|
|
101
|
+
@actions << [:import_from_csv, file]
|
102
|
+
end
|
103
|
+
|
104
|
+
op.on("-x", "--export-to-csv FILE", "Export contents to a CSV file") do |file|
|
105
|
+
@actions << [:export_to_csv, file]
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def default_options
|
112
|
+
options = OpenStruct.new
|
113
|
+
options.debug = 0
|
114
|
+
options.show_help = false
|
115
|
+
options.show_version = false
|
116
|
+
options.config_file = Keybox::Application::PasswordSafe::DEFAULT_CONFIG
|
117
|
+
options.db_file = Keybox::Application::PasswordSafe::DEFAULT_DB
|
118
|
+
options.use_password_hash_for_url = false
|
119
|
+
return options
|
120
|
+
end
|
121
|
+
|
122
|
+
# load options from the configuration file, if the file
|
123
|
+
# doesn't exist, create it and dump the default options to
|
124
|
+
# it.
|
125
|
+
#
|
126
|
+
# we use the default unless the parsed_options contain a
|
127
|
+
# configuration file then we use that one
|
128
|
+
def configuration_file_options
|
129
|
+
|
130
|
+
file_path = @parsed_options.config_file || DEFAULT_CONFIG
|
131
|
+
|
132
|
+
# if the file is 0 bytes, then this is illegal and needs
|
133
|
+
# to be overwritten.
|
134
|
+
if not File.exists?(file_path) or File.size(file_path) == 0 then
|
135
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
136
|
+
File.open(file_path,"w") do |f|
|
137
|
+
YAML.dump(default_options.marshal_dump,f)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
options = YAML.load_file(file_path) || Hash.new
|
141
|
+
end
|
142
|
+
|
143
|
+
def load_database
|
144
|
+
password = nil
|
145
|
+
if not File.exists?(@options.db_file) then
|
146
|
+
color_puts "Creating initial database.", :yellow
|
147
|
+
password = prompt("Initial Password for (#{@options.db_file})", false, true)
|
148
|
+
else
|
149
|
+
password = prompt("Password for (#{@options.db_file})", false)
|
150
|
+
end
|
151
|
+
@db = Keybox::Storage::Container.new(password,@options.db_file)
|
152
|
+
end
|
153
|
+
|
154
|
+
#
|
155
|
+
# add an account to the database If the account is a URL and
|
156
|
+
# use_password_hash_for_url is true then don't us the
|
157
|
+
# URLAccountEntry instead of the usual HostAccountEntry
|
158
|
+
#
|
159
|
+
def add(account)
|
160
|
+
entry = Keybox::HostAccountEntry.new(account, account)
|
161
|
+
|
162
|
+
if @options.use_password_hash_for_url then
|
163
|
+
begin
|
164
|
+
account_uri = URI.parse(account)
|
165
|
+
if not account_uri.scheme.nil? then
|
166
|
+
entry = Keybox::URLAccountEntry.new(account,account)
|
167
|
+
end
|
168
|
+
rescue ::URI::InvalidURIError => e
|
169
|
+
# just ignore it, we're fine with the Host
|
170
|
+
# Account Entry
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
new_entry = gather_info(entry)
|
175
|
+
color_puts "Adding #{new_entry.title} to database", :green
|
176
|
+
@db << new_entry
|
177
|
+
end
|
178
|
+
|
179
|
+
#
|
180
|
+
# Gather all the information for the
|
181
|
+
def gather_info(entry)
|
182
|
+
gathered = false
|
183
|
+
while not gathered do
|
184
|
+
color_puts "Gathering information for entry '#{entry.title}'", :yellow
|
185
|
+
|
186
|
+
entry = fill_entry(entry)
|
187
|
+
|
188
|
+
# dump the info we have gathered and make sure that
|
189
|
+
# it is the input that the user wants to store.
|
190
|
+
|
191
|
+
color_puts "-" * 40, :blue
|
192
|
+
@stdout.puts entry
|
193
|
+
color_puts "-" * 40, :blue
|
194
|
+
if prompt_y_n("Is this information correct (y/n) [N] ?") then
|
195
|
+
gathered = true
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
entry
|
200
|
+
end
|
201
|
+
|
202
|
+
#
|
203
|
+
# delete an entry from the database
|
204
|
+
#
|
205
|
+
def delete(account)
|
206
|
+
matches = @db.find(account)
|
207
|
+
count = 0
|
208
|
+
matches.each do |match|
|
209
|
+
color_puts "-" * 40, :blue
|
210
|
+
@stdout.puts match
|
211
|
+
color_puts "-" * 40, :blue
|
212
|
+
|
213
|
+
if prompt_y_n("Delete this entry (y/n) [N] ?") then
|
214
|
+
@db.delete(match)
|
215
|
+
count += 1
|
216
|
+
end
|
217
|
+
end
|
218
|
+
color_puts "#{count} records matching '#{account}' deleted.", :green
|
219
|
+
end
|
220
|
+
|
221
|
+
#
|
222
|
+
# edit an entry in the database
|
223
|
+
#
|
224
|
+
def edit(account)
|
225
|
+
matches = @db.find(account)
|
226
|
+
count = 0
|
227
|
+
matches.each do |match|
|
228
|
+
color_puts "-" * 40, :blue
|
229
|
+
@stdout.puts match
|
230
|
+
color_puts "-" * 40, :blue
|
231
|
+
|
232
|
+
if prompt_y_n("Edit this entry (y/n) [N] ?") then
|
233
|
+
entry = gather_info(match)
|
234
|
+
@db.delete(match)
|
235
|
+
@db << entry
|
236
|
+
count += 1
|
237
|
+
color_puts "Entry '#{entry.title}' updated.", :green
|
238
|
+
end
|
239
|
+
end
|
240
|
+
color_puts "#{count} records matching '#{account}' edited.", :green
|
241
|
+
end
|
242
|
+
|
243
|
+
#
|
244
|
+
# list all the entries in the database. This doesn't show
|
245
|
+
# the password for any of them, just lists the key
|
246
|
+
# information about each entry so the user can see what is
|
247
|
+
# in the database
|
248
|
+
#
|
249
|
+
def list(account)
|
250
|
+
matches = @db.find(account)
|
251
|
+
title = "Title"
|
252
|
+
username = "Username"
|
253
|
+
add_info = "Additional Information"
|
254
|
+
if matches.size > 0 then
|
255
|
+
lengths = {
|
256
|
+
:title => (matches.collect { |f| f.title.length } << title.length).max,
|
257
|
+
:username => (matches.collect { |f| f.username.length } << username.length).max,
|
258
|
+
:additional_info => add_info.length
|
259
|
+
}
|
260
|
+
|
261
|
+
full_length = lengths.values.inject(0) { |sum,n| sum + n}
|
262
|
+
header = " # #{"Title".ljust(lengths[:title])} #{"Username".ljust(lengths[:username])} #{add_info}"
|
263
|
+
color_puts header, :yellow
|
264
|
+
# 3 spaces for number column + 1 space after and 4 spaces between
|
265
|
+
# each other column
|
266
|
+
color_puts "-" * (header.length), :blue, false
|
267
|
+
|
268
|
+
matches.each_with_index do |match,i|
|
269
|
+
color_print sprintf("%3d ", i + 1), :white
|
270
|
+
# toggle colors
|
271
|
+
color = [:cyan, :magenta][i % 2]
|
272
|
+
columns = []
|
273
|
+
[:title, :username, :additional_info].each do |f|
|
274
|
+
t = match.send(f)
|
275
|
+
t = "-" if t.nil? or t.length == 0
|
276
|
+
columns << t.ljust(lengths[f])
|
277
|
+
end
|
278
|
+
color_puts columns.join(" " * 4), color
|
279
|
+
end
|
280
|
+
else
|
281
|
+
color_puts "No matching records were found.", :green
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
#
|
286
|
+
# output all the information for the accounts matching
|
287
|
+
#
|
288
|
+
def show(account)
|
289
|
+
matches = @db.find(account)
|
290
|
+
if matches.size > 0 then
|
291
|
+
matches.each_with_index do |match,i|
|
292
|
+
color_puts "#{sprintf("%3d",i + 1)}. #{match.title}", :yellow
|
293
|
+
max_name_length = match.max_field_length + 1
|
294
|
+
match.each do |name,value|
|
295
|
+
next if name == "title"
|
296
|
+
next if value.length == 0
|
297
|
+
|
298
|
+
name_out = name.rjust(max_name_length)
|
299
|
+
color_print name_out, :blue
|
300
|
+
color_print " : ", :white
|
301
|
+
|
302
|
+
if match.private_field?(name) then
|
303
|
+
color_print value, :red
|
304
|
+
color_print " (press any key).", :white
|
305
|
+
junk = get_one_char
|
306
|
+
color_print "\r#{name_out}", :blue
|
307
|
+
color_print " : ", :white
|
308
|
+
color_puts "#{"*" * 20}\e[K", :red
|
309
|
+
else
|
310
|
+
color_puts value, :cyan
|
311
|
+
end
|
312
|
+
end
|
313
|
+
@stdout.puts
|
314
|
+
end
|
315
|
+
else
|
316
|
+
color_puts "No matching records were found.", :green
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
#
|
321
|
+
# Change the master password on the database
|
322
|
+
#
|
323
|
+
def master_password(ignore_this)
|
324
|
+
new_password = prompt("Enter new master password", false, true, 30)
|
325
|
+
@db.passphrase = new_password
|
326
|
+
color_puts "New master password set.", :green
|
327
|
+
end
|
328
|
+
|
329
|
+
#
|
330
|
+
# Import data into the database from a CSV file
|
331
|
+
#
|
332
|
+
def import_from_csv(file)
|
333
|
+
entries = Keybox::Convert::CSV.from_file(file)
|
334
|
+
entries.each do |entry|
|
335
|
+
@db << entry
|
336
|
+
end
|
337
|
+
color_puts "Imported #{entries.size} records from #{file}.", :green
|
338
|
+
end
|
339
|
+
|
340
|
+
#
|
341
|
+
# Export data from the database into a CSV file
|
342
|
+
def export_to_csv(file)
|
343
|
+
Keybox::Convert::CSV.to_file(@db.records, file)
|
344
|
+
color_puts "Exported #{@db.records.size} records to #{file}.", :green
|
345
|
+
end
|
346
|
+
|
347
|
+
def fill_entry(entry)
|
348
|
+
|
349
|
+
# calculate maximum prompt width for pretty output
|
350
|
+
max_length = entry.fields.collect { |f| f.length }.max
|
351
|
+
max_length += entry.values.collect { |v| v.length }.max
|
352
|
+
|
353
|
+
# now prompt for the entry items
|
354
|
+
entry.fields.each do |field|
|
355
|
+
echo = true
|
356
|
+
validate = false
|
357
|
+
default = entry.send(field)
|
358
|
+
p = "#{field} [#{default}]"
|
359
|
+
|
360
|
+
# we don't echo private field prompts and we validate
|
361
|
+
# them
|
362
|
+
if entry.private_field?(field) then
|
363
|
+
echo = false
|
364
|
+
validate = true
|
365
|
+
p = "#{field}"
|
366
|
+
end
|
367
|
+
|
368
|
+
value = prompt(p,echo,validate,max_length)
|
369
|
+
|
370
|
+
if value.nil? or value.size == 0 then
|
371
|
+
value = default
|
372
|
+
end
|
373
|
+
entry.send("#{field}=",value)
|
374
|
+
end
|
375
|
+
return entry
|
376
|
+
end
|
377
|
+
|
378
|
+
def run
|
379
|
+
begin
|
380
|
+
error_version_help
|
381
|
+
merge_options
|
382
|
+
load_database
|
383
|
+
|
384
|
+
if @actions.size == 0 then
|
385
|
+
@actions << [:list, ".*"]
|
386
|
+
end
|
387
|
+
action, param = *@actions.shift
|
388
|
+
self.send(action, param)
|
389
|
+
|
390
|
+
if @db.modified? then
|
391
|
+
color_puts "Database modified, saving.", :green
|
392
|
+
@db.save
|
393
|
+
else
|
394
|
+
color_puts "Database not modified.", :green
|
395
|
+
end
|
396
|
+
rescue SignalException => se
|
397
|
+
@stdout.puts
|
398
|
+
color_puts "Interrupted", :red
|
399
|
+
color_puts "There may be private information on your screen.", :red
|
400
|
+
color_puts "Please close this terminal.", :red
|
401
|
+
exit 1
|
402
|
+
rescue StandardError => e
|
403
|
+
@stdout.puts
|
404
|
+
color_puts "Error: #{e.message}", :red
|
405
|
+
exit 1
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|