keybox 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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