mpw 4.0.0 → 4.1.0

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