Class: Chef::EncryptedAttribute::EncryptedMash::Version1

Inherits:
Version0 show all
Defined in:
lib/chef/encrypted_attribute/encrypted_mash/version1.rb

Overview

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

This is the Chef::EncryptedAttribute::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.

  • This implementation can be improved, is not optimized either for performance or for space.
  • Every time the EncryptedAttribute is updated, all the shared secrets are regenerated.

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).
  • x_json_class field is used, with the x_ prefix, to be easily integrated with Chef in the future.

EncryptedMash[encrypted_data][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.
  • In the future, this structure may contain some metadata like default configuration values.

EncryptedMash[encrypted_secret][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[hmac][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.
  • All the data required for decryption is included in the HMAC (except the secret key, of course): cipher, data and iv.
  • The data used to calculate the HMAC is the encrypted data, not the clear text data (Encrypt-then-MAC).
  • The secret used to calculate the HMAC is not the same as the secret used to encrypt the data.
  • The secret used to calculate the HMAC is shared inside encrypted_secret field with the data secret.

Direct Known Subclasses

Version2

Constant Summary collapse

SYMM_ALGORITHM =

Symmetric algorithm to use by default.

'aes-256-cbc'
HMAC_ALGORITHM =

Algorithm used for HMAC calculation.

'sha256'

Constants inherited from Chef::EncryptedAttribute::EncryptedMash

CHEF_TYPE, CHEF_TYPE_VALUE, JSON_CLASS, VERSION_PREFIX

Instance Method Summary collapse

Methods inherited from Chef::EncryptedAttribute::EncryptedMash

create, exist?, exists?, #for_json, #initialize, json_create, string_to_klass, #to_json, #update_from!, version_klass

Constructor Details

This class inherits a constructor from Chef::EncryptedAttribute::EncryptedMash

Instance Method Details

#can_be_decrypted_by?(keys) ⇒ Boolean

Checks if the current Chef::EncryptedAttribute::EncryptedMash can be decrypted by all of the provided keys.

Parameters:

  • keys (Array<OpenSSL::PKey::RSA>)

    list of public keys.

Returns:

  • (Boolean)

    true if all keys can decrypt the data.

Raises:



178
179
180
181
# 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) ⇒ Mixed

Decrypts the current Chef::EncryptedAttribute::EncryptedMash object.

Parameters:

  • key (String, OpenSSL::PKey::RSA)

    RSA private key used to decrypt.

Returns:

  • (Mixed)

    the value decrypted.

Raises:



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# 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) ⇒ EncryptedMash

Encrypts data inside the current Chef::EncryptedAttribute::EncryptedMash object.

Parameters:

  • value (Mixed)

    value to encrypt, will be converted to JSON.

  • public_keys (Array<String, OpenSSL::PKey::RSA>)

    publics keys that will be able to decrypt the Chef::EncryptedAttribute::EncryptedMash.

Returns:

Raises:



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# 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) ⇒ Boolean

Checks if the current Chef::EncryptedAttribute::EncryptedMash needs to be re-encrypted.

This usually happends when new keys are provided or some keys are removed from the previous encryption process.

In other words, this method checks all key can decrypt the data and only those keys.

Parameters:

  • keys (Array<String, OpenSSL::PKey::RSA>)

    list of RSA public keys.

Returns:

  • (Boolean)

    true if all keys can decrypt the data and only those keys can decrypt the data.

Raises:



184
185
186
187
188
# 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