tome 0.1.0.pre → 0.1.1.pre

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  ## Tome
2
2
 
3
- Tome is a lightweight password manager with a humane command-line interface. It is meant to be simple and secure.
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.
4
7
 
5
8
  *Disclaimer* I am not a security expert. I've only had limited formal training in security and cryptography.
6
9
  Now that I've scared off all but the bravest, feel free to look [under the hood](#under-the-hood) or
@@ -9,13 +12,16 @@ at the security bits in [crypt.rb](https://github.com/schmich/tome/blob/master/l
9
12
  ## Installation
10
13
 
11
14
  * Requires [Ruby 1.9.3](http://www.ruby-lang.org/en/downloads/) or newer.
12
- * *Coming soon* `gem install tome`
15
+ * `gem install tome --pre`
16
+ * `tome` is now available on the command-line.
13
17
 
14
18
  ## Usage
15
19
 
16
20
  The first time you run `tome`, you'll be asked to create a master password for your encrypted password database.
17
21
  Any operations involving your password database will require this master password.
18
22
 
23
+ Creating a new password is simple:
24
+
19
25
  > tome set linkedin.com
20
26
  Creating tome database.
21
27
  Master password:
@@ -24,7 +30,7 @@ Any operations involving your password database will require this master passwor
24
30
  Password (verify):
25
31
  Created password for linkedin.com.
26
32
 
27
- Recalling a password is easy:
33
+ Recalling a password is just as easy:
28
34
 
29
35
  > tome get linkedin.com
30
36
  Master password:
@@ -1,4 +1,6 @@
1
1
  require 'tome/tome'
2
2
  require 'tome/command'
3
3
  require 'tome/crypt'
4
+ require 'tome/padding'
5
+ require 'tome/usage'
4
6
  require 'tome/version'
@@ -29,7 +29,12 @@ module Tome
29
29
  begin
30
30
  handle_command(args)
31
31
  rescue CommandError => error
32
- @err.puts error.message
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}"
33
38
  return 1
34
39
  end
35
40
 
@@ -53,9 +58,9 @@ module Tome
53
58
  commands = {
54
59
  /\A(help|-h|--help)\z/i => :help,
55
60
  /\A(version|ver|-v|--version)\z/i => :version,
56
- /\A(set|s)\z/i => :set,
61
+ /\A(set|s|add)\z/i => :set,
57
62
  /\A(get|g|show)\z/i => :get,
58
- /\A(delete|del|d|rm|remove)\z/i => :delete,
63
+ /\A(delete|del|rm|remove)\z/i => :delete,
59
64
  /\A(generate|gen)\z/i => :generate,
60
65
  /\A(copy|cp)\z/i => :copy,
61
66
  /\A(rename|ren|rn)\z/i => :rename,
@@ -340,12 +345,25 @@ module Tome
340
345
  end
341
346
 
342
347
  def input_password
343
- @in.noecho { |stdin|
344
- password = stdin.gets.sub(/[\r\n]+\z/, '')
345
- @err.puts
348
+ input = proc { |stdin|
349
+ raw = stdin.gets
350
+ return nil if raw.nil?
351
+
352
+ password = raw.strip
353
+ @out.puts
346
354
 
347
355
  return password
348
356
  }
357
+
358
+ begin
359
+ @in.noecho { |stdin|
360
+ input.call stdin
361
+ }
362
+ rescue Errno::EBADF
363
+ # This can happen when stdin refers to a file or pipe.
364
+ # In this case, we ignore 'no echo' and do normal input.
365
+ input.call @in
366
+ end
349
367
  end
350
368
 
351
369
  def prompt_confirm(prompt)
@@ -381,7 +399,12 @@ module Tome
381
399
  tome = Tome.new(@tome_filename, master_password)
382
400
  rescue MasterPasswordError
383
401
  @err.puts 'Incorrect master password.'
384
- retry
402
+
403
+ if master_password.nil?
404
+ raise CommandError, 'Authentication failed.'
405
+ else
406
+ retry
407
+ end
385
408
  end
386
409
 
387
410
  return tome
@@ -397,199 +420,4 @@ module Tome
397
420
  end
398
421
  end
399
422
  end
400
-
401
- # TODO: Complete these.
402
- $usage = <<END
403
- Usage:
404
-
405
- tome set [user@]<domain> [password]
406
-
407
- Create or update the password for an account.
408
- Example: tome set foo@gmail.com
409
-
410
- tome generate [user@]<domain>
411
-
412
- Generate a random password for an account.
413
- Example: tome generate reddit.com
414
-
415
- tome get <pattern>
416
-
417
- Show the passwords for all accounts matching the pattern.
418
- Example: tome get youtube
419
-
420
- tome copy <pattern>
421
-
422
- Copy the password for the account matching the pattern.
423
- Example: tome copy news.ycombinator.com
424
-
425
- tome list
426
-
427
- Show all stored accounts and passwords.
428
- Example: tome list
429
-
430
- tome delete [user@]<domain>
431
-
432
- Delete the password for an account.
433
- Example: tome delete foo@slashdot.org
434
-
435
- tome rename <old> <new>
436
-
437
- Rename the account information stored.
438
- Example: tome rename twitter.com foo@twitter.com
439
-
440
- tome help
441
-
442
- Shows help for a specific command.
443
- Example: tome help set
444
-
445
- tome version
446
-
447
- Shows the version of tome.
448
- Example: tome version
449
- END
450
-
451
- $help_usage = <<END
452
- tome help
453
-
454
- Shows help for a specific command.
455
-
456
- Usage:
457
-
458
- tome help
459
- tome help <command>
460
-
461
- Examples:
462
-
463
- tome help
464
- tome help set
465
- tome help help (so meta)
466
-
467
- Alias: help, --help, -h
468
- END
469
-
470
- $set_usage = <<END
471
- tome set
472
-
473
- Create or update the password for an account. The user is optional.
474
- If you do not specify a password, you will be prompted for one.
475
-
476
- Usage:
477
-
478
- tome set [user@]<domain> [password]
479
-
480
- Examples:
481
-
482
- tome set gmail.com
483
- tome set gmail.com p4ssw0rd
484
- tome set foo@gmail.com
485
- tome set foo@gmail.com p4ssw0rd
486
-
487
- Alias: set, s
488
- END
489
-
490
- $get_usage = <<END
491
- tome get
492
-
493
- Show the passwords for all accounts matching the pattern.
494
- Matching is done with substring search. Wildcards are not supported.
495
-
496
- Usage:
497
-
498
- tome get <pattern>
499
-
500
- Examples:
501
-
502
- tome get gmail
503
- tome get foo@
504
- tome get foo@gmail.com
505
-
506
- Alias: get, g, show
507
- END
508
-
509
- $delete_usage = <<END
510
- tome delete
511
-
512
- Delete the password for an account.
513
-
514
- Usage:
515
-
516
- tome delete [user@]<domain>
517
-
518
- Examples:
519
-
520
- tome delete gmail.com
521
- tome delete foo@gmail.com
522
-
523
- Alias: delete, del, d, remove, rm
524
- END
525
-
526
- $generate_usage = <<END
527
- tome generate
528
-
529
- Generate a random password for an account. The user is optional.
530
-
531
- Usage:
532
-
533
- tome generate [user@]<domain>
534
-
535
- Examples:
536
-
537
- tome generate gmail.com
538
- tome generate foo@gmail.com
539
-
540
- Alias: generate, gen
541
- END
542
-
543
- $copy_usage = <<END
544
- tome copy
545
-
546
- Copy the password for the account matching the pattern.
547
- If more than one account matches the pattern, nothing happens.
548
- Matching is done with substring search. Wildcards are not supported.
549
-
550
- Usage:
551
-
552
- tome copy <pattern>
553
-
554
- Examples:
555
-
556
- tome copy gmail
557
- tome copy foo@
558
- tome copy foo@gmail.com
559
-
560
- Alias: copy, cp
561
- END
562
-
563
- $list_usage = <<END
564
- tome list
565
-
566
- Show all stored accounts and passwords.
567
-
568
- Usage:
569
-
570
- tome list
571
-
572
- Examples:
573
-
574
- tome list
575
-
576
- Alias: list, ls
577
- END
578
-
579
- $rename_usage = <<END
580
- tome rename
581
-
582
- Rename the account information stored.
583
-
584
- Usage:
585
-
586
- tome rename <old> <new>
587
-
588
- Examples:
589
-
590
- tome rename gmail.com foo@gmail.com
591
- tome rename foo@gmail.com bar@gmail.com
592
-
593
- Alias: rename, ren, rn
594
- END
595
- end
423
+ end
@@ -0,0 +1,16 @@
1
+ require 'yaml'
2
+ require 'securerandom'
3
+
4
+ module Tome
5
+ class Padding
6
+ def self.pad(value, min_pad, max_pad)
7
+ padding = Random.rand(min_pad..max_pad)
8
+ YAML.dump(:value => value, :padding => SecureRandom.random_bytes(padding))
9
+ end
10
+
11
+ def self.unpad(inflated_value)
12
+ yaml = YAML.load(inflated_value)
13
+ yaml[:value]
14
+ end
15
+ end
16
+ end
@@ -5,17 +5,36 @@ module Tome
5
5
  class MasterPasswordError < RuntimeError
6
6
  end
7
7
 
8
+ class FileFormatError < RuntimeError
9
+ end
10
+
8
11
  class Tome
9
12
  def self.exists?(tome_filename)
10
13
  return !load_tome(tome_filename).nil?
11
14
  end
12
15
 
13
16
  def self.create!(tome_filename, master_password, stretch = 100_000)
17
+ if tome_filename.nil? || tome_filename.empty?
18
+ raise ArgumentError
19
+ end
20
+
21
+ if master_password.nil? || master_password.empty?
22
+ raise MasterPasswordError
23
+ end
24
+
14
25
  save_tome(tome_filename, new_tome(stretch), {}, master_password)
15
26
  return Tome.new(tome_filename, master_password)
16
27
  end
17
28
 
18
29
  def initialize(tome_filename, master_password)
30
+ if tome_filename.nil? || tome_filename.empty?
31
+ raise ArgumentError
32
+ end
33
+
34
+ if master_password.nil? || master_password.empty?
35
+ raise MasterPasswordError
36
+ end
37
+
19
38
  @tome_filename = tome_filename
20
39
  @master_password = master_password
21
40
 
@@ -145,23 +164,53 @@ module Tome
145
164
  end
146
165
 
147
166
  def self.load_tome(tome_filename)
167
+ if tome_filename.nil? || tome_filename.empty?
168
+ raise ArgumentError
169
+ end
170
+
148
171
  return nil if !File.exist?(tome_filename)
149
172
 
150
173
  contents = File.open(tome_filename, 'rb') { |file| file.read }
151
174
  return nil if contents.length == 0
152
175
 
153
- values = YAML.load(contents)
154
- return nil if !values
155
-
156
- # TODO: Throw if these values are nil.
157
- # TODO: Verify version number, raise if incompatible.
158
- return {
159
- :version => values[:version],
160
- :salt => values[:salt],
161
- :iv => values[:iv],
162
- :stretch => values[:stretch],
163
- :store => values[:store]
164
- }
176
+ tome = YAML.load(contents)
177
+ return nil if !tome
178
+
179
+ validate_tome(tome)
180
+
181
+ return tome
182
+ end
183
+
184
+ def self.validate_tome(tome)
185
+ if tome[:version].nil? || tome[:version].class != Fixnum
186
+ raise FileFormatError, 'The tome database is invalid (missing or invalid version).'
187
+ end
188
+
189
+ if tome[:version] > FILE_VERSION
190
+ raise FileFormatError, "The tome database comes from a newer version of tome (v#{tome[:version]} > v#{FILE_VERSION}). Try updating tome."
191
+ end
192
+
193
+ if tome[:version] < FILE_VERSION
194
+ raise FileFormatError, "The tome database is incompatible with this version of tome (v#{tome[:version]} < v#{FILE_VERSION})."
195
+ end
196
+
197
+ # TODO: Check version number, do file format migration if necessary.
198
+
199
+ if tome[:salt].nil? || tome[:salt].class != String || tome[:salt].empty?
200
+ raise FileFormatError, 'The tome database is invalid (missing or invalid salt).'
201
+ end
202
+
203
+ if tome[:iv].nil? || tome[:iv].class != String || tome[:iv].empty?
204
+ raise FileFormatError, 'The tome database is invalid (missing or invalid IV).'
205
+ end
206
+
207
+ if tome[:stretch].nil? || tome[:stretch].class != Fixnum || tome[:stretch] < 0
208
+ raise FileFormatError, 'The tome database is invalid (missing or invalid key stretch).'
209
+ end
210
+
211
+ if tome[:store].nil? || tome[:store].class != String || tome[:store].empty?
212
+ raise FileFormatError, 'The tome database is invalid (missing or invalid store).'
213
+ end
165
214
  end
166
215
 
167
216
  def load_store(tome)
@@ -170,20 +219,19 @@ module Tome
170
219
  end
171
220
 
172
221
  begin
173
- store_yaml = Crypt.decrypt(
222
+ padded_store_yaml = Crypt.decrypt(
174
223
  :value => tome[:store],
175
224
  :password => @master_password,
176
225
  :stretch => tome[:stretch],
177
226
  :salt => tome[:salt],
178
227
  :iv => tome[:iv]
179
228
  )
180
- rescue ArgumentError
181
- # TODO: Should probably be raising an error here.
182
- return {}
183
229
  rescue OpenSSL::Cipher::CipherError
184
230
  raise MasterPasswordError
185
231
  end
186
232
 
233
+ store_yaml = Padding.unpad(padded_store_yaml)
234
+
187
235
  store = YAML.load(store_yaml)
188
236
  return store || {}
189
237
  end
@@ -194,12 +242,13 @@ module Tome
194
242
  end
195
243
 
196
244
  store_yaml = YAML.dump(store)
245
+ padded_store_yaml = Padding.pad(store_yaml, 1024, 4096)
197
246
 
198
247
  new_salt = Crypt.new_salt
199
248
  new_iv = Crypt.new_iv
200
249
 
201
250
  encrypted_store = Crypt.encrypt(
202
- :value => store_yaml,
251
+ :value => padded_store_yaml,
203
252
  :password => master_password,
204
253
  :salt => new_salt,
205
254
  :iv => new_iv,
@@ -259,6 +308,6 @@ module Tome
259
308
  readable_store { }
260
309
  end
261
310
 
262
- FILE_VERSION = 1
311
+ FILE_VERSION = 2
263
312
  end
264
313
  end
@@ -0,0 +1,193 @@
1
+ $usage = <<END
2
+ Usage:
3
+
4
+ tome set [user@]<domain> [password]
5
+
6
+ Create or update the password for an account.
7
+ Example: tome set foo@gmail.com
8
+
9
+ tome generate [user@]<domain>
10
+
11
+ Generate a random password for an account.
12
+ Example: tome generate reddit.com
13
+
14
+ tome get <pattern>
15
+
16
+ Show the passwords for all accounts matching the pattern.
17
+ Example: tome get youtube
18
+
19
+ tome copy <pattern>
20
+
21
+ Copy the password for the account matching the pattern.
22
+ Example: tome copy news.ycombinator.com
23
+
24
+ tome list
25
+
26
+ Show all stored accounts and passwords.
27
+ Example: tome list
28
+
29
+ tome delete [user@]<domain>
30
+
31
+ Delete the password for an account.
32
+ Example: tome delete foo@slashdot.org
33
+
34
+ tome rename <old> <new>
35
+
36
+ Rename the account information stored.
37
+ Example: tome rename twitter.com foo@twitter.com
38
+
39
+ tome help
40
+
41
+ Shows help for a specific command.
42
+ Example: tome help set
43
+
44
+ tome version
45
+
46
+ Shows the version of tome.
47
+ Example: tome version
48
+ END
49
+
50
+ $help_usage = <<END
51
+ tome help
52
+
53
+ Shows help for a specific command.
54
+
55
+ Usage:
56
+
57
+ tome help
58
+ tome help <command>
59
+
60
+ Examples:
61
+
62
+ tome help
63
+ tome help set
64
+ tome help help (so meta)
65
+
66
+ Alias: help, --help, -h
67
+ END
68
+
69
+ $set_usage = <<END
70
+ tome set
71
+
72
+ Create or update the password for an account. The user is optional.
73
+ If you do not specify a password, you will be prompted for one.
74
+
75
+ Usage:
76
+
77
+ tome set [user@]<domain> [password]
78
+
79
+ Examples:
80
+
81
+ tome set gmail.com
82
+ tome set gmail.com p4ssw0rd
83
+ tome set foo@gmail.com
84
+ tome set foo@gmail.com p4ssw0rd
85
+
86
+ Alias: set, s, add
87
+ END
88
+
89
+ $get_usage = <<END
90
+ tome get
91
+
92
+ Show the passwords for all accounts matching the pattern.
93
+ Matching is done with substring search. Wildcards are not supported.
94
+
95
+ Usage:
96
+
97
+ tome get <pattern>
98
+
99
+ Examples:
100
+
101
+ tome get gmail
102
+ tome get foo@
103
+ tome get foo@gmail.com
104
+
105
+ Alias: get, g, show
106
+ END
107
+
108
+ $delete_usage = <<END
109
+ tome delete
110
+
111
+ Delete the password for an account.
112
+
113
+ Usage:
114
+
115
+ tome delete [user@]<domain>
116
+
117
+ Examples:
118
+
119
+ tome delete gmail.com
120
+ tome delete foo@gmail.com
121
+
122
+ Alias: delete, del, remove, rm
123
+ END
124
+
125
+ $generate_usage = <<END
126
+ tome generate
127
+
128
+ Generate a random password for an account. The user is optional.
129
+
130
+ Usage:
131
+
132
+ tome generate [user@]<domain>
133
+
134
+ Examples:
135
+
136
+ tome generate gmail.com
137
+ tome generate foo@gmail.com
138
+
139
+ Alias: generate, gen
140
+ END
141
+
142
+ $copy_usage = <<END
143
+ tome copy
144
+
145
+ Copy the password for the account matching the pattern.
146
+ If more than one account matches the pattern, nothing happens.
147
+ Matching is done with substring search. Wildcards are not supported.
148
+
149
+ Usage:
150
+
151
+ tome copy <pattern>
152
+
153
+ Examples:
154
+
155
+ tome copy gmail
156
+ tome copy foo@
157
+ tome copy foo@gmail.com
158
+
159
+ Alias: copy, cp
160
+ END
161
+
162
+ $list_usage = <<END
163
+ tome list
164
+
165
+ Show all stored accounts and passwords.
166
+
167
+ Usage:
168
+
169
+ tome list
170
+
171
+ Examples:
172
+
173
+ tome list
174
+
175
+ Alias: list, ls
176
+ END
177
+
178
+ $rename_usage = <<END
179
+ tome rename
180
+
181
+ Rename the account information stored.
182
+
183
+ Usage:
184
+
185
+ tome rename <old> <new>
186
+
187
+ Examples:
188
+
189
+ tome rename gmail.com foo@gmail.com
190
+ tome rename foo@gmail.com bar@gmail.com
191
+
192
+ Alias: rename, ren, rn
193
+ END
@@ -1 +1 @@
1
- $version = '0.1.0.pre'
1
+ $version = '0.1.1.pre'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tome
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre
4
+ version: 0.1.1.pre
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -69,7 +69,9 @@ extra_rdoc_files: []
69
69
  files:
70
70
  - lib/tome/command.rb
71
71
  - lib/tome/crypt.rb
72
+ - lib/tome/padding.rb
72
73
  - lib/tome/tome.rb
74
+ - lib/tome/usage.rb
73
75
  - lib/tome/version.rb
74
76
  - lib/tome.rb
75
77
  - bin/tome