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.
@@ -22,466 +22,329 @@ require 'i18n'
22
22
  require 'yaml'
23
23
  require 'rotp'
24
24
  require 'mpw/item'
25
-
26
- module MPW
27
- class MPW
28
-
29
- # Constructor
30
- def initialize(key, wallet_file, gpg_pass=nil, gpg_exe=nil)
31
- @key = key
32
- @gpg_pass = gpg_pass
33
- @gpg_exe = gpg_exe
34
- @wallet_file = wallet_file
35
-
36
- if not @gpg_exe.to_s.empty?
37
- GPGME::Engine.set_info(GPGME::PROTOCOL_OpenPGP, @gpg_exe, "#{Dir.home}/.gnupg")
38
- end
39
- end
40
-
41
- # Read mpw file
42
- def read_data
43
- @config = {}
44
- @data = []
45
- @keys = {}
46
- @passwords = {}
47
- @otp_keys = {}
48
-
49
- data = nil
50
-
51
- return if not File.exists?(@wallet_file)
52
-
53
- Gem::Package::TarReader.new(File.open(@wallet_file)) do |tar|
54
- tar.each do |f|
55
- case f.full_name
56
- when 'wallet/config.gpg'
57
- @config = YAML.load(decrypt(f.read))
58
-
59
- when 'wallet/meta.gpg'
60
- data = decrypt(f.read)
61
-
62
- when /^wallet\/keys\/(?<key>.+)\.pub$/
63
- key = Regexp.last_match('key')
64
-
65
- if GPGME::Key.find(:public, key).length == 0
66
- GPGME::Key.import(f.read, armor: true)
67
- end
68
-
69
- @keys[key] = f.read
70
-
71
- when /^wallet\/passwords\/(?<id>[a-zA-Z0-9]+)\.gpg$/
72
- @passwords[Regexp.last_match('id')] = f.read
73
-
74
- when /^wallet\/otp_keys\/(?<id>[a-zA-Z0-9]+)\.gpg$/
75
- @otp_keys[Regexp.last_match('id')] = f.read
76
-
77
- else
78
- next
79
- end
80
- end
81
- end
82
-
83
- if not data.nil? and not data.empty?
84
- YAML.load(data).each_value do |d|
85
- @data.push(Item.new(id: d['id'],
86
- group: d['group'],
87
- host: d['host'],
88
- protocol: d['protocol'],
89
- user: d['user'],
90
- port: d['port'],
91
- otp: @otp_keys.has_key?(d['id']),
92
- comment: d['comment'],
93
- last_edit: d['last_edit'],
94
- created: d['created'],
95
- )
96
- )
97
- end
98
- end
99
-
100
- add_key(@key) if @keys[@key].nil?
101
- rescue Exception => e
102
- raise "#{I18n.t('error.mpw_file.read_data')}\n#{e}"
103
- end
104
-
105
- # Encrypt a file
106
- def write_data
107
- data = {}
108
- tmp_file = "#{@wallet_file}.tmp"
109
-
110
- @data.each do |item|
111
- next if item.empty?
112
-
113
- data.merge!(item.id => { 'id' => item.id,
114
- 'group' => item.group,
115
- 'host' => item.host,
116
- 'protocol' => item.protocol,
117
- 'user' => item.user,
118
- 'port' => item.port,
119
- 'comment' => item.comment,
120
- 'last_edit' => item.last_edit,
121
- 'created' => item.created,
122
- }
123
- )
124
- end
125
-
126
- @config['last_update'] = Time.now.to_i
127
-
128
- Gem::Package::TarWriter.new(File.open(tmp_file, 'w+')) do |tar|
129
- data_encrypt = encrypt(data.to_yaml)
130
- tar.add_file_simple('wallet/meta.gpg', 0400, data_encrypt.length) do |io|
131
- io.write(data_encrypt)
132
- end
133
-
134
- config = encrypt(@config.to_yaml)
135
- tar.add_file_simple('wallet/config.gpg', 0400, config.length) do |io|
136
- io.write(config)
137
- end
138
-
139
- @passwords.each do |id, password|
140
- tar.add_file_simple("wallet/passwords/#{id}.gpg", 0400, password.length) do |io|
141
- io.write(password)
142
- end
143
- end
144
-
145
- @otp_keys.each do |id, key|
146
- tar.add_file_simple("wallet/otp_keys/#{id}.gpg", 0400, key.length) do |io|
147
- io.write(key)
148
- end
149
- end
150
-
151
- @keys.each do |id, key|
152
- tar.add_file_simple("wallet/keys/#{id}.pub", 0400, key.length) do |io|
153
- io.write(key)
154
- end
155
- end
156
- end
157
-
158
- File.rename(tmp_file, @wallet_file)
159
- rescue Exception => e
160
- File.unlink(tmp_file) if File.exist?(tmp_file)
161
-
162
- raise "#{I18n.t('error.mpw_file.write_data')}\n#{e}"
163
- end
164
-
165
- # Get a password
166
- # args: id -> the item id
167
- def get_password(id)
168
- password = decrypt(@passwords[id])
169
-
170
- if /^\$[a-zA-Z0-9]{4,9}::(?<password>.+)$/ =~ password
171
- return Regexp.last_match('password')
172
- else
173
- return password
174
- end
175
- end
176
-
177
- # Set a password
178
- # args: id -> the item id
179
- # password -> the new password
180
- def set_password(id, password)
181
- salt = MPW::password(length: Random.rand(4..9))
182
- password = "$#{salt}::#{password}"
183
-
184
- @passwords[id] = encrypt(password)
185
- end
186
-
187
- # Return the list of all gpg keys
188
- # rtrn: an array with the gpg keys name
189
- def list_keys
190
- return @keys.keys
191
- end
192
-
193
- # Add a public key
194
- # args: key -> new public key file or name
195
- def add_key(key)
196
- if File.exists?(key)
197
- data = File.open(key).read
198
- key_import = GPGME::Key.import(data, armor: true)
199
- key = GPGME::Key.get(key_import.imports[0].fpr).uids[0].email
200
- else
201
- data = GPGME::Key.export(key, armor: true).read
202
- end
203
-
204
- if data.to_s.empty?
205
- raise I18n.t('error.export_key')
206
- end
207
-
208
- @keys[key] = data
209
- @passwords.each_key { |id| set_password(id, get_password(id)) }
210
- @otp_keys.each_key { |id| set_otp_key(id, get_otp_key(id)) }
211
- end
212
-
213
- # Delete a public key
214
- # args: key -> public key to delete
215
- def delete_key(key)
216
- @keys.delete(key)
217
- @passwords.each_key { |id| set_password(id, get_password(id)) }
218
- @otp_keys.each_key { |id| set_otp_key(id, get_otp_key(id)) }
219
- end
220
-
221
- # Set config
222
- # args: config -> a hash with config options
223
- def set_config(options={})
224
- @config = {} if @config.nil?
225
-
226
- @config['protocol'] = options[:protocol] if options.has_key?(:protocol)
227
- @config['host'] = options[:host] if options.has_key?(:host)
228
- @config['port'] = options[:port] if options.has_key?(:port)
229
- @config['user'] = options[:user] if options.has_key?(:user)
230
- @config['password'] = options[:password] if options.has_key?(:password)
231
- @config['path'] = options[:path] if options.has_key?(:path)
232
- @config['last_sync'] = @config['last_sync'].nil? ? 0 : @config['last_sync']
233
- end
234
-
235
- # Add a new item
236
- # @args: item -> Object MPW::Item
237
- def add(item)
238
- if not item.instance_of?(Item)
239
- raise I18n.t('error.bad_class')
240
- elsif item.empty?
241
- raise I18n.t('error.empty')
242
- else
243
- @data.push(item)
244
- end
245
- end
246
-
247
- # Search in some csv data
248
- # @args: options -> a hash with paramaters
249
- # @rtrn: a list with the resultat of the search
250
- def list(options={})
251
- result = []
252
-
253
- search = options[:pattern].to_s.downcase
254
- group = options[:group].to_s.downcase
255
-
256
- @data.each do |item|
257
- next if item.empty?
258
- next if not group.empty? and not group.eql?(item.group.to_s.downcase)
259
-
260
- host = item.host.to_s.downcase
261
- comment = item.comment.to_s.downcase
262
-
263
- if not host =~ /^.*#{search}.*$/ and not comment =~ /^.*#{search}.*$/
264
- next
265
- end
266
-
267
- result.push(item)
268
- end
269
-
270
- return result
271
- end
272
-
273
- # Search in some csv data
274
- # @args: id -> the id item
275
- # @rtrn: a row with the result of the search
276
- def search_by_id(id)
277
- @data.each do |item|
278
- return item if item.id == id
279
- end
280
-
281
- return nil
282
- end
283
-
284
- # Get last sync
285
- def get_last_sync
286
- return @config['last_sync'].to_i
287
- rescue
288
- return 0
289
- end
290
-
291
- # Sync data with remote file
292
- # @args: force -> force the sync
293
- def sync(force=false)
294
- return if @config.empty? or @config['protocol'].to_s.empty?
295
- return if get_last_sync + 300 > Time.now.to_i and not force
296
-
297
- tmp_file = "#{@wallet_file}.sync"
298
-
299
- case @config['protocol']
300
- when 'sftp', 'scp', 'ssh'
301
- require "mpw/sync/ssh"
302
- sync = SyncSSH.new(@config)
303
- when 'ftp'
304
- require 'mpw/sync/ftp'
305
- sync = SyncFTP.new(@config)
306
- else
307
- raise I18n.t('error.sync.unknown_type')
308
- end
309
-
310
- sync.connect
311
- sync.get(tmp_file)
312
-
313
- remote = MPW.new(@key, tmp_file, @gpg_pass, @gpg_exe)
314
- remote.read_data
315
-
316
- File.unlink(tmp_file) if File.exist?(tmp_file)
317
-
318
- return if remote.get_last_sync == @config['last_update']
319
-
320
- if not remote.to_s.empty?
321
- @data.each do |item|
322
- next if item.empty?
323
-
324
- update = false
325
-
326
- remote.list.each do |r|
327
- next if item.id != r.id
328
-
329
- # Update item
330
- if item.last_edit < r.last_edit
331
- item.update(group: r.group,
332
- host: r.host,
333
- protocol: r.protocol,
334
- user: r.user,
335
- port: r.port,
336
- comment: r.comment
337
- )
338
- set_password(item.id, remote.get_password(item.id))
339
- end
340
-
341
- r.delete
342
- update = true
343
-
344
- break
345
- end
346
-
347
- # Remove an old item
348
- if not update and item.last_sync.to_i < get_last_sync and item.last_edit < get_last_sync
349
- item.delete
350
- end
351
- end
352
- end
353
-
354
- # Add item
355
- remote.list.each do |r|
356
- next if r.last_edit <= get_last_sync
357
-
358
- item = Item.new(id: r.id,
359
- group: r.group,
360
- host: r.host,
361
- protocol: r.protocol,
362
- user: r.user,
363
- port: r.port,
364
- comment: r.comment,
365
- created: r.created,
366
- last_edit: r.last_edit
367
- )
368
-
369
- set_password(item.id, remote.get_password(item.id))
370
- add(item)
371
- end
372
-
373
- remote = nil
374
-
375
- @data.each do |item|
376
- item.set_last_sync
377
- end
378
-
379
- @config['last_sync'] = Time.now.to_i
380
-
381
- write_data
382
- sync.update(@wallet_file)
383
- rescue Exception => e
384
- File.unlink(tmp_file) if File.exist?(tmp_file)
385
-
386
- raise "#{I18n.t('error.sync.general')}\n#{e}"
387
- end
388
-
389
- # Set an opt key
390
- # args: id -> the item id
391
- # key -> the new key
392
- def set_otp_key(id, key)
393
- if not key.to_s.empty?
394
- @otp_keys[id] = encrypt(key.to_s)
395
- end
396
- end
397
-
398
- # Get an opt key
399
- # args: id -> the item id
400
- # key -> the new key
401
- def get_otp_key(id)
402
- if @otp_keys.has_key?(id)
403
- return decrypt(@otp_keys[id])
404
- else
405
- return nil
406
- end
407
- end
408
-
409
-
410
- # Get an otp code
411
- # @args: id -> the item id
412
- # @rtrn: an otp code
413
- def get_otp_code(id)
414
- if not @otp_keys.has_key?(id)
415
- return 0
416
- else
417
- return ROTP::TOTP.new(decrypt(@otp_keys[id])).now
418
- end
419
- end
420
-
421
- # Get remaining time before expire otp code
422
- # @rtrn: return time in seconde
423
- def get_otp_remaining_time
424
- return (Time.now.utc.to_i / 30 + 1) * 30 - Time.now.utc.to_i
425
- end
426
-
427
- # Generate a random password
428
- # @args: options -> :length, :special, :alpha, :numeric
429
- # @rtrn: a random string
430
- def self.password(options={})
431
- if not options.include?(:length) or options[:length].to_i <= 0
432
- length = 8
433
- elsif options[:length].to_i >= 32768
434
- length = 32768
435
- else
436
- length = options[:length].to_i
437
- end
438
-
439
- chars = []
440
- chars += [*('!'..'?')] - [*('0'..'9')] if options[:special]
441
- chars += [*('A'..'Z'),*('a'..'z')] if options[:alpha]
442
- chars += [*('0'..'9')] if options[:numeric]
443
- chars = [*('A'..'Z'),*('a'..'z'),*('0'..'9')] if chars.empty?
444
-
445
- result = ''
446
- while length > 62 do
447
- result << chars.sample(62).join
448
- length -= 62
449
- end
450
- result << chars.sample(length).join
451
-
452
- return result
453
- end
454
-
455
- # Decrypt a gpg file
456
- # @args: data -> string to decrypt
457
- private
458
- def decrypt(data)
459
- return nil if data.to_s.empty?
460
-
461
- crypto = GPGME::Crypto.new(armor: true)
462
-
463
- return crypto.decrypt(data, password: @gpg_pass).read.force_encoding('utf-8')
464
- rescue Exception => e
465
- raise "#{I18n.t('error.gpg_file.decrypt')}\n#{e}"
466
- end
467
-
468
- # Encrypt a file
469
- # args: data -> string to encrypt
470
- private
471
- def encrypt(data)
472
- recipients = []
473
- crypto = GPGME::Crypto.new(armor: true, always_trust: true)
474
-
475
- recipients.push(@key)
476
- @keys.each_key do |key|
477
- next if key == @key
478
- recipients.push(key)
479
- end
480
-
481
- return crypto.encrypt(data, recipients: recipients).read
482
- rescue Exception => e
483
- raise "#{I18n.t('error.gpg_file.encrypt')}\n#{e}"
484
- end
485
25
 
