tome 1.0.1 → 1.0.3

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,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