Module: PWN::Plugins::Vault

Defined in:
lib/pwn/plugins/vault.rb

Overview

Used to encrypt/decrypt configuration files leveraging AES256

Constant Summary collapse

CREDENTIAL_KDF_ITERATIONS =
600_000

Class Method Summary collapse

Class Method Details

.authorsObject

Author(s)

0day Inc. [email protected]



398
399
400
401
402
# File 'lib/pwn/plugins/vault.rb', line 398

public_class_method def self.authors
  "AUTHOR(S):
    0day Inc. <[email protected]>
  "
end

.create(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::Vault.create( file: 'required - encrypted file to create', decryptor_file: 'optional - file to save the key && iv values' )



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
# File 'lib/pwn/plugins/vault.rb', line 114

public_class_method def self.create(opts = {})
  file = opts[:file].to_s.scrub if File.exist?(opts[:file].to_s.scrub)
  decryptor_file = opts[:decryptor_file]

  cipher = OpenSSL::Cipher.new('aes-256-cbc')
  key = Base64.strict_encode64(cipher.random_key)
  iv = Base64.strict_encode64(cipher.random_iv)

  if decryptor_file
    decryptor_hash = { key: key, iv: iv }
    yaml_decryptor = YAML.dump(decryptor_hash).gsub(/^(\s*):/, '\1')
    File.write(decryptor_file, yaml_decryptor)
    # Change permissions to 400
    File.chmod(0o400, decryptor_file)
  else
    puts 'Please store the Key && IV in a secure location as they are required for decryption.'
    puts "Key: #{key}"
    puts "IV: #{iv}"
  end

  encrypt(
    file: file,
    key: key,
    iv: iv
  )
rescue StandardError => e
  raise e
end

.decrypt(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::Vault.decrypt( file: 'required - file to decrypt', key: 'required - key to decrypt', iv: 'required - iv to decrypt' )



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
# File 'lib/pwn/plugins/vault.rb', line 150

public_class_method def self.decrypt(opts = {})
  file = opts[:file].to_s.scrub if File.exist?(opts[:file].to_s.scrub)
  key = opts[:key] ||= PWN::Plugins::AuthenticationHelper.mask_password(
    prompt: 'Key'
  )

  iv = opts[:iv] ||= PWN::Plugins::AuthenticationHelper.mask_password(
    prompt: 'IV'
  )

  is_encrypted = file_encrypted?(file: file)
  raise 'ERROR: File is not encrypted.' unless is_encrypted

  cipher = OpenSSL::Cipher.new('aes-256-cbc')
  cipher.decrypt
  cipher.key = Base64.strict_decode64(key)
  cipher.iv = Base64.strict_decode64(iv)

  b64_decoded_file_contents = Base64.strict_decode64(File.read(file).chomp)
  plain_text = cipher.update(b64_decoded_file_contents) + cipher.final

  File.write(file, plain_text)
rescue ArgumentError
  raise 'ERROR: Incorrect Key or IV.'
rescue StandardError => e
  raise e
end

.dump(opts = {}) ⇒ Object

Supported Method Parameters

vault = PWN::Plugins::Vault.dump( file: 'required - file to dump', key: 'required - key to decrypt', iv: 'required - iv to decrypt', yaml: 'optional - dump as parsed yaml hash (default: true)' )



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/pwn/plugins/vault.rb', line 186

public_class_method def self.dump(opts = {})
  file = opts[:file].to_s.scrub if File.exist?(opts[:file].to_s.scrub)
  key = opts[:key] ||= PWN::Plugins::AuthenticationHelper.mask_password(
    prompt: 'Key'
  )

  iv = opts[:iv] ||= PWN::Plugins::AuthenticationHelper.mask_password(
    prompt: 'IV'
  )

  cipher = OpenSSL::Cipher.new('aes-256-cbc')
  cipher.decrypt
  cipher.key = Base64.strict_decode64(key)
  cipher.iv = Base64.strict_decode64(iv)
  bytes = Base64.strict_decode64(File.read(file).chomp)
  plaintext = cipher.update(bytes) + cipher.final
  opts[:yaml] == false ? plaintext : YAML.safe_load(plaintext, permitted_classes: [Symbol], aliases: true, symbolize_names: true)
rescue ArgumentError
  raise 'ERROR: Incorrect Key or IV.'
rescue StandardError => e
  raise e
end

.edit(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::Vault.edit( file: 'required - file to edit', key: 'required - key to decrypt', iv: 'required - iv to decrypt', editor: 'optional - editor to use (default: "/usr/bin/vim")' )



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
# File 'lib/pwn/plugins/vault.rb', line 217

public_class_method def self.edit(opts = {})
  file = opts[:file].to_s.scrub if File.exist?(opts[:file].to_s.scrub)
  key = opts[:key] ||= PWN::Plugins::AuthenticationHelper.mask_password(
    prompt: 'Key'
  )

  iv = opts[:iv] ||= PWN::Plugins::AuthenticationHelper.mask_password(
    prompt: 'IV'
  )

  editor = opts[:editor] ||= '/usr/bin/vim'

  raise 'ERROR: Editor not found.' unless File.exist?(editor)

  decrypt(
    file: file,
    key: key,
    iv: iv
  )

  # Get realtive editor in case aliases are used
  relative_editor = File.basename(editor)
  system(relative_editor, file)

  # If the Pry object exists, set refresh_config to true
  Pry.config.refresh_pwn_env = true if defined?(Pry)

  encrypt(
    file: file,
    key: key,
    iv: iv
  )
rescue ArgumentError
  raise 'ERROR: Incorrect Key or IV.'
rescue StandardError => e
  raise e
end

.encrypt(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::Vault.encrypt( file: 'required - file to encrypt', key: 'required - key to decrypt', iv: 'required - iv to decrypt' )



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/pwn/plugins/vault.rb', line 262

public_class_method def self.encrypt(opts = {})
  file = opts[:file].to_s.scrub if File.exist?(opts[:file].to_s.scrub)
  key = opts[:key] ||= PWN::Plugins::AuthenticationHelper.mask_password(
    prompt: 'Key'
  )

  iv = opts[:iv] ||= PWN::Plugins::AuthenticationHelper.mask_password(
    prompt: 'IV'
  )

  cipher = OpenSSL::Cipher.new('aes-256-cbc')
  cipher.encrypt
  cipher.key = Base64.strict_decode64(key)
  cipher.iv = Base64.strict_decode64(iv)

  data = File.read(file)
  encrypted = cipher.update(data) + cipher.final
  encrypted_string = Base64.strict_encode64(encrypted)

  File.write(file, "#{encrypted_string}\n")
rescue StandardError => e
  raise e
end

.expand(opts = {}) ⇒ Object



324
325
326
327
# File 'lib/pwn/plugins/vault.rb', line 324

public_class_method def self.expand(opts = {})
  text = opts[:text].to_s
  text.gsub(/\{\{vault:([^}]+)\}\}/) { fetch(label: Regexp.last_match(1).to_s.strip).to_s }
end

.fetch(opts = {}) ⇒ Object



314
315
316
317
318
319
320
321
322
# File 'lib/pwn/plugins/vault.rb', line 314

public_class_method def self.fetch(opts = {})
  label = opts[:label].to_s
  raise 'ERROR: label is required' if label.empty?

  row = load_box[label]
  return nil unless row

  decrypt_secret(row: row)
end

.file_encrypted?(opts = {}) ⇒ Boolean

Supported Method Parameters

PWN::Plugins::Vault.file_encrypted?( file: 'required - file to check if encrypted' )

Returns:

  • (Boolean)


290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/pwn/plugins/vault.rb', line 290

public_class_method def self.file_encrypted?(opts = {})
  file = opts[:file].to_s.scrub if File.exist?(opts[:file].to_s.scrub)

  raise 'ERROR: File does not exist.' unless File.exist?(file)

  file_contents = File.read(file).chomp
  file_contents.is_a?(String) && Base64.strict_encode64(Base64.strict_decode64(file_contents)) == file_contents
rescue ArgumentError
  false
rescue StandardError => e
  raise e
end

.helpObject

Display Usage for this Module



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
# File 'lib/pwn/plugins/vault.rb', line 406

public_class_method def self.help
  puts "USAGE:
    # Seal credentials in an authenticated AES256GCM envelope, without filesystem access.
    #{self}.seal_credentials(
      credentials: 'required - JSON-compatible credentials hash',
      passphrase: 'optional - required unless keyring is supplied',
      keyring: 'optional - callable OS keyring reader returning a raw 32-byte key',
      key_id: 'optional - keyring identifier, default pwn-ai-credentials'
    )
    # Open an authenticated envelope in memory; never writes plaintext.
    #{self}.open_credentials(
      envelope: 'required - sealed credentials hash',
      passphrase: 'optional - required for passphrase envelopes',
      keyring: 'optional - callable OS keyring reader for keyring envelopes'
    )
    # Run refresh encryption secrets and return its result
    #{self}.refresh_encryption_secrets(
      file: 'required - file to encrypt with new key and iv',
      key: 'required - key to decrypt',
      iv: 'required - iv to decrypt'
    )

    # Run create and return its result
    #{self}.create(
      file: 'required - encrypted file to create',
      decryptor_file: 'optional - file to save the key && iv values'
    )

    # Run decrypt and return its result
    #{self}.decrypt(
      file: 'required - file to decrypt',
      key: 'required - key to decrypt',
      iv: 'required - iv to decrypt'
    )

    # Run dump and return its result
    #{self}.dump(
      file: 'required - file to dump',
      key: 'required - key to decrypt',
      iv: 'required - iv to decrypt',
      yaml: 'optional - dump as parsed yaml hash (default: true)'
    )

    # Run edit and return its result
    #{self}.edit(
      file: 'required - file to edit',
      key: 'required - key to decrypt',
      iv: 'required - iv to decrypt',
      editor: 'optional - editor to use (default: /usr/bin/vim)'
    )

    # Run encrypt and return its result
    #{self}.encrypt(
      file: 'required - file to encrypt',
      key: 'required - key to decrypt',
      iv: 'required - iv to decrypt'
    )

    # Run file encrypted and return its result
    #{self}.file_encrypted?(
      file: 'required - file to check if encrypted'
    )

    # Store a secret outside the transcript (AES-GCM; key in ~/.pwn-vault.key).
    #{self}.store(
      label: 'required - vault label',
      secret: 'required - secret value'
    )

    # Fetch a stored secret by label.
    #{self}.fetch(
      label: 'required - vault label'
    )

    # Replace {{vault:label}} tokens with stored secrets.
    #{self}.expand(
      text: 'required - string possibly containing {{vault:label}} tokens'
    )

    # Replace stored secret values with {{vault:label}} placeholders.
    #{self}.redact(
      text: 'required - string that may contain stored secrets'
    )

    # Print the AUTHOR(S) string for this module.
    #{self}.authors
  "
  constants.sort