486
- end
26
+ module MPW
27
+ class MPW
28
+ # Constructor
29
+ def initialize(key, wallet_file, gpg_pass = nil, gpg_exe = nil, pinmode = false)
30
+ @key = key
31
+ @gpg_pass = gpg_pass
32
+ @gpg_exe = gpg_exe
33
+ @wallet_file = wallet_file
34
+ @pinmode = pinmode
35
+
36
+ GPGME::Engine.set_info(GPGME::PROTOCOL_OpenPGP, @gpg_exe, "#{Dir.home}/.gnupg") unless @gpg_exe.to_s.empty?
37
+ end
38
+
39
+ # Read mpw file
40
+ def read_data
41
+ @data = []
42
+ @keys = {}
43
+ @passwords = {}
44
+ @otp_keys = {}
45
+
46
+ data = nil
47
+
48
+ return unless File.exist?(@wallet_file)
49
+
50
+ Gem::Package::TarReader.new(File.open(@wallet_file)) do |tar|
51
+ tar.each do |f|
52
+ case f.full_name
53
+ when 'wallet/meta.gpg'
54
+ data = decrypt(f.read)
55
+
56
+ when %r{^wallet/keys/(?<key>.+)\.pub$}
57
+ key = Regexp.last_match('key')
58
+
59
+ if GPGME::Key.find(:public, key).empty?
60
+ GPGME::Key.import(f.read, armor: true)
61
+ end
62
+
63
+ @keys[key] = f.read
64
+
65
+ when %r{^wallet/passwords/(?<id>[a-zA-Z0-9]+)\.gpg$}
66
+ @passwords[Regexp.last_match('id')] = f.read
67
+
68
+ when %r{^wallet/otp_keys/(?<id>[a-zA-Z0-9]+)\.gpg$}
69
+ @otp_keys[Regexp.last_match('id')] = f.read
70
+
71
+ else
72
+ next
73
+ end
74
+ end
75
+ end
76
+
77
+ unless data.to_s.empty?
78
+ YAML.safe_load(data).each_value do |d|
79
+ @data.push(
80
+ Item.new(
81
+ id: d['id'],
82
+ group: d['group'],
83
+ host: d['host'],
84
+ protocol: d['protocol'],
85
+ user: d['user'],
86
+ port: d['port'],
87
+ otp: @otp_keys.key?(d['id']),
88
+ comment: d['comment'],
89
+ last_edit: d['last_edit'],
90
+ created: d['created'],
91
+ )
92
+ )
93
+ end
94
+ end
95
+
96
+ add_key(@key) unless @keys.key?(@key)
97
+ rescue => e
98
+ raise "#{I18n.t('error.mpw_file.read_data')}\n#{e}"
99
+ end
100
+
101
+ # Encrypt a file
102
+ def write_data
103
+ data = {}
104
+ tmp_file = "#{@wallet_file}.tmp"
105
+
106
+ @data.each do |item|
107
+ next if item.empty?
108
+
109
+ data.merge!(
110
+ item.id => {
111
+ 'id' => item.id,
112
+ 'group' => item.group,
113
+ 'host' => item.host,
114
+ 'protocol' => item.protocol,
115
+ 'user' => item.user,
116
+ 'port' => item.port,
117
+ 'comment' => item.comment,
118
+ 'last_edit' => item.last_edit,
119
+ 'created' => item.created,
120
+ }
121
+ )
122
+ end
123
+
124
+ Gem::Package::TarWriter.new(File.open(tmp_file, 'w+')) do |tar|
125
+ data_encrypt = encrypt(data.to_yaml)
126
+ tar.add_file_simple('wallet/meta.gpg', 0400, data_encrypt.length) do |io|
127
+ io.write(data_encrypt)
128
+ end
129
+
130
+ @passwords.each do |id, password|
131
+ tar.add_file_simple("wallet/passwords/#{id}.gpg", 0400, password.length) do |io|
132
+ io.write(password)
133
+ end
134
+ end
135
+
136
+ @otp_keys.each do |id, key|
137
+ tar.add_file_simple("wallet/otp_keys/#{id}.gpg", 0400, key.length) do |io|
138
+ io.write(key)
139
+ end
140
+ end
141
+
142
+ @keys.each do |id, key|
143
+ tar.add_file_simple("wallet/keys/#{id}.pub", 0400, key.length) do |io|
144
+ io.write(key)
145
+ end
146
+ end
147
+ end
148
+
149
+ File.rename(tmp_file, @wallet_file)
150
+ rescue => e
151
+ File.unlink(tmp_file) if File.exist?(tmp_file)
152
+
153
+ raise "#{I18n.t('error.mpw_file.write_data')}\n#{e}"
154
+ end
155
+
156
+ # Get a password
157
+ # args: id -> the item id
158
+ def get_password(id)
159
+ password = decrypt(@passwords[id])
160
+
161
+ if /^\$[a-zA-Z0-9]{4,9}::(?<password>.+)$/ =~ password
162
+ Regexp.last_match('password')
163
+ else
164
+ password
165
+ end
166
+ end
167
+
168
+ # Set a password
169
+ # args: id -> the item id
170
+ # password -> the new password
171
+ def set_password(id, password)
172
+ salt = MPW.password(length: Random.rand(4..9))
173
+ password = "$#{salt}::#{password}"
174
+
175
+ @passwords[id] = encrypt(password)
176
+ end
177
+
178
+ # Return the list of all gpg keys
179
+ # rtrn: an array with the gpg keys name
180
+ def list_keys
181
+ @keys.keys
182
+ end
183
+
184
+ # Add a public key
185
+ # args: key -> new public key file or name
186
+ def add_key(key)
187
+ if File.exist?(key)
188
+ data = File.open(key).read
189
+ key_import = GPGME::Key.import(data, armor: true)
190
+ key = GPGME::Key.get(key_import.imports[0].fpr).uids[0].email
191
+ else
192
+ data = GPGME::Key.export(key, armor: true).read
193
+ end
194
+
195
+ raise I18n.t('error.export_key') if data.to_s.empty?
196
+
197
+ @keys[key] = data
198
+ @passwords.each_key { |id| set_password(id, get_password(id)) }
199
+ @otp_keys.each_key { |id| set_otp_key(id, get_otp_key(id)) }
200
+ end
201
+
202
+ # Delete a public key
203
+ # args: key -> public key to delete
204
+ def delete_key(key)
205
+ @keys.delete(key)
206
+ @passwords.each_key { |id| set_password(id, get_password(id)) }
207
+ @otp_keys.each_key { |id| set_otp_key(id, get_otp_key(id)) }
208
+ end
209
+
210
+ # Add a new item
211
+ # @args: item -> Object MPW::Item
212
+ def add(item)
213
+ raise I18n.t('error.bad_class') unless item.instance_of?(Item)
214
+ raise I18n.t('error.empty') if item.empty?
215
+
216
+ @data.push(item)
217
+ end
218
+
219
+ # Search in some csv data
220
+ # @args: options -> a hash with paramaters
221
+ # @rtrn: a list with the resultat of the search
222
+ def list(**options)
223
+ result = []
224
+
225
+ search = options[:pattern].to_s.downcase
226
+ group = options[:group].to_s.downcase
227
+
228
+ @data.each do |item|
229
+ next if item.empty?
230
+ next unless group.empty? || group.eql?(item.group.to_s.downcase)
231
+
232
+ host = item.host.to_s.downcase
233
+ comment = item.comment.to_s.downcase
234
+
235
+ next unless host =~ /^.*#{search}.*$/ || comment =~ /^.*#{search}.*$/
236
+
237
+ result.push(item)
238
+ end
239
+
240
+ result
241
+ end
242
+
243
+ # Search in some csv data
244
+ # @args: id -> the id item
245
+ # @rtrn: a row with the result of the search
246
+ def search_by_id(id)
247
+ @data.each do |item|
248
+ return item if item.id == id
249
+ end
250
+
251
+ nil
252
+ end
253
+
254
+ # Set an opt key
255
+ # args: id -> the item id
256
+ # key -> the new key
257
+ def set_otp_key(id, key)
258
+ @otp_keys[id] = encrypt(key.to_s) unless key.to_s.empty?
259
+ end
260
+
261
+ # Get an opt key
262
+ # args: id -> the item id
263
+ # key -> the new key
264
+ def get_otp_key(id)
265
+ @otp_keys.key?(id) ? decrypt(@otp_keys[id]) : nil
266
+ end
267
+
268
+ # Get an otp code
269
+ # @args: id -> the item id
270
+ # @rtrn: an otp code
271
+ def get_otp_code(id)
272
+ @otp_keys.key?(id) ? 0 : ROTP::TOTP.new(decrypt(@otp_keys[id])).now
273
+ end
274
+
275
+ # Get remaining time before expire otp code
276
+ # @rtrn: return time in seconde
277
+ def get_otp_remaining_time
278
+ (Time.now.utc.to_i / 30 + 1) * 30 - Time.now.utc.to_i
279
+ end
280
+
281
+ # Generate a random password
282
+ # @args: options -> :length, :special, :alpha, :numeric
283
+ # @rtrn: a random string
284
+ def self.password(**options)
285
+ length =
286
+ if !options.include?(:length) || options[:length].to_i <= 0
287
+ 8
288
+ elsif options[:length].to_i >= 32_768
289
+ 32_768
290
+ else
291
+ options[:length].to_i
292
+ end
293
+
294
+ chars = []
295
+ chars += [*('!'..'?')] - [*('0'..'9')] if options[:special]
296
+ chars += [*('A'..'Z'), *('a'..'z')] if options[:alpha]
297
+ chars += [*('0'..'9')] if options[:numeric]
298
+ chars = [*('A'..'Z'), *('a'..'z'), *('0'..'9')] if chars.empty?
299
+
300
+ result = ''
301
+ while length > 62
302
+ result << chars.sample(62).join
303
+ length -= 62
304
+ end
305
+ result << chars.sample(length).join
306
+
307
+ result
308
+ end
309
+
310
+ private
311
+
312
+ # Decrypt a gpg file
313
+ # @args: data -> string to decrypt
314
+ def decrypt(data)
315
+ return nil if data.to_s.empty?
316
+
317
+ password =
318
+ if /^(1\.[0-9.]+|2\.0)(\.[0-9]+)?/ =~ GPGME::Engine.info.first.version || @pinmode
319
+ { password: @gpg_pass }
320
+ else
321
+ { password: @gpg_pass,
322
+ pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK }
323
+ end
324
+
325
+ crypto = GPGME::Crypto.new(armor: true)
326
+ crypto
327
+ .decrypt(data, password)
328
+ .read.force_encoding('utf-8')
329
+ rescue => e
330
+ raise "#{I18n.t('error.gpg_file.decrypt')}\n#{e}"
331
+ end
332
+
333
+ # Encrypt a file
334
+ # args: data -> string to encrypt
335
+ def encrypt(data)
336
+ recipients = []
337
+ crypto = GPGME::Crypto.new(armor: true, always_trust: true)
338
+
339
+ recipients.push(@key)
340
+ @keys.each_key do |key|
341
+ next if key == @key
342
+ recipients.push(key)
343
+ end
344
+
345
+ crypto.encrypt(data, recipients: recipients).read
346
+ rescue => e
347
+ raise "#{I18n.t('error.gpg_file.encrypt')}\n#{e}"
348
+ end
349
+ end
487
350
  end