class Chef::EncryptedAttribute::EncryptedMash::Version0

EncryptedMash Version0 format: using RSA without shared secret.

This is the first version, considered old. Uses public key cryptography (PKI) to encrypt the data. There is no shared secret or HMAC for data integrity checking.

# `EncryptedMash::Version0` Structure

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

“` EncryptedMash └── encrypted_data

├── pub_key_hash1: The data encrypted using PKI for the public key 1
│     (base64)
├── pub_key_hash2: The data encrypted using PKI for the public key 2
│     (base64)
└── ...

“`

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

Its content is the data encoded in JSON, then encrypted with the public key, and finally encoded in base64. The encryption is done using the RSA algorithm (PKI).

@see EncryptedMash

Public Instance Methods

can_be_decrypted_by?(keys) click to toggle source

Checks if the current {EncryptedMash} can be decrypted by all of the provided keys.

@param keys [Array<OpenSSL::PKey::RSA>] list of public keys. @return [Boolean] `true` if all keys can decrypt the data. @raise [InvalidPublicKey] if it is not a valid RSA public key. @raise [InvalidKey] if the RSA key format is wrong.

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

Decrypts the current {EncryptedMash} object.

@param key [String, OpenSSL::PKey::RSA] RSA private key used to

decrypt.

@return [Mixed] the value decrypted. @raise [DecryptionFailure] if the data cannot be decrypted by the

provided key.

@raise [InvalidPublicKey] if it is not a valid RSA public key. @raise [InvalidKey] if the RSA key format is wrong.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 88
def decrypt(key)
  key = parse_decryption_key(key)
  value_json = rsa_decrypt_multi_key(self['encrypted_data'], key)
  json_decode(value_json)
  # we avoid saving the decrypted value, only return it
end
encrypt(value, public_keys) click to toggle source

Encrypts data inside the current {EncryptedMash} object.

@param value [Mixed] value to encrypt, will be converted to JSON. @param public_keys [Array<String, OpenSSL::PKey::RSA>] publics keys

that will be able to decrypt the {EncryptedMash}.

@return [EncryptedMash] the value encrypted. @raise [EncryptionFailure] if there are encryption errors. @raise [InvalidPublicKey] if it is not a valid RSA public key. @raise [InvalidKey] if the RSA key format is wrong.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 71
def encrypt(value, public_keys)
  value_json = json_encode(value)
  public_keys = parse_public_keys(public_keys)
  self['encrypted_data'] =
    rsa_encrypt_multi_key(value_json, public_keys)
  self
end
needs_update?(keys) click to toggle source

