Class: MPW::Cli

Inherits:
Object
  • Object
show all
Defined in:
lib/mpw/cli.rb

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Cli

Constructor @args: config -> the config



33
34
35
# File 'lib/mpw/cli.rb', line 33

def initialize(config)
  @config = config
end

Instance Method Details

#add(password = false, text_editor = false, **values) ⇒ Object

Form to add a new item @args: password -> generate a random password

text_editor -> enable text editor mode
values -> hash with multiples value to set the item


435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/mpw/cli.rb', line 435

def add(password = false, text_editor = false, **values)
  options            = text_editor('add_form', password, nil, values) if text_editor
  item               = Item.new(options)
  options[:password] = MPW.password(@config.password) if password

  @mpw.add(item)
  @mpw.set_password(item.id, options[:password]) if options.key?(:password)
  @mpw.set_otp_key(item.id, options[:otp_key])   if options.key?(:otp_key)
  @mpw.write_data

  puts I18n.t('form.add_item.valid').to_s.green
rescue => e
  puts "#{I18n.t('display.error')} #13: #{e}".red
end

#add_key(key) ⇒ Object

Add a new public key args: key -> the key name or key file to add



379
380
381
382
383
384
385
386
# File 'lib/mpw/cli.rb', line 379

def add_key(key)
  @mpw.add_key(key)
  @mpw.write_data

  puts I18n.t('form.add_key.valid').to_s.green
rescue => e
  puts "#{I18n.t('display.error')} #13: #{e}".red
end

#clipboard(item, clipboard = true) ⇒ Object

Copy in clipboard the login and password @args: item -> the item

clipboard -> enable clipboard


281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/mpw/cli.rb', line 281

def clipboard(item, clipboard = true)
  # Security: force quit after 90s
  Thread.new do
    sleep 90
    exit
  end

  Kernel.loop do
    choice = ask(I18n.t('form.clipboard.choice')).to_s

    case choice
    when 'q', 'quit'
      break

    when 'l', 'login'
      if clipboard
        Clipboard.copy(item.user)
        puts I18n.t('form.clipboard.login').green
      else
        puts item.user
      end

    when 'p', 'password'
      if clipboard
        Clipboard.copy(@mpw.get_password(item.id))
        puts I18n.t('form.clipboard.password').yellow

        Thread.new do
          sleep 30

          Clipboard.clear
        end
      else
        puts @mpw.get_password(item.id)
      end

    when 'o', 'otp'
      if clipboard
        Clipboard.copy(@mpw.get_otp_code(item.id))
      else
        puts @mpw.get_otp_code(item.id)
      end
      puts I18n.t('form.clipboard.otp', time: @mpw.get_otp_remaining_time).yellow

    else
      puts "----- #{I18n.t('form.clipboard.help.name')} -----".cyan
      puts I18n.t('form.clipboard.help.login')
      puts I18n.t('form.clipboard.help.password')
      puts I18n.t('form.clipboard.help.otp_code')
      puts I18n.t('form.clipboard.help.quit')
      next
    end
  end

  Clipboard.clear
rescue SystemExit, Interrupt
  Clipboard.clear
end

#copy(clipboard = true, **options) ⇒ Object

Copy a password, otp, login @args: clipboard -> enable clipboard

options -> the option to search


505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'lib/mpw/cli.rb', line 505

def copy(clipboard = true, **options)
  items = @mpw.list(options)

  if items.empty?
    puts I18n.t('display.nothing')
  else
    table_items(items)

    item = get_item(items)
    clipboard(item, clipboard)
  end
rescue => e
  puts "#{I18n.t('display.error')} #14: #{e}".red
end

#decryptObject

Request the GPG password and decrypt the file



129
130
131
132
133
134
135
136
137
138
139
# File 'lib/mpw/cli.rb', line 129

def decrypt
  unless defined?(@mpw)
    @password = ask(I18n.t('display.gpg_password')) { |q| q.echo = false }
    @mpw      = MPW.new(@config.gpg_key, @wallet_file, @password, @config.gpg_exe, @config.pinmode)
  end

  @mpw.read_data
rescue => e
  puts "#{I18n.t('display.error')} #11: #{e}".red
  exit 2
end

#delete(**options) ⇒ Object

Remove an item @args: options -> the option to search



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/mpw/cli.rb', line 480

