class Chef::EncryptedAttribute::EncryptedMash::Version1

EncryptedMash Version1 format: using RSA with a shared secret and message authentication (HMAC).

This is the {EncryptedMash} version used by default. Uses public key cryptography (PKI) to encrypt a shared secret. Then this shared secret is used to encrypt the data.

# `EncryptedMash::Version1` 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-cbc” (string). │ ├── data: PKI encrypted data (base64). │ └── iv: Initialization vector (in base64). ├── encrypted_secret │ ├── pub_key_hash1: The shared secrets encrypted for the public key 1 │ │ (base64). │ ├── pub_key_hash2: The shared secrets encrypted for the public key 2 │ │ (base64). │ └── … └── hmac

├── cipher: The used HMAC algorithm, currently ignored and always
│     "sha256" (string).
└── data: Hash-based message authentication code value (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-cbc'` 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 secrets in base64. The encryption is done using the RSA algorithm (PKI).

After decryption, you find the following structure in JSON:

“` └── encrypted_secret

└── pub_key_hash1 (PKI encrypted JSON in base64)
    ├── data: The shared secret used to encrypt the data (base64).
    └── hmac: The shared secret used for the HMAC calculation
          (base64).

“`

## `EncryptedMash[data]`

The HMAC data is in base64. The hashing algorithm used is `'sha256'`.

The following data is used in a alphabetically sorted JSON to calculate the HMAC:

“` Data to calculate the HMAC from ├── cipher: The algorithm used for `encrypted_data` encryption │ (“aes-256-cbc”). ├── data: The `encrypted_data` data content after the encryption │ (encrypt-then-mac). └── iv: The initialization vector used to encrypt the encrypted_data. “`

@see EncryptedMash

Constants

HMAC_ALGORITHM

Algorithm used for HMAC calculation.

HMAC_EXCEPTIONS

Exception list raised by the HMAC calculation process.

`RuntimeError` is raised for unsupported algorithms.

@api private

SYMM_ALGORITHM

Symmetric algorithm to use by default.

Public Instance Methods

can_be_decrypted_by?(keys) click to toggle source

(see EncryptedMash::Version0#can_be_decrypted_by?)

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 178
def can_be_decrypted_by?(keys)
  return false unless encrypted?
  data_can_be_decrypted_by_keys?(self['encrypted_secret'], keys)
end
decrypt(key) click to toggle source

(see EncryptedMash::Version0#decrypt) @raise [MessageAuthenticationFailure] if HMAC calculation error.

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 157
def decrypt(key)
  key = parse_decryption_key(key)
  enc_value = self['encrypted_data'].dup
  hmac = self['hmac'].dup
  # decrypt the shared secrets
  secrets =
    json_decode(rsa_decrypt_multi_key(self['encrypted_secret'], key))
  enc_value['secret'] = secrets['data']
  hmac['secret'] = secrets['hmac']
  # check hmac (encrypt-then-mac -> mac-then-decrypt)
  unless hmac_matches?(hmac, json_encode(self['encrypted_data'].sort))
    fail DecryptionFailure,
         'Error decrypting encrypted attribute: invalid hmac. Most '\
         'likely the data is corrupted.'
  end
  # 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::Version0#encrypt) @raise [MessageAuthenticationFailure] if HMAC calculation error.

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 136
def encrypt(value, public_keys)
  secrets = {}
  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
  secrets['data'] = encrypted_data.delete('secret')
  self['encrypted_data'] = encrypted_data
  # generate hmac (encrypt-then-mac), excluding the secret
  hmac = generate_hmac(json_encode(self['encrypted_data'].sort))
  secrets['hmac'] = hmac.delete('secret')
  self['hmac'] = hmac
  # encrypt the shared secrets
  self['encrypted_secret'] =
    rsa_encrypt_multi_key(json_encode(secrets), public_keys)
  self
end
needs_update?(keys) click to toggle source

(see EncryptedMash::Version0#needs_update?)

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 184
def needs_update?(keys)
  keys = parse_public_keys(keys)
  !can_be_decrypted_by?(keys) ||
    self['encrypted_secret'].keys.count != keys.count
end

Protected Instance Methods

encrypted?() click to toggle source

(see EncryptedMash::Version0#encrypted?)

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 237
def encrypted?
  super &&
    encrypted_data? &&
    encrypted_secret? &&
    encrypted_hmac?
end
encrypted_data?() click to toggle source

Checks if the encrypted data structure is correct.

Checks the `self` structure.

@return [Boolean] `true` if it is correct.

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 212
def encrypted_data?
  encrypted_data_contain_fields?(iv: String, data: String)
end
encrypted_data_contain_fields?(fields) click to toggle source

Checks if the encrypted data Mash contains all the fields an has the correct type.

Checks the `self` structure.

@param [Hash<Symbol, Class>] fields to check. For example:

`{ iv: String, data: String }`.

@return [Boolean] `true` if all the fields are exists.

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 200
def encrypted_data_contain_fields?(fields)
  data = self['encrypted_data']
  fields.reduce(true) do |r, (field, kind_of)|
    r && data.key?(field) && data[field].is_a?(kind_of)
  end
end
encrypted_hmac?() click to toggle source

Checks if the HMAC structure is correct.

Checks the `self` structure.

@return [Boolean] `true` if it is correct.

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 230
def encrypted_hmac?
  self['hmac'].is_a?(Hash) &&
    self['hmac'].key?('data') &&
    self['hmac']['data'].is_a?(String)
end
encrypted_secret?() click to toggle source

Checks if the encrypted secrets structure is correct.

Checks the `self` structure.

@return [Boolean] `true` if it is correct.

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 221
def encrypted_secret?
  self['encrypted_secret'].is_a?(Hash)
end
generate_hmac(data, algo = HMAC_ALGORITHM) click to toggle source

Calculates [HMAC] (en.wikipedia.org/wiki/Hash-based_message_authentication_code) value for data.

Uses a randomly generated secret for the HMAC calculation.

@param data [String] data to calculate HMAC for. @param algo [String] HMAC algorithm to use. @return [Mash] hash structure with HMAC data:

* `['cipher']`: algorithm used.
* `['secret']` random secret used for HMAC calculation in Base64.
* `['data']` HMAC value in Base64.

@raise [MessageAuthenticationFailure] if HMAC calculation error.

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 315
def generate_hmac(data, algo = HMAC_ALGORITHM)
  # [cipher] is ignored, only as info
  hmac = Mash.new('cipher' => algo)
  digest = OpenSSL::Digest.new(algo)
  secret = OpenSSL::Random.random_bytes(digest.block_length)
  hmac['secret'] = Base64.encode64(secret)
  hmac['data'] =
    Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data))
  hmac
rescue *HMAC_EXCEPTIONS => e
  raise MessageAuthenticationFailure, "#{e.class}: #{e}"
end
hmac_matches?(orig_hmac, data, algo = HMAC_ALGORITHM) click to toggle source

Checks if the [HMAC] (en.wikipedia.org/wiki/Hash-based_message_authentication_code) matches

Uses a randomly generated secret for the HMAC calculation.

@param orig_hmac [Mash] hash structure with HMAC data:

* `['cipher']`: algorithm used (this is ignored).
* `['secret']` secret used for HMAC calculation in Base64.
* `['data']` HMAC value in Base64.

@param data [String] data to calculate HMAC for. @param algo [String] HMAC algorithm to use. @return [Boolean] `true` if HMAC value matches. @raise [MessageAuthenticationFailure] if HMAC calculation error. @see generate_hmac

# File lib/chef/encrypted_attribute/encrypted_mash/version1.rb, line 343
def hmac_matches?(orig_hmac, data, algo = HMAC_ALGORITHM)
  digest = OpenSSL::Digest.new(algo)
  secret = Base64.decode64(orig_hmac['secret'])
  new_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data))
  orig_hmac['data'] == new_hmac
rescue *HMAC_EXCEPTIONS => e
  raise MessageAuthenticationFailure, "#{e.class}: #{e}"
end
symmetric_decrypt_value(enc_value, algo = SYMM_ALGORITHM) click to toggle source

Decrypts data using a symmetric 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.
* `['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/version1.rb, line 282
def symmetric_decrypt_value(enc_value, algo = SYMM_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 = Base64.decode64(enc_value['secret'])
  cipher.iv = Base64.decode64(enc_value['iv'])
  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 = SYMM_ALGORITHM) click to toggle source

Encrypts a value using a symmetric 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.
* `['data']`: data encrypted and in Base64.

@raise [EncryptionFailure] if encryption error.

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