end

.open_credentials(opts = {}) ⇒ Object

Decrypts in memory only. Does not modify or open any credential file.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/pwn/plugins/vault.rb', line 35

public_class_method def self.open_credentials(opts = {})
  envelope = opts.fetch(:envelope).transform_keys(&:to_s)
  raise ArgumentError, 'Unsupported credential envelope' unless envelope['version'] == 1 && envelope['cipher'] == 'aes-256-gcm'

  cipher = OpenSSL::Cipher.new('aes-256-gcm')
  cipher.decrypt
  cipher.key = credential_key(opts.merge(envelope: envelope))
  cipher.iv = Base64.strict_decode64(envelope.fetch('iv'))
  tag = Base64.strict_decode64(envelope.fetch('tag'))
  raise ArgumentError, 'Invalid credential authentication tag' unless tag.bytesize == 16

  cipher.auth_tag = tag
  cipher.auth_data = credential_aad(envelope: envelope)
  JSON.parse(cipher.update(Base64.strict_decode64(envelope.fetch('ct'))) + cipher.final)
rescue OpenSSL::Cipher::CipherError, JSON::ParserError
  raise ArgumentError, 'Credential authentication failed'
end

.redact(opts = {}) ⇒ Object



329
330
331
332
333
334
335
336
337
338
# File 'lib/pwn/plugins/vault.rb', line 329

