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.
- checksums.yaml +4 -4
- data/.rubocop.yml +122 -0
- data/.travis.yml +2 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +7 -9
- data/README.md +1 -7
- data/VERSION +1 -1
- data/bin/mpw +25 -25
- data/bin/mpw-add +54 -25
- data/bin/mpw-config +74 -62
- data/bin/mpw-copy +25 -30
- data/bin/mpw-delete +24 -29
- data/bin/mpw-export +27 -32
- data/bin/mpw-genpwd +22 -22
- data/bin/mpw-import +20 -25
- data/bin/mpw-list +24 -29
- data/bin/mpw-update +61 -32
- data/bin/mpw-wallet +48 -73
- data/i18n/en.yml +15 -26
- data/i18n/fr.yml +15 -26
- data/lib/mpw/cli.rb +554 -546
- data/lib/mpw/config.rb +168 -139
- data/lib/mpw/item.rb +75 -82
- data/lib/mpw/mpw.rb +328 -465
- data/mpw.gemspec +11 -10
- data/templates/add_form.erb +8 -8
- data/templates/update_form.erb +7 -7
- data/test/test_config.rb +70 -55
- data/test/test_item.rb +167 -167
- data/test/test_mpw.rb +132 -132
- data/test/test_translate.rb +23 -23
- data/test/tests.rb +1 -1
- metadata +4 -45
- data/lib/mpw/sync/ftp.rb +0 -68
- data/lib/mpw/sync/ssh.rb +0 -67
data/lib/mpw/cli.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
2
|
# MPW is a software to crypt and manage your passwords
|
3
|
-
# Copyright (C)
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
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
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
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
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
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
|