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.
@@ -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