def delete(**options)
  items = @mpw.list(options)

  if items.empty?
    puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
  else
    table_items(items)

    item    = get_item(items)
    confirm = ask("#{I18n.t('form.delete_item.ask')} (y/N) ").to_s

    return unless confirm =~ /^(y|yes|YES|Yes|Y)$/

    item.delete
    @mpw.write_data

    puts I18n.t('form.delete_item.valid').to_s.green
  end
rescue => e
  puts "#{I18n.t('display.error')} #16: #{e}".red
end

#delete_key(key) ⇒ Object

Add new public key args: key -> the key name to delete



390
391
392
393
394
395
396
397
# File 'lib/mpw/cli.rb', line 390

def delete_key(key)
  @mpw.delete_key(key)
  @mpw.write_data

  puts I18n.t('form.delete_key.valid').to_s.green
rescue => e
  puts "#{I18n.t('display.error')} #15: #{e}".red
end

#export(file, options) ⇒ Object

Export the items in a CSV file @args: file -> the destination file

options -> option to search


523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
# File 'lib/mpw/cli.rb', line 523

def export(file, options)
  file  = 'export-mpw.yml' if file.to_s.empty?
  items = @mpw.list(options)
  data  = {}

  items.each do |item|
    data.merge!(
      item.id => {
        'host'      => item.host,
        'user'      => item.user,
        'group'     => item.group,
        'password'  => @mpw.get_password(item.id),
        'protocol'  => item.protocol,
        'port'      => item.port,
        'otp_key'   => @mpw.get_otp_key(item.id),
        'comment'   => item.comment,
        'last_edit' => item.last_edit,
        'created'   => item.created,
      }
    )
  end

  File.open(file, 'w') { |f| f << data.to_yaml }

  puts I18n.t('form.export.valid', file: file).to_s.green
rescue => e
  puts "#{I18n.t('display.error')} #17: #{e}".red
end

#get_item(items) ⇒ Object

Get an item when multiple choice @args: items -> array of items @rtrn: item



269
270
271
272
273
274
275
276
# File 'lib/mpw/cli.rb', line 269

def get_item(items)
  return items[0] if items.length == 1

  items.sort! { |a, b| a.group.to_s.downcase <=> b.group.to_s.downcase }
  choice = ask(I18n.t('form.select')).to_i

  choice >= 1 && choice <= items.length ? items[choice - 1] : nil
end

#get_wallet(wallet = nil) ⇒ Object

Display the wallet @args: wallet -> the wallet name



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/mpw/cli.rb', line 354

def get_wallet(wallet = nil)
  @wallet =
    if wallet.to_s.empty?
      wallets = Dir.glob("#{@config.wallet_dir}/*.mpw")
      if wallets.length == 1
        File.basename(wallets[0], '.mpw')
      elsif !@config.default_wallet.to_s.empty?
        @config.default_wallet
      else
        'default'
      end
    else
      wallet
    end

  @wallet_file =
    if @config.wallet_paths.key?(@wallet)
      "#{@config.wallet_paths[@wallet]}/#{@wallet}.mpw"
    else
      "#{@config.wallet_dir}/#{@wallet}.mpw"
    end
end

#import(file) ⇒ Object

Import items from a YAML file @args: file -> the import file



554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
# File 'lib/mpw/cli.rb', line 554

def import(file)
  raise I18n.t('form.import.file_empty')     if file.to_s.empty?
  raise I18n.t('form.import.file_not_exist') unless File.exist?(file)

  YAML.load_file(file).each_value do |row|
    item = Item.new(group:    row['group'],
                    host:     row['host'],
                    protocol: row['protocol'],
                    user:     row['user'],
                    port:     row['port'],
                    comment:  row['comment'])

    next if item.empty?

    @mpw.add(item)
    @mpw.set_password(item.id, row['password']) unless row['password'].to_s.empty?
    @mpw.set_otp_key(item.id, row['otp_key'])   unless row['otp_key'].to_s.empty?
  end

  @mpw.write_data

  puts I18n.t('form.import.valid').to_s.green
rescue => e
  puts "#{I18n.t('display.error')} #18: #{e}".red
end

#list(**options) ⇒ Object

Display the query’s result @args: options -> the option to search



256
257
258
259
260
261
262
263
264
# File 'lib/mpw/cli.rb', line 256