public_class_method def self.redact(opts = {})
  text = opts[:text].to_s
  load_box.each do |label, row|
    val = decrypt_secret(row: row).to_s
    next if val.empty?

    text = text.gsub(val, "{{vault:#{label}}}")
  end
  text
end

.refresh_encryption_secrets(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::Vault.refresh_encryption_secrets( file: 'required - file to encrypt with new key and iv', key: 'required - key to decrypt', iv: 'required - iv to decrypt' )



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/pwn/plugins/vault.rb', line 88

public_class_method def self.refresh_encryption_secrets(opts = {})
  file = opts[:file].to_s.scrub if File.exist?(opts[:file].to_s.scrub)
  key = opts[:key]
  iv = opts[:iv]

  decrypt(
    file: file,
    key: key,
    iv: iv
  )

  create(
    file: file
  )
rescue ArgumentError
  raise 'ERROR: Incorrect Key or IV.'
rescue StandardError => e
  raise e
end

.seal_credentials(opts = {}) ⇒ Object

Returns an authenticated envelope; the caller persists only this value. keyring is an injected OS-keyring reader responding to call(key_id).



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/pwn/plugins/vault.rb', line 17

public_class_method def self.seal_credentials(opts = {})
  envelope = { 'version' => 1, 'cipher' => 'aes-256-gcm',
               'kdf' => opts[:keyring] ? 'keyring' : 'pbkdf2-sha256',
               'key_id' => opts[:key_id] || 'pwn-ai-credentials',
               'iterations' => CREDENTIAL_KDF_ITERATIONS,
               'salt' => Base64.strict_encode64(OpenSSL::Random.random_bytes(16)) }
  cipher = OpenSSL::Cipher.new('aes-256-gcm')
  cipher.encrypt
  cipher.key = credential_key(opts.merge(envelope: envelope))
  envelope['iv'] = Base64.strict_encode64(cipher.random_iv)
  cipher.auth_data = credential_aad(envelope: envelope)
  plaintext = JSON.generate(opts.fetch(:credentials))
  envelope['ct'] = Base64.strict_encode64(cipher.update(plaintext) + cipher.final)
  envelope['tag'] = Base64.strict_encode64(cipher.auth_tag)
  envelope
end

.store(opts = {}) ⇒ Object



303
304
305
306
307
308
309
310
311
312
# File 'lib/pwn/plugins/vault.rb', line 303

public_class_method def self.store(opts = {})
  label = opts[:label].to_s
  secret = opts[:secret].to_s
  raise 'ERROR: label and secret are required' if label.empty? || secret.empty?

  box = load_box
  box[label] = encrypt_secret(secret: secret)
  save_box(box: box)
  { label: label, stored: true }
end