tome 1.0.1 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6043d9572fcacc2fc1ce08e196b43741cb7d7e8f
4
+ data.tar.gz: 18d884c8f681e3410d08c1dedda61a807a71e00f
5
+ SHA512:
6
+ metadata.gz: ba3dfb8372777f1663d9e774fdd619f227b3dec1145c055b2e6199a38803a7f7dcb7ad73f163f72b0677ec1b9f314e869015f8f777a49c22b6baa985dc95d43a
7
+ data.tar.gz: 140a796fd311a64abe09896b509d761371d9e93d20d9263bec5415ef2e8b81973422fa2fc480fc70f5b4a0c82c49279e0a444e62766bb2b23b9a4579fb3dcaf2
data/README.md CHANGED
@@ -1,121 +1,128 @@
1
- ## Tome
2
-
3
- Tome is a lightweight password manager with a humane command-line interface.
4
-
5
- Tome stores your passwords in an encrypted file which you manage with a single master password.
6
- You can keep track of multiple complex passwords without having to remember any of them.
7
-
8
- *Disclaimer* I am not a security expert. I've only had limited formal training in security and cryptography.
9
- Now that I've scared off all but the bravest, feel free to look [under the hood](#under-the-hood) or
10
- at the security bits in [crypt.rb](https://github.com/schmich/tome/blob/master/lib/tome/crypt.rb).
11
-
12
- [![Build Status](https://secure.travis-ci.org/schmich/tome.png)](http://travis-ci.org/schmich/tome)
13
- [![Dependency Status](https://gemnasium.com/schmich/tome.png)](http://gemnasium.com/schmich/tome)
14
- [![Code Quality](https://codeclimate.com/badge.png)](https://codeclimate.com/github/schmich/tome)
15
-
16
- ## Installation
17
-
18
- * Install [Ruby 1.9.3](http://www.ruby-lang.org/en/downloads/) or newer.
19
- * `gem install tome`
20
- * `tome` should now be available on the command-line. Run `tome help` to get started.
21
-
22
- ## Usage
23
-
24
- The first time you run `tome`, you'll be asked to create a master password for your encrypted password database.
25
- Any operations involving your password database will require this master password.
26
-
27
- Creating a new password is simple:
28
-
29
- > tome set linkedin.com
30
- Creating tome database.
31
- Master password:
32
- Master password (verify):
33
- Password:
34
- Password (verify):
35
- Created password for linkedin.com.
36
-
37
- Recalling a password is just as easy:
38
-
39
- > tome get linkedin.com
40
- Master password:
41
- Password for linkedin.com:
42
- p4ssw0rd
43
-
44
- In fact, it's even simpler than that. `tome get` does substring pattern matching to recall a password,
45
- so this works, too:
46
-
47
- > tome get linked
48
- Master password:
49
- Password for linkedin.com:
50
- p4ssw0rd
51
-
52
- You can also generate and copy complex passwords without having to remember anything:
53
-
54
- > tome generate last.fm
55
- Master password:
56
- Generated password for last.fm.
57
-
58
- > tome get last
59
- Master password:
60
- Password for last.fm:
61
- kizWy76F2@G(21c11(9Tf?f@43B!kq
62
-
63
- > tome copy last
64
- Master password:
65
- Password for last.fm copied to clipboard.
66
-
67
- If you want, you can specify a username with your domain:
68
-
69
- > tome set foo@bar.com baz
70
- Master password:
71
- Created password for foo@bar.com.
72
-
73
- > tome get bar
74
- Master password:
75
- Password for foo@bar.com:
76
- baz
77
-
78
- See `tome help` for advanced commands and usage.
79
-
80
- ## Philosophy
81
-
82
- Tome is meant to be simple and secure. Instead of having blind trust in the secure coding practices
83
- of every website you sign up with, you can use tome to help mitigate your risk and exposure.
84
-
85
- **Benefits**
86
- * Easily maintain unique per-site passwords.
87
- * Have complex passwords without having to remember them (see `tome generate`).
88
- * If a website leaks your password or its hash, you can quickly generate another unique complex password.
89
- * You can keep track of all of the various websites you have accounts with.
90
-
91
- **Drawbacks**
92
- * Single point of failure: if your `.tome` file is compromised, all of your passwords are potentially at risk.
93
- The encryption on the `.tome` file is meant to mitigate this danger. Brute-force decryption should take significant
94
- computing power and time. To further reduce risk, don't store usernames (e.g. do `tome set gmail.com` instead of `tome set foo@gmail.com`).
95
- * Dependence on the `.tome` file: if your `.tome` file is lost or corrupt and you forget your passwords, you'll have to reset them.
96
- * If you want access to your passwords on multiple machines, you'll have to sync the `.tome` file between machines.
97
- * Trust in *my* secure coding practices. I encourage you to look at the source yourself.
98
-
99
- ## Under the hood
100
-
101
- All account and password information is stored in a single `.tome` file in the user's home directory. This file is
102
- YAML-formatted and stores the encrypted account and password information as well as the encryption parameters.
103
- These encryption parameters, along with the master password, are used to decrypt the password information.
104
-
105
- A randomly-generated 1K-4K block of data is appended to the actual password data to obfuscate the number of passwords
106
- stored in the database. This is not a security mechanism, but rather a hindrance to attempts to infer
107
- anything from the encrypted data.
108
-
109
- Each time the `.tome` file is modified, new encryption parameters (i.e. the salt and IV) are randomly generated
110
- and used for encryption.
111
-
112
- **Password database encryption**
113
- * Encryption algorithm: symmetric [AES-256](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard)
114
- [CBC](http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher-block_chaining_.28CBC.29)
115
- using the [Ruby OpenSSL library](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/index.html).
116
- * Key derivation:
117
- * [PBKDF2](http://en.wikipedia.org/wiki/PBKDF2)/[HMAC-SHA-512](http://en.wikipedia.org/wiki/SHA-2) with a master password.
118
- * [UUID](http://en.wikipedia.org/wiki/UUID)-based random, probabilistically unique [salt](http://en.wikipedia.org/wiki/Salt_%28cryptography%29)
119
- from [SecureRandom#uuid](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/securerandom/rdoc/SecureRandom.html#method-c-uuid).
120
- * Randomly-generated [IV](http://en.wikipedia.org/wiki/Initialization_vector) from [OpenSSL::Cipher#random_iv](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/Cipher.html#method-i-random_iv).
121
- * 100,000 [key stretch](http://en.wikipedia.org/wiki/Key_stretching) iterations.
1
+ ## Tome
2
+
3
+ Tome is a lightweight password manager with a humane command-line interface.
4
+
5
+ Tome stores your passwords in an encrypted file which you manage with a single master password.
6
+ You can keep track of multiple complex passwords without having to remember any of them.
7
+
8
+ *Disclaimer* I am not a security expert. I've only had limited formal training in security and cryptography.
9
+ Now that I've scared off all but the bravest, feel free to look [under the hood](#under-the-hood) or
10
+ at the security bits in [crypt.rb](https://github.com/schmich/tome/blob/master/lib/tome/crypt.rb).
11
+
12
+ [![Gem Version](https://badge.fury.io/rb/tome.svg)](http://rubygems.org/gems/tome)
13
+ [![Build Status](https://secure.travis-ci.org/schmich/tome.svg)](http://travis-ci.org/schmich/tome)
14
+ [![Dependency Status](https://gemnasium.com/schmich/tome.svg)](http://gemnasium.com/schmich/tome)
15
+ [![Code Quality](http://img.shields.io/codeclimate/github/schmich/tome.svg)](https://codeclimate.com/github/schmich/tome)
16
+
17
+ ## Installation
18
+
19
+ * Install [Ruby 1.9.3](http://www.ruby-lang.org/en/downloads/) or newer.
20
+ * `gem install tome`
21
+ * `tome` should now be available on the command-line. Run `tome help` to get started.
22
+
23
+ ## Usage
24
+
25
+ The first time you run `tome`, you'll be asked to create a master password for your encrypted password database.
26
+ Any operations involving your password database will require this master password.
27
+
28
+ Creating a new password is simple:
29
+
30
+ > tome set linkedin.com
31
+ Creating tome database.
32
+ Master password:
33
+ Master password (verify):
34
+ Password:
35
+ Password (verify):
36
+ Created password for linkedin.com.
37
+
38
+ Recalling a password is just as easy:
39
+
40
+ > tome get linkedin.com
41
+ Master password:
42
+ Password for linkedin.com:
43
+ p4ssw0rd
44
+
45
+ In fact, it's even simpler than that. `tome get` does substring pattern matching to recall a password,
46
+ so this works, too:
47
+
48
+ > tome get linked
49
+ Master password:
50
+ Password for linkedin.com:
51
+ p4ssw0rd
52
+
53
+ You can also generate and copy complex passwords without having to remember anything:
54
+
55
+ > tome generate last.fm
56
+ Master password:
57
+ Generated and copied password for last.fm.
58
+
59
+ > tome get last
60
+ Master password:
61
+ Password for last.fm:
62
+ kizWy76F2@G(21c11(9Tf?f@43B!kq
63
+
64
+ > tome copy last
65
+ Master password:
66
+ Password for last.fm copied to clipboard.
67
+
68
+ If you want, you can specify a username with your domain:
69
+
70
+ > tome set foo@bar.com baz
71
+ Master password:
72
+ Created password for foo@bar.com.
73
+
74
+ > tome get bar
75
+ Master password:
76
+ Password for foo@bar.com:
77
+ baz
78
+
79
+ See `tome help` for advanced commands and usage.
80
+
81
+ ## Philosophy
82
+
83
+ Tome is meant to be simple and secure. Instead of having blind trust in the secure coding practices
84
+ of every website you sign up with, you can use tome to help mitigate your risk and exposure.
85
+
86
+ **Benefits**
87
+ * Easily maintain unique per-site passwords.
88
+ * Have complex passwords without having to remember them (see `tome generate`).
89
+ * If a website leaks your password or its hash, you can quickly generate another unique complex password.
90
+ * You can keep track of all of the various websites you have accounts with.
91
+
92
+ **Drawbacks**
93
+ * Single point of failure: if your `.tome` file is compromised, all of your passwords are potentially at risk.
94
+ The encryption on the `.tome` file is meant to mitigate this danger. Brute-force decryption should take significant
95
+ computing power and time. To further reduce risk, don't store usernames (e.g. do `tome set gmail.com` instead of `tome set foo@gmail.com`).
96
+ * Dependence on the `.tome` file: if your `.tome` file is lost or corrupt and you forget your passwords, you'll have to reset them.
97
+ * If you want access to your passwords on multiple machines, you'll have to sync the `.tome` file between machines.
98
+ * Trust in *my* secure coding practices. I encourage you to look at the source yourself.
99
+
100
+ ## Under the hood
101
+
102
+ All account and password information is stored in a single `.tome` file in the user's home directory. This file is
103
+ YAML-formatted and stores the encrypted account and password information as well as the encryption parameters.
104
+ These encryption parameters, along with the master password, are used to decrypt the password information.
105
+
106
+ A randomly-generated 1K-4K block of data is appended to the actual password data to obfuscate the number of passwords
107
+ stored in the database. This is not a security mechanism, but rather a hindrance to attempts to infer
108
+ anything from the encrypted data.
109
+
110
+ Each time the `.tome` file is modified, new encryption parameters (i.e. the salt and IV) are randomly generated
111
+ and used for encryption.
112
+
113
+ **Password database encryption**
114
+ * Encryption algorithm: symmetric [AES-256](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard)
115
+ [CBC](http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher-block_chaining_.28CBC.29)
116
+ using the [Ruby OpenSSL library](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/index.html).
117
+ * Key derivation:
118
+ * [PBKDF2](http://en.wikipedia.org/wiki/PBKDF2)/[HMAC-SHA-512](http://en.wikipedia.org/wiki/SHA-2) with a master password.
119
+ * [UUID](http://en.wikipedia.org/wiki/UUID)-based random, probabilistically unique [salt](http://en.wikipedia.org/wiki/Salt_%28cryptography%29)
120
+ from [SecureRandom#uuid](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/securerandom/rdoc/SecureRandom.html#method-c-uuid).
121
+ * Randomly-generated [IV](http://en.wikipedia.org/wiki/Initialization_vector) from [OpenSSL::Cipher#random_iv](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/Cipher.html#method-i-random_iv).
122
+ * 100,000 [key stretch](http://en.wikipedia.org/wiki/Key_stretching) iterations.
123
+
124
+ ## License
125
+
126
+ Copyright © 2013-2014 Chris Schmich
127
+ <br />
128
+ MIT License, see LICENSE for details.
data/bin/tome CHANGED
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'tome'
4
- tome_filename = File.join(Dir.home, '.tome')
5
- exit(Tome::Command.run(tome_filename, ARGV))
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'tome'
4
+ tome_filename = File.join(Dir.home, '.tome')
5
+ exit(Tome::Command.run(tome_filename, ARGV))
@@ -1,6 +1,6 @@
1
- require 'tome/tome'
2
- require 'tome/command'
3
- require 'tome/crypt'
4
- require 'tome/padding'
5
- require 'tome/usage'
6
- require 'tome/version'
1
+ require 'tome/tome'
2
+ require 'tome/command'
3
+ require 'tome/crypt'
4
+ require 'tome/padding'
5
+ require 'tome/usage'
6
+ require 'tome/version'
@@ -1,440 +1,460 @@
1
- require 'io/console'
2
- require 'passgen'
3
- require 'clipboard'
4
-
5
- module Tome
6
- class CommandError < RuntimeError
7
- end
8
-
9
- class Command
10
- private_class_method :new
11
-
12
- def self.run(tome_filename, args, stdout = $stdout, stderr = $stderr, stdin = $stdin)
13
- command = new()
14
- return command.send(:run, tome_filename, args, stdout, stderr, stdin)
15
- end
16
-
17
- private
18
- def run(tome_filename, args, stdout, stderr, stdin)
19
- @out = stdout
20
- @err = stderr
21
- @in = stdin
22
- @tome_filename = tome_filename
23
-
24
- if args.length < 1
25
- usage()
26
- return 1
27
- end
28
-
29
- begin
30
- handle_command(args)
31
- rescue CommandError => error
32
- @err.puts "Error: #{error.message}"
33
- return 1
34
- rescue FileFormatError => error
35
- # Fix file separators for Windows.
36
- filename = @tome_filename.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
37
- @err.puts "Error: Cannot read #{filename}: #{error.message}"
38
- return 1
39
- end
40
-
41
- return 0
42
- end
43
-
44
- def handle_command(args)
45
- # TODO: Handle 'command --help', e.g. 'tome set --help'.
46
-
47
- command = command_from_arg(args[0])
48
-
49
- if command.nil?
50
- raise CommandError, "Unrecognized command: #{args[0]}.\n\n#{$usage}"
51
- end
52
-
53
- args.shift
54
- send(command, args)
55
- end
56
-
57
- def command_from_arg(arg)
58
- commands = {
59
- /\A(help|-h|--help)\z/i => :help,
60
- /\A(version|ver|-v|--version)\z/i => :version,
61
- /\A(set|s|add)\z/i => :set,
62
- /\A(get|g|show)\z/i => :get,
63
- /\A(delete|del|rm|remove)\z/i => :delete,
64
- /\A(generate|gen)\z/i => :generate,
65
- /\A(copy|cp)\z/i => :copy,
66
- /\A(rename|ren|rn)\z/i => :rename,
67
- /\A(master)\z/i => :master,
68
- /\A(list|ls)\z/i => :list
69
- }
70
-
71
- commands.each { |pattern, command|
72
- return command if arg =~ pattern
73
- }
74
-
75
- return nil
76
- end
77
-
78
- def help(args)
79
- if args.length > 1
80
- raise CommandError, "Invalid arguments.\n\n#{$usage}"
81
- end
82
-
83
- if args.empty?
84
- usage()
85
- return
86
- end
87
-
88
- command = command_from_arg(args[0])
89
-
90
- if command.nil?
91
- raise CommandError, "No help for unrecognized command: #{args[0]}.\n\n#{$usage}"
92
- end
93
-
94
- help = {
95
- :help => $help_usage,
96
- :set => $set_usage,
97
- :get => $get_usage,
98
- :delete => $delete_usage,
99
- :generate => $generate_usage,
100
- :copy => $copy_usage,
101
- :rename => $rename_usage,
102
- :master => $master_usage,
103
- :list => $list_usage
104
- }
105
-
106
- usage = help[command]
107
- if usage.nil?
108
- raise CommandError, "No help available for command: #{args[0]}."
109
- end
110
-
111
- @out.puts usage
112
- end
113
-
114
- def version(args)
115
- @out.puts "tome version #{$version}"
116
- end
117
-
118
- def set(args)
119
- if args.length < 1 || args.length > 2
120
- raise CommandError, "Invalid arguments.\n\n#{$set_usage}"
121
- end
122
-
123
- created, tome = tome_create_connect()
124
-
125
- case args.length
126
- # TODO: Validate that first argument is in [username@]domain form.
127
-
128
- # tome set bar.com
129
- # tome set foo@bar.com
130
- when 1
131
- id = args[0]
132
- password = prompt_password()
133
-
134
- # tome set bar.com p4ssw0rd
135
- # tome set foo@bar.com p4ssw0rd
136
- when 2
137
- id = args[0]
138
- password = args[1]
139
- end
140
-
141
- exists = !tome.get(id).nil?
142
- if exists
143
- confirm = prompt_confirm("A password already exists for #{id}. Overwrite (y/n)? ")
144
- if !confirm
145
- raise CommandError, 'Aborted.'
146
- end
147
- end
148
-
149
- created = tome.set(id, password)
150
- if created
151
- @out.print 'Created '
152
- else
153
- @out.print 'Updated '
154
- end
155
-
156
- @out.puts "password for #{id}."
157
- end
158
-
159
- def get(args)
160
- if args.length != 1
161
- raise CommandError, "Invalid arguments.\n\n#{$get_usage}"
162
- end
163
-
164
- # tome get bar.com
165
- # tome get foo@bar.com
166
- pattern = args[0]
167
-
168
- tome = tome_connect()
169
- matches = tome.find(pattern)
170
-
171
- if matches.empty?
172
- raise CommandError, "No password found for #{pattern}."
173
- elsif matches.count == 1
174
- match = matches.first
175
- @out.puts "Password for #{match.first}:"
176
- @out.puts match.last
177
- else
178
- @out.puts "Multiple matches for #{pattern}:"
179
- matches.each { |key, password|
180
- @out.puts "#{key}: #{password}"
181
- }
182
- end
183
- end
184
-
185
- def delete(args)
186
- if args.length != 1
187
- raise CommandError, "Invalid arguments.\n\n#{$delete_usage}"
188
- end
189
-
190
- tome = tome_connect()
191
-
192
- # tome del bar.com
193
- # tome del foo@bar.com
194
- id = args[0]
195
-
196
- exists = !tome.get(id).nil?
197
- if exists
198
- confirmed = prompt_confirm("Are you sure you want to delete the password for #{id} (y/n)? ")
199
- if !confirmed
200
- raise CommandError, 'Aborted.'
201
- end
202
- end
203
-
204
- deleted = tome.delete(id)
205
-
206
- if deleted
207
- @out.puts "Deleted password for #{id}."
208
- else
209
- @out.puts "No password found for #{id}."
210
- end
211
- end
212
-
213
- def generate(args)
214
- if args.length != 1
215
- raise CommandError, "Invalid arguments.\n\n#{$generate_usage}"
216
- end
217
-
218
- created, tome = tome_create_connect()
219
-
220
- # tome gen bar.com
221
- # tome gen foo@bar.com
222
- id = args[0]
223
- password = generate_password()
224
-
225
- exists = !tome.get(id).nil?
226
- if exists
227
- confirm = prompt_confirm("A password already exists for #{id}. Overwrite (y/n)? ")
228
- if !confirm
229
- raise CommandError, 'Aborted.'
230
- end
231
- end
232
-
233
- created = tome.set(id, password)
234
- Clipboard.copy(password)
235
-
236
- if created
237
- @out.puts "Generated and copied password for #{id}."
238
- else
239
- @out.puts "Updated and copied password for #{id}."
240
- end
241
- end
242
-
243
- def copy(args)
244
- if args.length != 1
245
- raise CommandError, "Invalid arguments.\n\n#{$copy_usage}"
246
- end
247
-
248
- # tome cp bar.com
249
- # tome cp foo@bar.com
250
- pattern = args[0]
251
-
252
- tome = tome_connect()
253
- matches = tome.find(pattern)
254
-
255
- if matches.empty?
256
- raise CommandError, "No password found for #{pattern}."
257
- elsif matches.count > 1
258
- message = "Found multiple matches for #{pattern}. Did you mean one of the following?\n\n"
259
- error.matches.each { |match|
260
- message += "\t#{match}\n"
261
- }
262
-
263
- raise CommandError, message
264
- else
265
- match = matches.first
266
- password = match.last
267
-
268
- Clipboard.copy(password)
269
- if Clipboard.paste == password
270
- @out.puts "Password for #{match.first} copied to clipboard."
271
- else
272
- @err.puts "Failed to copy password for #{match.first} to clipboard."
273
- end
274
- end
275
- end
276
-
277
- def list(args)
278
- if !args.empty?
279
- raise CommandError, "Invalid arguments.\n\n#{$list_usage}"
280
- end
281
-
282
- tome = tome_connect()
283
-
284
- count = 0
285
- tome.each_password { |id, password|
286
- @out.puts "#{id}: #{password}"
287
- count += 1
288
- }
289
-
290
- if count == 0
291
- @out.puts 'No passwords stored.'
292
- end
293
- end
294
-
295
- def rename(args)
296
- if args.count != 2
297
- raise CommandError, "Invalid arguments.\n\n#{$rename_usage}"
298
- end
299
-
300
- tome = tome_connect()
301
-
302
- old_id = args[0]
303
- new_id = args[1]
304
-
305
- overwriting = !tome.get(new_id).nil?
306
- if overwriting
307
- confirm = prompt_confirm("A password already exists for #{new_id}. Overwrite (y/n)? ")
308
- if !confirm
309
- raise CommandError, 'Aborted.'
310
- end
311
- end
312
-
313
- renamed = tome.rename(old_id, new_id)
314
-
315
- if !renamed
316
- raise CommandError, "#{old_id} does not exist."
317
- else
318
- @out.puts "#{old_id} renamed to #{new_id}."
319
- end
320
- end
321
-
322
- def master(args)
323
- if args.count > 0
324
- raise CommandError, "Invalid arguments.\n\n#{$master_usage}"
325
- end
326
-
327
- created, tome = tome_create_connect()
328
-
329
- if !created
330
- master_password = prompt_password('New master password')
331
- tome.master_password = master_password
332
- @out.puts 'Master password updated.'
333
- end
334
- end
335
-
336
- def generate_password
337
- Passgen.generate(:length => 30, :symbols => true)
338
- end
339
-
340
- def prompt_password(prompt = 'Password')
341
- begin
342
- @err.print "#{prompt}: "
343
- password = input_password()
344
-
345
- if password.empty?
346
- @err.puts 'Password cannot be blank.'
347
- raise
348
- end
349
-
350
- @err.print "#{prompt} (verify): "
351
- verify = input_password()
352
-
353
- if verify != password
354
- @err.puts 'Passwords do not match.'
355
- raise
356
- end
357
- rescue
358
- retry
359
- end
360
-
361
- return password
362
- end
363
-
364
- def input_password
365
- input = proc { |stdin|
366
- raw = stdin.gets
367
- return nil if raw.nil?
368
-
369
- password = raw.strip
370
- @out.puts
371
-
372
- return password
373
- }
374
-
375
- begin
376
- @in.noecho { |stdin|
377
- input.call stdin
378
- }
379
- rescue Errno::EBADF
380
- # This can happen when stdin refers to a file or pipe.
381
- # In this case, we ignore 'no echo' and do normal input.
382
- input.call @in
383
- end
384
- end
385
-
386
- def prompt_confirm(prompt)
387
- begin
388
- @out.print prompt
389
-
390
- confirm = @in.gets.strip
391
-
392
- if confirm =~ /\Ay/i
393
- return true
394
- elsif confirm =~ /\An/i
395
- return false
396
- end
397
- rescue
398
- retry
399
- end
400
- end
401
-
402
- def usage
403
- @err.puts "tome version #{$version}"
404
- @err.puts
405
- @err.puts $usage
406
- end
407
-
408
- def tome_connect
409
- if !Tome.exists?(@tome_filename)
410
- raise CommandError, "Tome database does not exist. Use 'tome set' or 'tome generate' to create a password first."
411
- end
412
-
413
- begin
414
- @err.print 'Master password: '
415
- master_password = input_password()
416
- tome = Tome.new(@tome_filename, master_password)
417
- rescue MasterPasswordError
418
- @err.puts 'Incorrect master password.'
419
-
420
- if master_password.nil?
421
- raise CommandError, 'Authentication failed.'
422
- else
423
- retry
424
- end
425
- end
426
-
427
- return tome
428
- end
429
-
430
- def tome_create_connect
431
- if !Tome.exists?(@tome_filename)
432
- @out.puts 'Creating tome database.'
433
- master_password = prompt_password('Master password')
434
- return true, Tome.create!(@tome_filename, master_password)
435
- else
436
- return false, tome_connect()
437
- end
438
- end
439
- end
440
- end
1
+ require 'io/console'
2
+ require 'passgen'
3
+ require 'clipboard'
4
+
5
+ module Tome
6
+ class CommandError < RuntimeError
7
+ end
8
+
9
+ class Command
10
+ private_class_method :new
11
+
12
+ def self.run(tome_filename, args, stdout = $stdout, stderr = $stderr, stdin = $stdin)
13
+ command = new()
14
+ return command.send(:run, tome_filename, args, stdout, stderr, stdin)
15
+ end
16
+
17
+ private
18
+ def run(tome_filename, args, stdout, stderr, stdin)
19
+ @out = stdout
20
+ @err = stderr
21
+ @in = stdin
22
+ @tome_filename = tome_filename
23
+
24
+ if args.length < 1
25
+ usage()
26
+ return 1
27
+ end
28
+
29
+ begin
30
+ handle_command(args)
31
+ rescue CommandError => error
32
+ @err.puts "Error: #{error.message}"
33
+ return 1
34
+ rescue FileFormatError => error
35
+ # Fix file separators for Windows.
36
+ filename = @tome_filename.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
37
+ @err.puts "Error: Cannot read #{filename}: #{error.message}"
38
+ return 1
39
+ end
40
+
41
+ return 0
42
+ end
43
+
44
+ def handle_command(args)
45
+ # TODO: Handle 'command --help', e.g. 'tome set --help'.
46
+
47
+ command = command_from_arg(args[0])
48
+
49
+ if command.nil?
50
+ raise CommandError, "Unrecognized command: #{args[0]}.\n\n#{$usage}"
51
+ end
52
+
53
+ args.shift
54
+ send(command, args)
55
+ end
56
+
57
+ def command_from_arg(arg)
58
+ commands = {
59
+ /\A(help|-h|--help)\z/i => :help,
60
+ /\A(version|ver|-v|--version)\z/i => :version,
61
+ /\A(set|s|add)\z/i => :set,
62
+ /\A(get|g|show)\z/i => :get,
63
+ /\A(delete|del|rm|remove)\z/i => :delete,
64
+ /\A(generate|gen)\z/i => :generate,
65
+ /\A(copy|cp)\z/i => :copy,
66
+ /\A(rename|ren|rn)\z/i => :rename,
67
+ /\A(master)\z/i => :master,
68
+ /\A(list|ls)\z/i => :list
69
+ }
70
+
71
+ commands.each { |pattern, command|
72
+ return command if arg =~ pattern
73
+ }
74
+
75
+ return nil
76
+ end
77
+
78
+ def help(args)
79
+ if args.length > 1
80
+ raise CommandError, "Invalid arguments.\n\n#{$usage}"
81
+ end
82
+
83
+ if args.empty?
84
+ usage()
85
+ return
86
+ end
87
+
88
+ command = command_from_arg(args[0])
89
+
90
+ if command.nil?
91
+ raise CommandError, "No help for unrecognized command: #{args[0]}.\n\n#{$usage}"
92
+ end
93
+
94
+ help = {
95
+ :help => $help_usage,
96
+ :set => $set_usage,
97
+ :get => $get_usage,
98
+ :delete => $delete_usage,
99
+ :generate => $generate_usage,
100
+ :copy => $copy_usage,
101
+ :rename => $rename_usage,
102
+ :master => $master_usage,
103
+ :list => $list_usage
104
+ }
105
+
106
+ usage = help[command]
107
+ if usage.nil?
108
+ raise CommandError, "No help available for command: #{args[0]}."
109
+ end
110
+
111
+ @out.puts usage
112
+ end
113
+
114
+ def version(args)
115
+ @out.puts "tome version #{VERSION}"
116
+ end
117
+
118
+ def set(args)
119
+ if args.length < 1 || args.length > 2
120
+ raise CommandError, "Invalid arguments.\n\n#{$set_usage}"
121
+ end
122
+
123
+ created, tome = tome_create_connect()
124
+
125
+ case args.length
126
+ # TODO: Validate that first argument is in [username@]domain form.
127
+
128
+ # tome set bar.com
129
+ # tome set foo@bar.com
130
+ when 1
131
+ id = args[0]
132
+ password = prompt_password()
133
+
134
+ # tome set bar.com p4ssw0rd
135
+ # tome set foo@bar.com p4ssw0rd
136
+ when 2
137
+ id = args[0]
138
+ password = args[1]
139
+ end
140
+
141
+ exists = !tome.get(id).nil?
142
+ if exists
143
+ confirm = prompt_confirm("A password already exists for #{id}. Overwrite (y/n)? ")
144
+ if !confirm
145
+ raise CommandError, 'Aborted.'
146
+ end
147
+ end
148
+
149
+ created = tome.set(id, password)
150
+ if created
151
+ @out.print 'Created '
152
+ else
153
+ @out.print 'Updated '
154
+ end
155
+
156
+ @out.puts "password for #{id}."
157
+ end
158
+
159
+ def get(args)
160
+ if args.length != 1
161
+ raise CommandError, "Invalid arguments.\n\n#{$get_usage}"
162
+ end
163
+
164
+ # tome get bar.com
165
+ # tome get foo@bar.com
166
+ pattern = args[0]
167
+
168
+ tome = tome_connect()
169
+ matches = tome.find(pattern)
170
+
171
+ if matches.empty?
172
+ raise CommandError, "No password found for #{pattern}."
173
+ elsif matches.count == 1
174
+ match = matches.first
175
+ @out.puts "Password for #{match.first}:"
176
+ @out.puts match.last
177
+ else
178
+ @out.puts "Multiple matches for #{pattern}:"
179
+ matches.each { |key, password|
180
+ @out.puts "#{key}: #{password}"
181
+ }
182
+ end
183
+ end
184
+
185
+ def delete(args)
186
+ if args.length != 1
187
+ raise CommandError, "Invalid arguments.\n\n#{$delete_usage}"
188
+ end
189
+
190
+ tome = tome_connect()
191
+
192
+ # tome del bar.com
193
+ # tome del foo@bar.com
194
+ id = args[0]
195
+
196
+ exists = !tome.get(id).nil?
197
+ if exists
198
+ confirmed = prompt_confirm("Are you sure you want to delete the password for #{id} (y/n)? ")
199
+ if !confirmed
200
+ raise CommandError, 'Aborted.'
201
+ end
202
+ end
203
+
204
+ deleted = tome.delete(id)
205
+
206
+ if deleted
207
+ @out.puts "Deleted password for #{id}."
208
+ else
209
+ @out.puts "No password found for #{id}."
210
+ end
211
+ end
212
+
213
+ def generate(args)
214
+ if args.length != 1
215
+ raise CommandError, "Invalid arguments.\n\n#{$generate_usage}"
216
+ end
217
+
218
+ created, tome = tome_create_connect()
219
+
220
+ # tome gen bar.com
221
+ # tome gen foo@bar.com
222
+ id = args[0]
223
+ password = generate_password()
224
+
225
+ exists = !tome.get(id).nil?
226
+ if exists
227
+ confirm = prompt_confirm("A password already exists for #{id}. Overwrite (y/n)? ")
228
+ if !confirm
229
+ raise CommandError, 'Aborted.'
230
+ end
231
+ end
232
+
233
+ created = tome.set(id, password)
234
+ Clipboard.copy(password)
235
+
236
+ if created
237
+ @out.puts "Generated and copied password for #{id}."
238
+ else
239
+ @out.puts "Updated and copied password for #{id}."
240
+ end
241
+ end
242
+
243
+ def copy(args)
244
+ if args.length != 1
245
+ raise CommandError, "Invalid arguments.\n\n#{$copy_usage}"
246
+ end
247
+
248
+ # tome cp bar.com
249
+ # tome cp foo@bar.com
250
+ pattern = args[0]
251
+
252
+ tome = tome_connect()
253
+ matches = tome.find(pattern)
254
+
255
+ if matches.empty?
256
+ raise CommandError, "No password found for #{pattern}."
257
+ elsif matches.count > 1
258
+ match = pick_match(pattern, matches)
259
+ else
260
+ match = matches.first
261
+ end
262
+
263
+ name = match.first
264
+ password = match.last
265
+
266
+ Clipboard.copy(password)
267
+ if Clipboard.paste == password
268
+ @out.puts "Password for #{name} copied to clipboard."
269
+ else
270
+ @err.puts "Failed to copy password for #{name} to clipboard."
271
+ end
272
+ end
273
+
274
+ def pick_match(pattern, matches)
275
+ matches = matches.to_a
276
+
277
+ @out.puts "Found multiple matches for \"#{pattern}\":\n\n"
278
+ matches.each_with_index { |match, i|
279
+ @out.puts "\t#{i + 1}: #{match[0]}"
280
+ }
281
+
282
+ begin
283
+ @out.print "\n> "
284
+
285
+ index = @in.gets.to_i
286
+ if index <= 0 || index > matches.length
287
+ @out.puts 'Invalid option.'
288
+ raise
289
+ end
290
+
291
+ return matches[index - 1]
292
+ rescue
293
+ retry
294
+ end
295
+ end
296
+
297
+ def list(args)
298
+ if !args.empty?
299
+ raise CommandError, "Invalid arguments.\n\n#{$list_usage}"
300
+ end
301
+
302
+ tome = tome_connect()
303
+
304
+ count = 0
305
+ tome.each_password { |id, password|
306
+ @out.puts "#{id}: #{password}"
307
+ count += 1
308
+ }
309
+
310
+ if count == 0
311
+ @out.puts 'No passwords stored.'
312
+ end
313
+ end
314
+
315
+ def rename(args)
316
+ if args.count != 2
317
+ raise CommandError, "Invalid arguments.\n\n#{$rename_usage}"
318
+ end
319
+
320
+ tome = tome_connect()
321
+
322
+ old_id = args[0]
323
+ new_id = args[1]
324
+
325
+ overwriting = !tome.get(new_id).nil?
326
+ if overwriting
327
+ confirm = prompt_confirm("A password already exists for #{new_id}. Overwrite (y/n)? ")
328
+ if !confirm
329
+ raise CommandError, 'Aborted.'
330
+ end
331
+ end
332
+
333
+ renamed = tome.rename(old_id, new_id)
334
+
335
+ if !renamed
336
+ raise CommandError, "#{old_id} does not exist."
337
+ else
338
+ @out.puts "#{old_id} renamed to #{new_id}."
339
+ end
340
+ end
341
+
342
+ def master(args)
343
+ if args.count > 0
344
+ raise CommandError, "Invalid arguments.\n\n#{$master_usage}"
345
+ end
346
+
347
+ created, tome = tome_create_connect()
348
+
349
+ if !created
350
+ master_password = prompt_password('New master password')
351
+ tome.master_password = master_password
352
+ @out.puts 'Master password updated.'
353
+ end
354
+ end
355
+
356
+ def generate_password
357
+ Passgen.generate(:length => 30, :symbols => true)
358
+ end
359
+
360
+ def prompt_password(prompt = 'Password')
361
+ begin
362
+ @err.print "#{prompt}: "
363
+ password = input_password()
364
+
365
+ if password.empty?
366
+ @err.puts 'Password cannot be blank.'
367
+ raise
368
+ end
369
+
370
+ @err.print "#{prompt} (verify): "
371
+ verify = input_password()
372
+
373
+ if verify != password
374
+ @err.puts 'Passwords do not match.'
375
+ raise
376
+ end
377
+ rescue
378
+ retry
379
+ end
380
+
381
+ return password
382
+ end
383
+
384
+ def input_password
385
+ input = proc { |stdin|
386
+ raw = stdin.gets
387
+ return nil if raw.nil?
388
+
389
+ password = raw.strip
390
+ @out.puts
391
+
392
+ return password
393
+ }
394
+
395
+ begin
396
+ @in.noecho { |stdin|
397
+ input.call stdin
398
+ }
399
+ rescue Errno::EBADF
400
+ # This can happen when stdin refers to a file or pipe.
401
+ # In this case, we ignore 'no echo' and do normal input.
402
+ input.call @in
403
+ end
404
+ end
405
+
406
+ def prompt_confirm(prompt)
407
+ begin
408
+ @out.print prompt
409
+
410
+ confirm = @in.gets.strip
411
+
412
+ if confirm =~ /\Ay/i
413
+ return true
414
+ elsif confirm =~ /\An/i
415
+ return false
416
+ end
417
+ rescue
418
+ retry
419
+ end
420
+ end
421
+
422
+ def usage
423
+ @err.puts "tome version #{VERSION}"
424
+ @err.puts
425
+ @err.puts $usage
426
+ end
427
+
428
+ def tome_connect
429
+ if !Tome.exists?(@tome_filename)
430
+ raise CommandError, "Tome database does not exist. Use 'tome set' or 'tome generate' to create a password first."
431
+ end
432
+
433
+ begin
434
+ @err.print 'Master password: '
435
+ master_password = input_password()
436
+ tome = Tome.new(@tome_filename, master_password)
437
+ rescue MasterPasswordError
438
+ @err.puts 'Incorrect master password.'
439
+
440
+ if master_password.nil?
441
+ raise CommandError, 'Authentication failed.'
442
+ else
443
+ retry
444
+ end
445
+ end
446
+
447
+ return tome
448
+ end
449
+
450
+ def tome_create_connect
451
+ if !Tome.exists?(@tome_filename)
452
+ @out.puts 'Creating tome database.'
453
+ master_password = prompt_password('Master password')
454
+ return true, Tome.create!(@tome_filename, master_password)
455
+ else
456
+ return false, tome_connect()
457
+ end
458
+ end
459
+ end
460
+ end