mpw 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/ruby
2
2
  # MPW is a software to crypt and manage your passwords
3
- # Copyright (C) 2016 Adrien Waksberg <mpw@yae.im>
4
- #
3
+ # Copyright (C) 2017 Adrien Waksberg <mpw@yae.im>
4
+ #
5
5
  # This program is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU General Public License
7
7
  # as published by the Free Software Foundation; either version 2
8
8
  # of the License, or (at your option) any later version.
9
- #
9
+ #
10
10
  # This program is distributed in the hope that it will be useful,
11
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
12
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
13
  # GNU General Public License for more details.
14
- #
14
+ #
15
15
  # You should have received a copy of the GNU General Public License
16
16
  # along with this program; if not, write to the Free Software
17
17
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@@ -27,546 +27,554 @@ require 'mpw/item'
27
27
  require 'mpw/mpw'
28
28
 
29
29
  module MPW
30
- class Cli
31
-
32
- # Constructor
33
- # @args: config -> the config
34
- # sync -> boolean for sync or not
35
- def initialize(config, sync=true)
36
- @config = config
37
- @sync = sync
38
- end
39
-
40
- # Change a parameter int the config after init
41
- # @args: options -> param to change
42
- def set_config(options)
43
- @config.setup(options)
44
-
45
- puts "#{I18n.t('form.set_config.valid')}".green
46
- rescue Exception => e
47
- puts "#{I18n.t('display.error')} #15: #{e}".red
48
- exit 2
49
- end
50
-
51
- # Create a new config file
52
- # @args: options -> set param
53
- def setup(options)
54
- options[:lang] = options[:lang] || Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1]
55
-
56
- I18n.locale = options[:lang].to_sym
57
-
58
- @config.setup(options)
59
-
60
- load_config
61
-
62
- puts "#{I18n.t('form.setup_config.valid')}".green
63
- rescue Exception => e
64
- puts "#{I18n.t('display.error')} #8: #{e}".red
65
- exit 2
66
- end
67
-
68
- # Setup a new GPG key
69
- # @args: gpg_key -> the key name
70
- def setup_gpg_key(gpg_key)
71
- return if @config.check_gpg_key?
72
-
73
- password = ask(I18n.t('form.setup_gpg_key.password')) {|q| q.echo = false}
74
- confirm = ask(I18n.t('form.setup_gpg_key.confirm_password')) {|q| q.echo = false}
75
-
76
- if password != confirm
77
- raise I18n.t('form.setup_gpg_key.error_password')
78
- end
79
-
80
- @password = password.to_s
81
-
82
- puts I18n.t('form.setup_gpg_key.wait')
83
-
84
- @config.setup_gpg_key(@password, gpg_key)
85
-
86
- puts "#{I18n.t('form.setup_gpg_key.valid')}".green
87
- rescue Exception => e
88
- puts "#{I18n.t('display.error')} #8: #{e}".red
89
- exit 2
90
- end
91
-
92
- # Setup wallet config for sync
93
- # @args: options -> value to change
94
- def setup_wallet_config(options={})
95
- if not options[:password].nil?
96
- options[:password] = ask(I18n.t('form.setup_wallet.password')) {|q| q.echo = false}
97
- end
98
-
99
- #wallet_file = wallet.nil? ? "#{@config.wallet_dir}/default.mpw" : "#{@config.wallet_dir}/#{wallet}.mpw"
100
-
101
- @mpw = MPW.new(@config.gpg_key, @wallet_file, @password, @config.gpg_exe)
102
- @mpw.read_data
103
- @mpw.set_config(options)
104
- @mpw.write_data
105
-
106
- puts "#{I18n.t('form.setup_wallet.valid')}".green
107
- rescue Exception => e
108
- puts "#{I18n.t('display.error')} #10: #{e}".red
109
- exit 2
110
- end
111
-
112
- # List gpg keys in wallet
113
- def list_keys
114
- table_list('keys', @mpw.list_keys)
115
- end
116
-
117
- # Load config
118
- def load_config
119
- @config.load_config
120
-
121
- rescue Exception => e
122
- puts "#{I18n.t('display.error')} #10: #{e}".red
123
- exit 2
124
- end
125
-
126
- # Request the GPG password and decrypt the file
127
- def decrypt
128
- if not defined?(@mpw)
129
- @password = ask(I18n.t('display.gpg_password')) {|q| q.echo = false}
130
- @mpw = MPW.new(@config.gpg_key, @wallet_file, @password, @config.gpg_exe)
131
- end
132
-
133
- @mpw.read_data
134
- @mpw.sync if @sync
135
- rescue Exception => e
136
- puts "#{I18n.t('display.error')} #11: #{e}".red
137
- exit 2
138
- end
139
-
140
- # Format list on a table
141
- def table_list(title, list)
142
- i = 1
143
- length = 0
144
-
145
- list.each do |item|
146
- length = item.length if length < item.length
147
- end
148
- length += 7
149
-
150
- puts "\n#{I18n.t("display.#{title}")}".red
151
- print ' '
152
- length.times { print '=' }
153
- print "\n"
154
-
155
- list.each do |item|
156
- print " #{i}".cyan
157
- (3 - i.to_s.length).times { print ' ' }
158
- puts "| #{item}"
159
- i += 1
160
- end
161
-
162
- print "\n"
163
- end
164
-
165
- # Format items on a table
166
- def table_items(items=[])
167
- group = '.'
168
- i = 1
169
- length_total = 10
170
- data = { id: { length: 3, color: 'cyan' },
171
- host: { length: 9, color: 'yellow' },
172
- user: { length: 7, color: 'green' },
173
- protocol: { length: 9, color: 'white' },
174
- port: { length: 5, color: 'white' },
175
- otp: { length: 4, color: 'white' },
176
- comment: { length: 14, color: 'magenta' },
177
- }
178
-
179
- items.each do |item|
180
- data.each do |k, v|
181
- next if k == :id or k == :otp
182
-
183
- v[:length] = item.send(k.to_s).to_s.length + 3 if item.send(k.to_s).to_s.length >= v[:length]
184
- end
185
- end
186
- data[:id][:length] = items.length.to_s.length + 2 if items.length.to_s.length > data[:id][:length]
187
-
188
- data.each_value { |v| length_total += v[:length] }
189
- items.sort! { |a, b| a.group.to_s.downcase <=> b.group.to_s.downcase }
190
-
191
- items.each do |item|
192
- if group != item.group
193
- group = item.group
194
-
195
- if group.to_s.empty?
196
- puts "\n#{I18n.t('display.no_group')}".red
197
- else
198
- puts "\n#{group}".red
199
- end
200
-
201
- print ' '
202
- length_total.times { print '=' }
203
- print "\n "
204
- data.each do |k, v|
205
- case k
206
- when :id
207
- print ' ID'
208
- when :otp
209
- print '| OTP'
210
- else
211
- print "| #{k.to_s.capitalize}"
212
- end
213
-
214
- (v[:length] - k.to_s.length).times { print ' ' }
215
- end
216
- print "\n "
217
- length_total.times { print '=' }
218
- print "\n"
219
- end
220
-
221
- print " #{i}".send(data[:id][:color])
222
- (data[:id][:length] - i.to_s.length).times { print ' ' }
223
- data.each do |k, v|
224
- next if k == :id
225
-
226
- if k == :otp
227
- print '| '
228
- if item.otp; print ' X ' else 4.times { print ' ' } end
229
-
230
- next
231
- end
232
-
233
- print '| '
234
- print "#{item.send(k.to_s)}".send(v[:color])
235
- (v[:length] - item.send(k.to_s).to_s.length).times { print ' ' }
236
- end
237
- print "\n"
238
-
239
- i += 1
240
- end
241
-
242
- print "\n"
243
- end
244
-
245
- # Display the query's result
246
- # @args: options -> the option to search
247
- def list(options={})
248
- result = @mpw.list(options)
249
-
250
- if result.length == 0
251
- puts I18n.t('display.nothing')
252
- else
253
- table_items(result)
254
- end
255
- end
256
-
257
- # Get an item when multiple choice
258
- # @args: items -> array of items
259
- # @rtrn: item
260
- def get_item(items)
261
- return items[0] if items.length == 1
262
-
263
- items.sort! { |a,b| a.group.to_s.downcase <=> b.group.to_s.downcase }
264
- choice = ask(I18n.t('form.select')).to_i
265
-
266
- if choice >= 1 and choice <= items.length
267
- return items[choice-1]
268
- else
269
- return nil
270
- end
271
- end
272
-
273
- # Copy in clipboard the login and password
274
- # @args: item -> the item
275
- # clipboard -> enable clipboard
276
- def clipboard(item, clipboard=true)
277
- pid = nil
278
-
279
- # Security: force quit after 90s
280
- Thread.new do
281
- sleep 90
282
- exit
283
- end
284
-
285
- while true
286
- choice = ask(I18n.t('form.clipboard.choice')).to_s
287
-
288
- case choice
289
- when 'q', 'quit'
290
- break
291
-
292
- when 'l', 'login'
293
- if clipboard
294
- Clipboard.copy(item.user)
295
- puts I18n.t('form.clipboard.login').green
296
- else
297
- puts item.user
298
- end
299
-
300
- when 'p', 'password'
301
- if clipboard
302
- Clipboard.copy(@mpw.get_password(item.id))
303
- puts I18n.t('form.clipboard.password').yellow
304
-
305
- Thread.new do
306
- sleep 30
307
-
308
- Clipboard.clear
309
- end
310
- else
311
- puts @mpw.get_password(item.id)
312
- end
313
-
314
- when 'o', 'otp'
315
- if clipboard
316
- Clipboard.copy(@mpw.get_otp_code(item.id))
317
- else
318
- puts @mpw.get_otp_code(item.id)
319
- end
320
- puts I18n.t('form.clipboard.otp', time: @mpw.get_otp_remaining_time).yellow
321
-
322
- else
323
- puts "----- #{I18n.t('form.clipboard.help.name')} -----".cyan
324
- puts I18n.t('form.clipboard.help.login')
325
- puts I18n.t('form.clipboard.help.password')
326
- puts I18n.t('form.clipboard.help.otp_code')
327
- puts I18n.t('form.clipboard.help.quit')
328
- next
329
- end
330
- end
331
-
332
- Clipboard.clear
333
- rescue SystemExit, Interrupt
334
- Clipboard.clear
335
- end
336
-
337
- # List all wallets
338
- def list_wallet
339
- wallets = []
340
- Dir.glob("#{@config.wallet_dir}/*.mpw").each do |f|
341
- wallet = File.basename(f, '.mpw')
342
- wallet += ' *'.green if wallet == @config.default_wallet
343
- wallets << wallet
344
- end
345
-
346
- table_list('wallets', wallets)
347
- end
348
-
349
- # Display the wallet
350
- # @args: wallet -> the wallet name
351
- def get_wallet(wallet=nil)
352
- if wallet.to_s.empty?
353
- wallets = Dir.glob("#{@config.wallet_dir}/*.mpw")
354
-
355
- if wallets.length == 1
356
- @wallet_file = wallets[0]
357
- elsif not @config.default_wallet.to_s.empty?
358
- @wallet_file = "#{@config.wallet_dir}/#{@config.default_wallet}.mpw"
359
- else
360
- @wallet_file = "#{@config.wallet_dir}/default.mpw"
361
- end
362
- else
363
- @wallet_file = "#{@config.wallet_dir}/#{wallet}.mpw"
364
- end
365
- end
366
-
367
- # Add a new public key
368
- # args: key -> the key name or key file to add
369
- def add_key(key)
370
- @mpw.add_key(key)
371
- @mpw.write_data
372
- @mpw.sync(true) if @sync
373
-
374
- puts "#{I18n.t('form.add_key.valid')}".green
375
- rescue Exception => e
376
- puts "#{I18n.t('display.error')} #13: #{e}".red
377
- end
378
-
379
- # Add new public key
380
- # args: key -> the key name to delete
381
- def delete_key(key)
382
- @mpw.delete_key(key)
383
- @mpw.write_data
384
- @mpw.sync(true) if @sync
385
-
386
- puts "#{I18n.t('form.delete_key.valid')}".green
387
- rescue Exception => e
388
- puts "#{I18n.t('display.error')} #15: #{e}".red
389
- end
390
-
391
- # Text editor interface
392
- # @args: template -> template name
393
- # item -> the item to edit
394
- # password -> disable field password
395
- def text_editor(template_name, item=nil, password=false)
396
- editor = ENV['EDITOR'] || 'nano'
397
- options = {}
398
- opts = {}
399
- template_file = "#{File.expand_path('../../../templates', __FILE__)}/#{template_name}.erb"
400
- template = ERB.new(IO.read(template_file))
401
-
402
- Dir.mktmpdir do |dir|
403
- tmp_file = "#{dir}/#{template_name}.yml"
404
-
405
- File.open(tmp_file, 'w') do |f|
406
- f << template.result(binding)
407
- end
408
-
409
- system("#{editor} #{tmp_file}")
410
-
411
- opts = YAML::load_file(tmp_file)
412
- end
413
-
414
- opts.delete_if { |k,v| v.to_s.empty? }
415
-
416
- opts.each do |k,v|
417
- options[k.to_sym] = v
418
- end
419
-
420
- return options
421
- end
422
-
423
- # Form to add a new item
424
- # @args: password -> generate a random password
425
- def add(password=false)
426
- options = text_editor('add_form', nil, password)
427
- item = Item.new(options)
428
- options[:password] = MPW::password(@config.password) if password
429
-
430
- @mpw.add(item)
431
- @mpw.set_password(item.id, options[:password]) if options.has_key?(:password)
432
- @mpw.set_otp_key(item.id, options[:otp_key]) if options.has_key?(:otp_key)
433
- @mpw.write_data
434
- @mpw.sync(true) if @sync
435
-
436
- puts "#{I18n.t('form.add_item.valid')}".green
437
- rescue Exception => e
438
- puts "#{I18n.t('display.error')} #13: #{e}".red
439
- end
440
-
441
- # Update an item
442
- # @args: password -> generate a random password
443
- # options -> the option to search
444
- def update(password=false, options={})
445
- items = @mpw.list(options)
446
-
447
- if items.length == 0
448
- puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
449
- else
450
- table_items(items) if items.length > 1
451
-
452
- item = get_item(items)
453
- options = text_editor('update_form', item, password)
454
- options[:password] = MPW::password(@config.password) if password
455
-
456
- item.update(options)
457
- @mpw.set_password(item.id, options[:password]) if options.has_key?(:password)
458
- @mpw.set_otp_key(item.id, options[:otp_key]) if options.has_key?(:otp_key)
459
- @mpw.write_data
460
- @mpw.sync(true) if @sync
461
-
462
- puts "#{I18n.t('form.update_item.valid')}".green
463
- end
464
- rescue Exception => e
465
- puts "#{I18n.t('display.error')} #14: #{e}".red
466
- end
467
-
468
- # Remove an item
469
- # @args: options -> the option to search
470
- def delete(options={})
471
- items = @mpw.list(options)
472
-
473
- if items.length == 0
474
- puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
475
- else
476
- table_items(items)
477
-
478
- item = get_item(items)
479
- confirm = ask("#{I18n.t('form.delete_item.ask')} (y/N) ").to_s
480
-
481
- if not confirm =~ /^(y|yes|YES|Yes|Y)$/
482
- return false
483
- end
484
-
485
- item.delete
486
- @mpw.write_data
487
- @mpw.sync(true) if @sync
488
-
489
- puts "#{I18n.t('form.delete_item.valid')}".green
490
- end
491
- rescue Exception => e
492
- puts "#{I18n.t('display.error')} #16: #{e}".red
493
- end
494
-
495
- # Copy a password, otp, login
496
- # @args: clipboard -> enable clipboard
497
- # options -> the option to search
498
- def copy(clipboard=true, options={})
499
- items = @mpw.list(options)
500
-
501
- if items.length == 0
502
- puts I18n.t('display.nothing')
503
- else
504
- table_items(items)
505
-
506
- item = get_item(items)
507
- clipboard(item, clipboard)
508
- end
509
- rescue Exception => e
510
- puts "#{I18n.t('display.error')} #14: #{e}".red
511
- end
512
-
513
- # Export the items in a CSV file
514
- # @args: file -> the destination file
515
- # options -> option to search
516
- def export(file, options)
517
- file = 'export-mpw.yml' if file.to_s.empty?
518
- items = @mpw.list(options)
519
- data = {}
520
-
521
- items.each do |item|
522
- data.merge!(item.id => { 'host' => item.host,
523
- 'user' => item.user,
524
- 'group' => item.group,
525
- 'password' => @mpw.get_password(item.id),
526
- 'protocol' => item.protocol,
527
- 'port' => item.port,
528
- 'otp_key' => @mpw.get_otp_key(item.id),
529
- 'comment' => item.comment,
530
- 'last_edit' => item.last_edit,
531
- 'created' => item.created,
532
- }
533
- )
534
- end
535
-
536
- File.open(file, 'w') {|f| f << data.to_yaml}
537
-
538
- puts "#{I18n.t('form.export.valid', file: file)}".green
539
- rescue Exception => e
540
- puts "#{I18n.t('display.error')} #17: #{e}".red
541
- end
542
-
543
- # Import items from a YAML file
544
- # @args: file -> the import file
545
- def import(file)
546
- raise I18n.t('form.import.file_empty') if file.to_s.empty?
547
- raise I18n.t('form.import.file_not_exist') if not File.exist?(file)
548
-
549
- YAML::load_file(file).each_value do |row|
550
- item = Item.new(group: row['group'],
551
- host: row['host'],
552
- protocol: row['protocol'],
553
- user: row['user'],
554
- port: row['port'],
555
- comment: row['comment'],
556
- )
557
-
558
- next if item.empty?
559
-
560
- @mpw.add(item)
561
- @mpw.set_password(item.id, row['password']) if not row['password'].to_s.empty?
562
- @mpw.set_otp_key(item.id, row['otp_key']) if not row['otp_key'].to_s.empty?
563
- end
564
-
565
- @mpw.write_data
566
-
567
- puts "#{I18n.t('form.import.valid')}".green
568
- rescue Exception => e
569
- puts "#{I18n.t('display.error')} #18: #{e}".red
570
- end
571
- end
30
+ class Cli
31
+ # Constructor
32
+ # @args: config -> the config
33
+ def initialize(config)
34
+ @config = config
35
+ end
36
+
37
+ # Change a parameter int the config after init
38
+ # @args: options -> param to change
39
+ def set_config(options)
40
+ @config.setup(options)
41
+
42
+ puts I18n.t('form.set_config.valid').to_s.green
43
+ rescue => e
44
+ puts "#{I18n.t('display.error')} #15: #{e}".red
45
+ exit 2
46
+ end
47
+
48
+ # Change the wallet path
49
+ # @args: path -> the new path
50
+ def set_wallet_path(path)
51
+ @config.set_wallet_path(path, @wallet)
52
+
53
+ puts I18n.t('form.set_wallet_path.valid').to_s.green
54
+ rescue => e
55
+ puts "#{I18n.t('display.error')} #19: #{e}".red
56
+ exit 2
57
+ end
58
+
59
+ # Create a new config file
60
+ # @args: options -> set param
61
+ def setup(options)
62
+ options[:lang] = options[:lang] || Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1]
63
+
64
+ I18n.locale = options[:lang].to_sym
65
+
66
+ @config.setup(options)
67
+
68
+ load_config
69
+
70
+ puts I18n.t('form.setup_config.valid').to_s.green
71
+ rescue => e
72
+ puts "#{I18n.t('display.error')} #8: #{e}".red
73
+ exit 2
74
+ end
75
+
76
+ # Setup a new GPG key
77
+ # @args: gpg_key -> the key name
78
+ def setup_gpg_key(gpg_key)
79
+ return if @config.check_gpg_key?
80
+
81
+ password = ask(I18n.t('form.setup_gpg_key.password')) { |q| q.echo = false }
82
+ confirm = ask(I18n.t('form.setup_gpg_key.confirm_password')) { |q| q.echo = false }
83
+
84
+ raise I18n.t('form.setup_gpg_key.error_password') if password != confirm
85
+
86
+ @password = password.to_s
87
+
88
+ puts I18n.t('form.setup_gpg_key.wait')
89
+
90
+ @config.setup_gpg_key(@password, gpg_key)
91
+
92
+ puts I18n.t('form.setup_gpg_key.valid').to_s.green
93
+ rescue => e
94
+ puts "#{I18n.t('display.error')} #8: #{e}".red
95
+ exit 2
96
+ end
97
+
98
+ # List gpg keys in wallet
99
+ def list_keys
100
+ table_list('keys', @mpw.list_keys)
101
+ end
102
+
103
+ # List config
104
+ def list_config
105
+ config = {
106
+ 'lang' => @config.lang,
107
+ 'gpg_key' => @config.gpg_key,
108
+ 'default_wallet' => @config.default_wallet,
109
+ 'config_dir' => @config.config_dir,
110
+ 'pinmode' => @config.pinmode,
111
+ 'gpg_exe' => @config.gpg_exe
112
+ }
113
+
114
+ @config.wallet_paths.each { |k, v| config["path_wallet_#{k}"] = "#{v}/#{k}.mpw" }
115
+ @config.password.each { |k, v| config["password_#{k}"] = v }
116
+
117
+ table_list('config', config)
118
+ end
119
+
120
+ # Load config
121
+ def load_config
122
+ @config.load_config
123
+ rescue => e
124
+ puts "#{I18n.t('display.error')} #10: #{e}".red
125
+ exit 2
126
+ end
127
+
128
+ # Request the GPG password and decrypt the file
129
+ def decrypt
130
+ unless defined?(@mpw)
131
+ @password = ask(I18n.t('display.gpg_password')) { |q| q.echo = false }
132
+ @mpw = MPW.new(@config.gpg_key, @wallet_file, @password, @config.gpg_exe, @config.pinmode)
133
+ end
134
+
135
+ @mpw.read_data
136
+ rescue => e
137
+ puts "#{I18n.t('display.error')} #11: #{e}".red
138
+ exit 2
139
+ end
140
+
141
+ # Format list on a table
142
+ # @args: title -> the name of table
143
+ # list -> array or hash
144
+ def table_list(title, list)
145
+ length = { k: 0, v: 0 }
146
+
147
+ if list.is_a?(Array)
148
+ i = 0
149
+ list = list.map do |item|
150
+ i += 1
151
+ [i, item]
152
+ end.to_h
153
+ end
154
+
155
+ list.each do |k, v|
156
+ length[:k] = k.to_s.length if length[:k] < k.to_s.length
157
+ length[:v] = v.to_s.length if length[:v] < v.to_s.length
158
+ end
159
+
160
+ puts "\n#{I18n.t("display.#{title}")}".red
161
+ print ' '
162
+ (length[:k] + length[:v] + 5).times { print '=' }
163
+ print "\n"
164
+
165
+ list.each do |k, v|
166
+ print " #{k}".cyan
167
+ (length[:k] - k.to_s.length + 1).times { print ' ' }
168
+ puts "| #{v}"
169
+ end
170
+
171
+ print "\n"
172
+ end
173
+
174
+ # Format items on a table
175
+ # @args: items -> an aray items
176
+ def table_items(items = [])
177
+ group = '.'
178
+ i = 1
179
+ length_total = 10
180
+ data = { id: { length: 3, color: 'cyan' },
181
+ host: { length: 9, color: 'yellow' },
182
+ user: { length: 7, color: 'green' },
183
+ protocol: { length: 9, color: 'white' },
184
+ port: { length: 5, color: 'white' },
185
+ otp: { length: 4, color: 'white' },
186
+ comment: { length: 14, color: 'magenta' } }
187
+
188
+ items.each do |item|
189
+ data.each do |k, v|
190
+ next if k == :id || k == :otp
191
+
192
+ v[:length] = item.send(k.to_s).to_s.length + 3 if item.send(k.to_s).to_s.length >= v[:length]
193
+ end
194
+ end
195
+ data[:id][:length] = items.length.to_s.length + 2 if items.length.to_s.length > data[:id][:length]
196
+
197
+ data.each_value { |v| length_total += v[:length] }
198
+ items.sort! { |a, b| a.group.to_s.downcase <=> b.group.to_s.downcase }
199
+
200
+ items.each do |item|
201
+ if group != item.group
202
+ group = item.group
203
+
204
+ if group.to_s.empty?
205
+ puts "\n#{I18n.t('display.no_group')}".red
206
+ else
207
+ puts "\n#{group}".red
208
+ end
209
+
210
+ print ' '
211
+ length_total.times { print '=' }
212
+ print "\n "
213
+ data.each do |k, v|
214
+ case k
215
+ when :id
216
+ print ' ID'
217
+ when :otp
218
+ print '| OTP'
219
+ else
220
+ print "| #{k.to_s.capitalize}"
221
+ end
222
+
223
+ (v[:length] - k.to_s.length).times { print ' ' }
224
+ end
225
+ print "\n "
226
+ length_total.times { print '=' }
227
+ print "\n"
228
+ end
229
+
230
+ print " #{i}".send(data[:id][:color])
231
+ (data[:id][:length] - i.to_s.length).times { print ' ' }
232
+ data.each do |k, v|
233
+ next if k == :id
234
+
235
+ if k == :otp
236
+ print '| '
237
+ item.otp ? (print ' X ') : 4.times { print ' ' }
238
+
239
+ next
240
+ end
241
+
242
+ print '| '
243
+ print item.send(k.to_s).to_s.send(v[:color])
244
+ (v[:length] - item.send(k.to_s).to_s.length).times { print ' ' }
245
+ end
246
+ print "\n"
247
+
248
+ i += 1
249
+ end
250
+
251
+ print "\n"
252
+ end
253
+
254
+ # Display the query's result
255
+ # @args: options -> the option to search
256
+ def list(**options)
257
+ result = @mpw.list(options)
258
+
259
+ if result.empty?
260
+ puts I18n.t('display.nothing')
261
+ else
262
+ table_items(result)
263
+ end
264
+ end
265
+
266
+ # Get an item when multiple choice
267
+ # @args: items -> array of items
268
+ # @rtrn: item
269
+ def get_item(items)
270
+ return items[0] if items.length == 1
271
+
272
+ items.sort! { |a, b| a.group.to_s.downcase <=> b.group.to_s.downcase }
273
+ choice = ask(I18n.t('form.select')).to_i
274
+
275
+ choice >= 1 && choice <= items.length ? items[choice - 1] : nil
276
+ end
277
+
278
+ # Copy in clipboard the login and password
279
+ # @args: item -> the item
280
+ # clipboard -> enable clipboard
281
+ def clipboard(item, clipboard = true)
282
+ # Security: force quit after 90s
283
+ Thread.new do
284
+ sleep 90
285
+ exit
286
+ end
287
+
288
+ Kernel.loop do
289
+ choice = ask(I18n.t('form.clipboard.choice')).to_s
290
+
291
+ case choice
292
+ when 'q', 'quit'
293
+ break
294
+
295
+ when 'l', 'login'
296
+ if clipboard
297
+ Clipboard.copy(item.user)
298
+ puts I18n.t('form.clipboard.login').green
299
+ else
300
+ puts item.user
301
+ end
302
+
303
+ when 'p', 'password'
304
+ if clipboard
305
+ Clipboard.copy(@mpw.get_password(item.id))
306
+ puts I18n.t('form.clipboard.password').yellow
307
+
308
+ Thread.new do
309
+ sleep 30
310
+
311
+ Clipboard.clear
312
+ end
313
+ else
314
+ puts @mpw.get_password(item.id)
315
+ end
316
+
317
+ when 'o', 'otp'
318
+ if clipboard
319
+ Clipboard.copy(@mpw.get_otp_code(item.id))
320
+ else
321
+ puts @mpw.get_otp_code(item.id)
322
+ end
323
+ puts I18n.t('form.clipboard.otp', time: @mpw.get_otp_remaining_time).yellow
324
+
325
+ else
326
+ puts "----- #{I18n.t('form.clipboard.help.name')} -----".cyan
327
+ puts I18n.t('form.clipboard.help.login')
328
+ puts I18n.t('form.clipboard.help.password')
329
+ puts I18n.t('form.clipboard.help.otp_code')
330
+ puts I18n.t('form.clipboard.help.quit')
331
+ next
332
+ end
333
+ end
334
+
335
+ Clipboard.clear
336
+ rescue SystemExit, Interrupt
337
+ Clipboard.clear
338
+ end
339
+
340
+ # List all wallets
341
+ def list_wallet
342
+ wallets = []
343
+ Dir.glob("#{@config.wallet_dir}/*.mpw").each do |f|
344
+ wallet = File.basename(f, '.mpw')
345
+ wallet += ' *'.green if wallet == @config.default_wallet
346
+ wallets << wallet
347
+ end
348
+
349
+ table_list('wallets', wallets)
350
+ end
351
+
352
+ # Display the wallet
353
+ # @args: wallet -> the wallet name
354
+ def get_wallet(wallet = nil)
355
+ @wallet =
356
+ if wallet.to_s.empty?
357
+ wallets = Dir.glob("#{@config.wallet_dir}/*.mpw")
358
+ if wallets.length == 1
359
+ File.basename(wallets[0], '.mpw')
360
+ elsif !@config.default_wallet.to_s.empty?
361
+ @config.default_wallet
362
+ else
363
+ 'default'
364
+ end
365
+ else
366
+ wallet
367
+ end
368
+
369
+ @wallet_file =
370
+ if @config.wallet_paths.key?(@wallet)
371
+ "#{@config.wallet_paths[@wallet]}/#{@wallet}.mpw"
372
+ else
373
+ "#{@config.wallet_dir}/#{@wallet}.mpw"
374
+ end
375
+ end
376
+
377
+ # Add a new public key
378
+ # args: key -> the key name or key file to add
379
+ def add_key(key)
380
+ @mpw.add_key(key)
381
+ @mpw.write_data
382
+
383
+ puts I18n.t('form.add_key.valid').to_s.green
384
+ rescue => e
385
+ puts "#{I18n.t('display.error')} #13: #{e}".red
386
+ end
387
+
388
+ # Add new public key
389
+ # args: key -> the key name to delete
390
+ def delete_key(key)
391
+ @mpw.delete_key(key)
392
+ @mpw.write_data
393
+
394
+ puts I18n.t('form.delete_key.valid').to_s.green
395
+ rescue => e
396
+ puts "#{I18n.t('display.error')} #15: #{e}".red
397
+ end
398
+
399
+ # Text editor interface
400
+ # @args: template -> template name
401
+ # item -> the item to edit
402
+ # password -> disable field password
403
+ # @rtrn: a hash with the value for an item
404
+ def text_editor(template_name, password = false, item = nil, **options)
405
+ editor = ENV['EDITOR'] || 'nano'
406
+ opts = {}
407
+ template_file = "#{File.expand_path('../../../templates', __FILE__)}/#{template_name}.erb"
408
+ template = ERB.new(IO.read(template_file))
409
+
410
+ Dir.mktmpdir do |dir|
411
+ tmp_file = "#{dir}/#{template_name}.yml"
412
+
413
+ File.open(tmp_file, 'w') do |f|
414
+ f << template.result(binding)
415
+ end
416
+
417
+ system("#{editor} #{tmp_file}")
418
+
419
+ opts = YAML.load_file(tmp_file)
420
+ end
421
+
422
+ opts.delete_if { |_, v| v.to_s.empty? }
423
+
424
+ opts.each do |k, v|
425
+ options[k.to_sym] = v
426
+ end
427
+
428
+ options
429
+ end
430
+
431
+ # Form to add a new item
432
+ # @args: password -> generate a random password
433
+ # text_editor -> enable text editor mode
434
+ # values -> hash with multiples value to set the item
435
+ def add(password = false, text_editor = false, **values)
436
+ options = text_editor('add_form', password, nil, values) if text_editor
437
+ item = Item.new(options)
438
+ options[:password] = MPW.password(@config.password) if password
439
+
440
+ @mpw.add(item)
441
+ @mpw.set_password(item.id, options[:password]) if options.key?(:password)
442
+ @mpw.set_otp_key(item.id, options[:otp_key]) if options.key?(:otp_key)
443
+ @mpw.write_data
444
+
445
+ puts I18n.t('form.add_item.valid').to_s.green
446
+ rescue => e
447
+ puts "#{I18n.t('display.error')} #13: #{e}".red
448
+ end
449
+
450
+ # Update an item
451
+ # @args: password -> generate a random password
452
+ # text_editor -> enable text editor mode
453
+ # options -> the option to search
454
+ # values -> hash with multiples value to set the item
455
+ def update(password = false, text_editor = false, options = {}, **values)
456
+ items = @mpw.list(options)
457
+
458
+ if items.empty?
459
+ puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
460
+ else
461
+ table_items(items) if items.length > 1
462
+
463
+ item = get_item(items)
464
+ values = text_editor('update_form', password, item, values) if text_editor
465
+ values[:password] = MPW.password(@config.password) if password
466
+
467
+ item.update(values)
468
+ @mpw.set_password(item.id, values[:password]) if values.key?(:password)
469
+ @mpw.set_otp_key(item.id, values[:otp_key]) if values.key?(:otp_key)
470
+ @mpw.write_data
471
+
472
+ puts I18n.t('form.update_item.valid').to_s.green
473
+ end
474
+ rescue => e
475
+ puts "#{I18n.t('display.error')} #14: #{e}".red
476
+ end
477
+
478
+ # Remove an item
479
+ # @args: options -> the option to search
480
+ def delete(**options)
481
+ items = @mpw.list(options)
482
+
483
+ if items.empty?
484
+ puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
485
+ else
486
+ table_items(items)
487
+
488
+ item = get_item(items)
489
+ confirm = ask("#{I18n.t('form.delete_item.ask')} (y/N) ").to_s
490
+
491
+ return unless confirm =~ /^(y|yes|YES|Yes|Y)$/
492
+
493
+ item.delete
494
+ @mpw.write_data
495
+
496
+ puts I18n.t('form.delete_item.valid').to_s.green
497
+ end
498
+ rescue => e
499
+ puts "#{I18n.t('display.error')} #16: #{e}".red
500
+ end
501
+
502
+ # Copy a password, otp, login
503
+ # @args: clipboard -> enable clipboard
504
+ # options -> the option to search
505
+ def copy(clipboard = true, **options)
506
+ items = @mpw.list(options)
507
+
508
+ if items.empty?
509
+ puts I18n.t('display.nothing')
510
+ else
511
+ table_items(items)
512
+
513
+ item = get_item(items)
514
+ clipboard(item, clipboard)
515
+ end
516
+ rescue => e
517
+ puts "#{I18n.t('display.error')} #14: #{e}".red
518
+ end
519
+
520
+ # Export the items in a CSV file
521
+ # @args: file -> the destination file
522
+ # options -> option to search
523
+ def export(file, options)
524
+ file = 'export-mpw.yml' if file.to_s.empty?
525
+ items = @mpw.list(options)
526
+ data = {}
527
+
528
+ items.each do |item|
529
+ data.merge!(
530
+ item.id => {
531
+ 'host' => item.host,
532
+ 'user' => item.user,
533
+ 'group' => item.group,
534
+ 'password' => @mpw.get_password(item.id),
535
+ 'protocol' => item.protocol,
536
+ 'port' => item.port,
537
+ 'otp_key' => @mpw.get_otp_key(item.id),
538
+ 'comment' => item.comment,
539
+ 'last_edit' => item.last_edit,
540
+ 'created' => item.created,
541
+ }
542
+ )
543
+ end
544
+
545
+ File.open(file, 'w') { |f| f << data.to_yaml }
546
+
547
+ puts I18n.t('form.export.valid', file: file).to_s.green
548
+ rescue => e
549
+ puts "#{I18n.t('display.error')} #17: #{e}".red
550
+ end
551
+
552
+ # Import items from a YAML file
553
+ # @args: file -> the import file
554
+ def import(file)
555
+ raise I18n.t('form.import.file_empty') if file.to_s.empty?
556
+ raise I18n.t('form.import.file_not_exist') unless File.exist?(file)
557
+
558
+ YAML.load_file(file).each_value do |row|
559
+ item = Item.new(group: row['group'],
560
+ host: row['host'],
561
+ protocol: row['protocol'],
562
+ user: row['user'],
563
+ port: row['port'],
564
+ comment: row['comment'])
565
+
566
+ next if item.empty?
567
+
568
+ @mpw.add(item)
569
+ @mpw.set_password(item.id, row['password']) unless row['password'].to_s.empty?
570
+ @mpw.set_otp_key(item.id, row['otp_key']) unless row['otp_key'].to_s.empty?
571
+ end
572
+
573
+ @mpw.write_data
574
+
575
+ puts I18n.t('form.import.valid').to_s.green
576
+ rescue => e
577
+ puts "#{I18n.t('display.error')} #18: #{e}".red
578
+ end
579
+ end
572
580
  end