class Chef::EncryptedAttribute::EncryptedMash::Version2

EncryptedMash Version2 format: using RSA with a shared secret and GCM.

Uses public key cryptography (PKI) to encrypt a shared secret. Then this shared secret is used to encrypt the data using [GCM] (en.wikipedia.org/wiki/Galois/Counter_Mode).

# `EncryptedMash::Version2` Structure

If you try to read this encrypted attribute structure, you can see a `Mash` attribute with the following content:

“` EncryptedMash ├── chef_type: “encrypted_attribute” (string). ├── x_json_class: The used `EncryptedMash` version class name (string). ├── encrypted_data │ ├── cipher: The used PKI algorithm, “aes-256-gcm” (string). │ ├── data: PKI encrypted data (base64). │ ├── auth_tag: GCM authentication tag (base64). │ └── iv: Initialization vector (in base64). └── encrypted_secret

├── pub_key_hash1: The shared secret encrypted for the public key 1
│     (base64).
├── pub_key_hash2: The shared secret encrypted for the public key 2
│     (base64).
└── ...

“`

## `EncryptedMash[data]`

The data inside `encrypted_data` is symmetrically encrypted using the secret shared key. The data is converted to JSON before the encryption, then encrypted and finally encoded in base64. By default, the `'aes-256-gcm'` algorithm is used for encryption.

After decryption, the JSON has the following structure:

“` └── encrypted_data

└── data (symmetrically encrypted JSON in base64)
    └── content: attribute content as a Mash.

“`

## `EncryptedMash[pub_key_hash1]`

The `public_key_hash1` key value is the SHA1 of the public key used for encryption.

Its content is the encrypted shared secret in raw. The encryption is done using the RSA algorithm (PKI).

After decryption, you find the shared secret in raw (in Version1 this is a JSON in base64).

@see EncryptedMash

Constants

ALGORITHM

Symmetric [AEAD] (en.wikipedia.org/wiki/AEAD_block_cipher_modes_of_operation) algorithm to use by default.

Public Class Methods

new(enc_hs = nil) click to toggle source

EncrytpedMash::Version2 constructor.

Checks that GCM is correctly supported by Ruby and OpenSSL.

@raise [RequirementsFailure] if the specified encrypted attribute

version cannot be used.
# File lib/chef/encrypted_attribute/encrypted_mash/version2.rb, line 112
def initialize(enc_hs = nil)
  assert_aead_requirements_met!(ALGORITHM)
  super
end

Public Instance Methods

decrypt(key) click to toggle source

(see EncryptedMash::Version1#decrypt)

# File lib/chef/encrypted_attribute/encrypted_mash/version2.rb, line 132
def decrypt(key)
  key = parse_decryption_key(key)
  enc_value = self['encrypted_data'].dup
  # decrypt the shared secret
  enc_value['secret'] =
    rsa_decrypt_multi_key(self['encrypted_secret'], key)
  # decrypt the data
  value_json = symmetric_decrypt_value(enc_value)
  json_decode(value_json)
end
encrypt(value, public_keys) click to toggle source

(see EncryptedMash::Version1#encrypt)

# File lib/chef/encrypted_attribute/encrypted_mash/version2.rb, line 118
def encrypt(value, public_keys)
  value_json = json_encode(value)
  public_keys = parse_public_keys(public_keys)
  # encrypt the data
  encrypted_data = symmetric_encrypt_value(value_json)
  # should no include the secret in clear
  secret = encrypted_data.delete('secret')
  self['encrypted_data'] = encrypted_data
  # encrypt the shared secret
  self['encrypted_secret'] = rsa_encrypt_multi_key(secret, public_keys)
  self
end

Protected Instance Methods

encrypted?() click to toggle source

(see EncryptedMash::Version1#encrypted?)

# File lib/chef/encrypted_attribute/encrypted_mash/version2.rb, line 153
def encrypted?
  Version0.instance_method(:encrypted?).bind(self).call &&
    encrypted_data? &&
    encrypted_secret?
end
encrypted_data?() click to toggle source

(see EncryptedMash::Version1#encrypted_data?)

# File lib/chef/encrypted_attribute/encrypted_mash/version2.rb, line 146
def encrypted_data?
  encrypted_data_contain_fields?(
    iv: String, auth_tag: String, data: String
  )
end
symmetric_decrypt_value(enc_value, algo = ALGORITHM) click to toggle source

Decrypts data using a symmetric [AEAD] (en.wikipedia.org/wiki/AEAD_block_cipher_modes_of_operation) cryptographic algorithm.

@param enc_value [Mash] hash structure with encrypted data:

* `['cipher']`: algorithm used.
* `['secret']`: secret used for encryption in Base64.
* `['iv']`: initialization vector in Base64.
* `['auth_tag']`: authentication tag in Base64.
* `['data']`: data encrypted in Base64.

@param algo [String] symmetric algorithm to use. @raise [DecryptionFailure] if decryption error. @see symmetric_encrypt_value

# File lib/chef/encrypted_attribute/encrypted_mash/version2.rb, line 203
def symmetric_decrypt_value(enc_value, algo = ALGORITHM)
  # TODO: maybe it's better to ignore [cipher] ?
  cipher = OpenSSL::Cipher.new(enc_value['cipher'] || algo)
  cipher.decrypt
  # We must set key before iv: https://bugs.ruby-lang.org/issues/8221
  cipher.key = enc_value['secret']
  cipher.iv = Base64.decode64(enc_value['iv'])
  cipher.auth_tag = Base64.decode64(enc_value['auth_tag'])
  cipher.update(Base64.decode64(enc_value['data'])) + cipher.final
rescue OpenSSL::Cipher::CipherError => e
  raise DecryptionFailure, "#{e.class.name}: #{e}"
end
symmetric_encrypt_value(value, algo = ALGORITHM) click to toggle source

Encrypts a value using a symmetric [AEAD] (en.wikipedia.org/wiki/AEAD_block_cipher_modes_of_operation) cryptographic algorithm.

Uses a randomly generated secret and IV.

@param value [String] data to encrypt. @param algo [String] symmetric algorithm to use. @return [Mash] hash structure with symmetrically encrypted data:

* `['cipher']`: algorithm used.
* `['secret']`: random secret used for encryption in Base64.
* `['iv']`: random initialization vector in Base64.
* `['auth_tag']`: authentication tag in Base64.
* `['data']`: data encrypted and in Base64.

@raise [EncryptionFailure] if encryption error.

# File lib/chef/encrypted_attribute/encrypted_mash/version2.rb, line 174
def symmetric_encrypt_value(value, algo = ALGORITHM)
  enc_value = Mash.new('cipher' => algo)
  begin
    cipher = OpenSSL::Cipher.new(algo)
    cipher.encrypt
    enc_value['secret'] = cipher.key = cipher.random_key
    enc_value['iv'] = Base64.encode64(cipher.iv = cipher.random_iv)
    enc_data = cipher.update(value) + cipher.final
    enc_value['auth_tag'] = Base64.encode64(cipher.auth_tag)
  rescue OpenSSL::Cipher::CipherError => e
    raise EncryptionFailure, "#{e.class.name}: #{e}"
  end
  enc_value['data'] = Base64.encode64(enc_data)
  enc_value
end