def list(**options)
  result = @mpw.list(options)

  if result.empty?
    puts I18n.t('display.nothing')
  else
    table_items(result)
  end
end

#list_configObject

List config



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/mpw/cli.rb', line 104

def list_config
  config = {
    'lang'           => @config.lang,
    'gpg_key'        => @config.gpg_key,
    'default_wallet' => @config.default_wallet,
    'config_dir'     => @config.config_dir,
    'pinmode'        => @config.pinmode,
    'gpg_exe'        => @config.gpg_exe
  }

  @config.wallet_paths.each { |k, v| config["path_wallet_#{k}"] = "#{v}/#{k}.mpw" }
  @config.password.each     { |k, v| config["password_#{k}"] = v }

  table_list('config', config)
end

#list_keysObject

List gpg keys in wallet



99
100
101
# File 'lib/mpw/cli.rb', line 99

def list_keys
  table_list('keys', @mpw.list_keys)
end

#list_walletObject

List all wallets



341
342
343
344
345
346
347
348
349
350
# File 'lib/mpw/cli.rb', line 341

def list_wallet
  wallets = []
  Dir.glob("#{@config.wallet_dir}/*.mpw").each do |f|
    wallet = File.basename(f, '.mpw')
    wallet += ' *'.green if wallet == @config.default_wallet
    wallets << wallet
  end

  table_list('wallets', wallets)
end

#load_configObject

Load config



121
122
123
124
125
126
# File 'lib/mpw/cli.rb', line 121

def load_config
  @config.load_config
rescue => e
  puts "#{I18n.t('display.error')} #10: #{e}".red
  exit 2
end

#set_config(options) ⇒ Object

Change a parameter int the config after init @args: options -> param to change



39
40
41
42
43
44
45
46
# File 'lib/mpw/cli.rb', line 39

def set_config(options)
  @config.setup(options)

  puts I18n.t('form.set_config.valid').to_s.green
rescue => e
  puts "#{I18n.t('display.error')} #15: #{e}".red
  exit 2
end

#set_wallet_path(path) ⇒ Object

Change the wallet path @args: path -> the new path



50
51
52
53
54
55
56
57
# File 'lib/mpw/cli.rb', line 50

def set_wallet_path(path)
  @config.set_wallet_path(path, @wallet)

  puts I18n.t('form.set_wallet_path.valid').to_s.green
rescue => e
  puts "#{I18n.t('display.error')} #19: #{e}".red
  exit 2
end

#setup(options) ⇒ Object

Create a new config file @args: options -> set param



61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/mpw/cli.rb', line 61

def setup(options)
  options[:lang] = options[:lang] || Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1]

  I18n.locale = options[:lang].to_sym

  @config.setup(options)

  load_config

  puts I18n.t('form.setup_config.valid').to_s.green
rescue => e
  puts "#{I18n.t('display.error')} #8: #{e}".red
  exit 2
end

#setup_gpg_key(gpg_key) ⇒ Object

Setup a new GPG key @args: gpg_key -> the key name



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/mpw/cli.rb', line 78

def setup_gpg_key(gpg_key)
  return if @config.check_gpg_key?

  password = ask(I18n.t('form.setup_gpg_key.password')) { |q| q.echo = false }
  confirm  = ask(I18n.t('form.setup_gpg_key.confirm_password')) { |q| q.echo = false }

  raise I18n.t('form.setup_gpg_key.error_password') if password != confirm

  @password = password.to_s

  puts I18n.t('form.setup_gpg_key.wait')

  @config.setup_gpg_key(@password, gpg_key)

  puts I18n.t('form.setup_gpg_key.valid').to_s.green
rescue => e
  puts "#{I18n.t('display.error')} #8: #{e}".red
  exit 2
end

#table_items(items = []) ⇒ Object

Format items on a table @args: items -> an aray items



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/mpw/cli.rb', line 176