Checks if the current {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.

@param keys [Array<String, OpenSSL::PKey::RSA>] list of RSA public

keys.

@return [Boolean] `true` if all keys can decrypt the data and only

those keys can decrypt the data.

@raise [InvalidPublicKey] if it is not a valid RSA public key. @raise [InvalidKey] if the RSA key format is wrong.

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

Protected Instance Methods

data_can_be_decrypted_by_key?(enc_value, key) click to toggle source

Checks if data can be decrypted by the provided key. Where data is encrypted for multiple keys.

This method is not immune to any kind of data corruption. Only checks that the data seems to be decipherable by the key. No MAC checking.

@param enc_value [Mash] encrypted data structure. @param key [OpenSSL::PKey::RSA] RSA key. @return [Boolean] `true` if the data can be decrypted. @see rsa_encrypt_multi_key

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 333
def data_can_be_decrypted_by_key?(enc_value, key)
  enc_value.key?(node_key(key.public_key))
end
data_can_be_decrypted_by_keys?(data, keys) click to toggle source

Checks if the data can be decrypted by all of the provided keys.

@param data [Mash] encrypted data to check. This usually refers to

`self['encrypted_data']`.

@param keys [Array<OpenSSL::PKey::RSA>] list of public keys. @return [Boolean] `true` if all keys can decrypt the data. @raise [InvalidPublicKey] if it is not a valid RSA public key. @raise [InvalidKey] if the RSA key format is wrong.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 345
def data_can_be_decrypted_by_keys?(data, keys)
  parse_public_keys(keys).reduce(true) do |r, k|
    r && data_can_be_decrypted_by_key?(data, k)
  end
end
encrypted?() click to toggle source

Checks if encrypted data exists in the current Mash.

@return [Boolean] `true` if there is encrypted data.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 132
def encrypted?
  key?('encrypted_data') && self['encrypted_data'].is_a?(Hash)
end
json_decode(o) click to toggle source

Decodes a JSON string.

@param o [String] JSON string to decode. @return [Mixed] Ruby representation of the JSON string. @raise [DecryptionFailure] if JSON string format is wrong.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 213
def json_decode(o)
  YAJL_NAMESPACE::Parser.parse(o.to_s)
rescue YAJL_NAMESPACE::ParseError => e
  raise DecryptionFailure, "#{e.class.name}: #{e}"
end
json_encode(o) click to toggle source

Converts an object to its JSON representation.

@param o [Mixed] object to convert. @return [String] JSON object as string.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 202
def json_encode(o)
  # TODO: This does not check if the object is correct, should be an
  # Array or a Hash
  YAJL_NAMESPACE::Encoder.encode(o)
end
node_key(public_key) click to toggle source

Gets the hash key to use for saving the encrypted data for a node.

It uses a SHA1 hexadecimal digest of the public key as key.

@param public_key [OpenSSL::PKey::RSA] RSA public key. @return [String] hash key for the public key.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 262
def node_key(public_key)
  Digest::SHA1.hexdigest(rsa_ensure_x509(public_key).to_der)
end
parse_decryption_key(key) click to toggle source

Parses a RSA key used for decryption. Must contain both the public and the private key. It also checks that the current {EncryptedMash} object can be decrypted by the provided key.

@param key [String, OpenSSL::PKey::RSA] RSA key to parse. @return [OpenSSL::PKey::RSA] RSA key. @raise [DecryptionFailure] if the data cannot be decrypted by the

provided key.

@raise [InvalidPublicKey] if it is not a valid RSA public key. @raise [InvalidKey] if the RSA key format is wrong.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 171
def parse_decryption_key(key)
  key = pem_to_key(key)
  unless key.public? && key.private?
    fail InvalidKey,
         'The provided key for decryption is invalid, a valid public '\
         'and private key is required.'
  end
  # TODO: optimize, node key digest is calculated multiple times
  unless can_be_decrypted_by?(key)
    fail DecryptionFailure,
         'Attribute data cannot be decrypted by the provided key.'
  end
  key
end
parse_public_key(key) click to toggle source

Parses a RSA public key used for encryption.

@param key [String, OpenSSL::PKey::RSA] RSA key to parse. @return [OpenSSL::PKey::RSA] RSA public key. @raise [InvalidPublicKey] if it is not a valid RSA public key. @raise [InvalidKey] if the RSA key format is wrong.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 153
def parse_public_key(key)
  key = pem_to_key(key)
  unless key.public?
    fail InvalidPublicKey, 'Invalid public key provided.'
  end
  key
end
parse_public_keys(keys) click to toggle source

Parses a list of RSA public keys, used for encryption.

@param keys [Array<String, OpenSSL::PKey::RSA>] list of keys. @return [Array<OpenSSL::PKey::RSA>] list of keys parsed. @raise [InvalidPublicKey] if it is not a valid RSA public key. @raise [InvalidKey] if the RSA key format is wrong.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 192
def parse_public_keys(keys)
  keys = [keys].flatten
  keys_parsed = keys.map { |k| parse_public_key(k) }
  keys_parsed.uniq { |k| k.public_key.to_s.chomp }
end
pem_to_key(k) click to toggle source

Converts the RSA key to an `OpenSSL::PKey::RSA` object.

@param k [String, OpenSSL::PKey::RSA] RSA key to convert. @return [OpenSSL::PKey::RSA] RSA key. @raise [InvalidKey] if the RSA key format is wrong.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 141
def pem_to_key(k)
  k.is_a?(OpenSSL::PKey::RSA) ? k : OpenSSL::PKey::RSA.new(k)
rescue OpenSSL::PKey::RSAError, TypeError
  raise InvalidKey, "The provided key is invalid: #{k.inspect}"
end
rsa_decrypt_multi_key(enc_value, key) click to toggle source

Decrypts RSA value from a data structure encrypted for multiple keys.

@param enc_value [Mash] encrypted data structure. @param key [OpenSSL::PKey::RSA] RSA key to use (public and private key

is required).

@return [String] data decrypted. @see rsa_decrypt_value

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 318
def rsa_decrypt_multi_key(enc_value, key)
  enc_value = enc_value[node_key(key.public_key)]
  rsa_decrypt_value(enc_value, key)
end
rsa_decrypt_value(value, key) click to toggle source

Decrypts a value using a RSA private key.

@param value [String] encrypted data to decrypt in its Base64

representation.

@param key [OpenSSL::PKey::RSA] private key used for decryption. @return [String] value decrypted. @raise [DecryptionFailure] if there are decryption errors.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 285
def rsa_decrypt_value(value, key)
  key.private_decrypt(Base64.decode64(value.to_s))
rescue OpenSSL::PKey::RSAError => e
  raise DecryptionFailure, "#{e.class.name}: #{e}"
end
rsa_encrypt_multi_key(value, public_keys) click to toggle source

Returns data encrypted for multiple keys using RSA.

Returns a `Mash` with the following structure:

  • Hash keys: hexadecimal SHA1 of the public key.

  • Hash values: RSA encrypted data and then converted to Base64.

@param value [String] data to encrypt. @param public_keys [Array<OpenSSL::PKey::RSA>] public keys list. @return [Mash] data encrypted. @raise [EncryptionFailure] if there are encryption errors. @see node_key @see rsa_encrypt_value

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 303
def rsa_encrypt_multi_key(value, public_keys)
  Mash.new(Hash[
    public_keys.map do |public_key|
      [node_key(public_key), rsa_encrypt_value(value, public_key)]
    end
 ])
end
rsa_encrypt_value(value, public_key) click to toggle source

Encrypts a value using a RSA public key.

@param value [String] data to encrypt. @param public_key [OpenSSL::PKey::RSA] public key used for encryption. @return [String] data encrypted in its Base64 representation. @raise [EncryptionFailure] if there are encryption errors.

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 272
def rsa_encrypt_value(value, public_key)
  Base64.encode64(public_key.public_encrypt(value))
rescue OpenSSL::PKey::RSAError => e
  raise EncryptionFailure, "#{e.class.name}: #{e}"
end
rsa_ensure_x509(rsa) click to toggle source

Returns any RSA key in X.509 format.

Fixes RSA key format in Ruby `< 1.9.3`.

@param rsa [OpenSSL::PKey::RSA] RSA key. @return [OpenSSL::ASN1::Sequence] RSA key in X.509 format. @see rsa_ensure_x509_ruby192

# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 252
def rsa_ensure_x509(rsa)
  RUBY_VERSION < '1.9.3' ? rsa_ensure_x509_ruby192(rsa) : rsa
end
rsa_ensure_x509_ruby192(rsa) click to toggle source

Encodes Ruby `< 1.9.3` RSA key using X.509 format.

In Ruby `< 1.9.3` RSA keys are in [PKCS#1] (en.wikipedia.org/wiki/PKCS_1) format.

In Ruby `>= 1.9.3` RSA keys are in [X.509] (en.wikipedia.org/wiki/X.509) format (private keys in [PKCS#8] (en.wikipedia.org/wiki/PKCS_8)).

@param rsa [OpenSSL::PKey::RSA] RSA key. @return [OpenSSL::ASN1::Sequence] RSA key in X.509 format. @note Heavily based on @sl4m code:

https://gist.github.com/sl4m/1470360
# File lib/chef/encrypted_attribute/encrypted_mash/version0.rb, line 232
def rsa_ensure_x509_ruby192(rsa)
  modulus = rsa.n
  exponent = rsa.e

  asn1 = OpenSSL::ASN1
  oid = asn1::ObjectId.new('rsaEncryption')
  alg_id = asn1::Sequence.new([oid, asn1::Null.new(nil)])
  ary = [asn1::Integer.new(modulus), asn1::Integer.new(exponent)]
  pub_key = asn1::Sequence.new(ary)
  enc_pk = asn1::BitString.new(pub_key.to_der)
  asn1::Sequence.new([alg_id, enc_pk])
end