def table_items(items = [])
  group        = '.'
  i            = 1
  length_total = 10
  data         = { id:       { length: 3,  color: 'cyan' },
                   host:     { length: 9,  color: 'yellow' },
                   user:     { length: 7,  color: 'green' },
                   protocol: { length: 9,  color: 'white' },
                   port:     { length: 5,  color: 'white' },
                   otp:      { length: 4,  color: 'white' },
                   comment:  { length: 14, color: 'magenta' } }

  items.each do |item|
    data.each do |k, v|
      next if k == :id || k == :otp

      v[:length] = item.send(k.to_s).to_s.length + 3 if item.send(k.to_s).to_s.length >= v[:length]
    end
  end
  data[:id][:length] = items.length.to_s.length + 2 if items.length.to_s.length > data[:id][:length]

  data.each_value { |v| length_total += v[:length] }
  items.sort!     { |a, b| a.group.to_s.downcase <=> b.group.to_s.downcase }

  items.each do |item|
    if group != item.group
      group = item.group

      if group.to_s.empty?
        puts "\n#{I18n.t('display.no_group')}".red
      else
        puts "\n#{group}".red
      end

      print ' '
      length_total.times { print '=' }
      print "\n "
      data.each do |k, v|
        case k
        when :id
          print ' ID'
        when :otp
          print '| OTP'
        else
          print "| #{k.to_s.capitalize}"
        end

        (v[:length] - k.to_s.length).times { print ' ' }
      end
      print "\n "
      length_total.times { print '=' }
      print "\n"
    end

    print "  #{i}".send(data[:id][:color])
    (data[:id][:length] - i.to_s.length).times { print ' ' }
    data.each do |k, v|
      next if k == :id

      if k == :otp
        print '| '
        item.otp ? (print ' X  ') : 4.times { print ' ' }

        next
      end

      print '| '
      print item.send(k.to_s).to_s.send(v[:color])
      (v[:length] - item.send(k.to_s).to_s.length).times { print ' ' }
    end
    print "\n"

    i += 1
  end

  print "\n"
end

#table_list(title, list) ⇒ Object

Format list on a table @args: title -> the name of table

list -> array or hash


144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/mpw/cli.rb', line 144

def table_list(title, list)
  length = { k: 0, v: 0 }

  if list.is_a?(Array)
    i    = 0
    list = list.map do |item|
      i += 1
      [i, item]
    end.to_h
  end

  list.each do |k, v|
    length[:k] = k.to_s.length if length[:k] < k.to_s.length
    length[:v] = v.to_s.length if length[:v] < v.to_s.length
  end

  puts "\n#{I18n.t("display.#{title}")}".red
  print ' '
  (length[:k] + length[:v] + 5).times { print '=' }
  print "\n"

  list.each do |k, v|
    print "  #{k}".cyan
    (length[:k] - k.to_s.length + 1).times { print ' ' }
    puts "| #{v}"
  end

  print "\n"
end

#text_editor(template_name, password = false, item = nil, **options) ⇒ Object

Text editor interface @args: template -> template name

item -> the item to edit
password -> disable field password

@rtrn: a hash with the value for an item



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/mpw/cli.rb', line 404

def text_editor(template_name, password = false, item = nil, **options)
  editor        = ENV['EDITOR'] || 'nano'
  opts          = {}
  template_file = "#{File.expand_path('../../../templates', __FILE__)}/#{template_name}.erb"
  template      = ERB.new(IO.read(template_file))

  Dir.mktmpdir do |dir|
    tmp_file = "#{dir}/#{template_name}.yml"

    File.open(tmp_file, 'w') do |f|
      f << template.result(binding)
    end

    system("#{editor} #{tmp_file}")

    opts = YAML.load_file(tmp_file)
  end

  opts.delete_if { |_, v| v.to_s.empty? }

  opts.each do |k, v|
    options[k.to_sym] = v
  end

  options
end

#update(password = false, text_editor = false, options = {}, **values) ⇒ Object

Update an item @args: password -> generate a random password

text_editor -> enable text editor mode
options -> the option to search
values -> hash with multiples value to set the item


455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# File 'lib/mpw/cli.rb', line 455

def update(password = false, text_editor = false, options = {}, **values)
  items = @mpw.list(options)

  if items.empty?
    puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow
  else
    table_items(items) if items.length > 1

    item              = get_item(items)
    values            = text_editor('update_form', password, item, values) if text_editor
    values[:password] = MPW.password(@config.password) if password

    item.update(values)
    @mpw.set_password(item.id, values[:password]) if values.key?(:password)
    @mpw.set_otp_key(item.id, values[:otp_key])   if values.key?(:otp_key)
    @mpw.write_data

    puts I18n.t('form.update_item.valid').to_s.green
  end
rescue => e
  puts "#{I18n.t('display.error')} #14: #{e}".red
end