zuazo/chef-encrypted-attributes

View on GitHub
lib/chef/encrypted_attribute/encrypted_mash/version0.rb

Summary

Maintainability
A
0 mins
Test Coverage
# encoding: UTF-8
#
# Author:: Xabier de Zuazo (<xabier@zuazo.org>)
# Copyright:: Copyright (c) 2014 Onddo Labs, SL.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'chef/version'
require 'chef/encrypted_attribute/encrypted_mash'
require 'chef/encrypted_attribute/exceptions'
require 'chef/encrypted_attribute/yajl'

# Use the YAJL library that Chef provides.  Determine which to use based on
# Chef version.
YAJL_NAMESPACE = Chef::EncryptedAttribute::Yajl.load_requirement(Chef::VERSION)

class Chef
  class EncryptedAttribute
    class EncryptedMash
      # 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
      class Version0 < Chef::EncryptedAttribute::EncryptedMash
        # 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.
        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

        # 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.
        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

        # 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.
        def can_be_decrypted_by?(keys)
          return false unless encrypted?
          data_can_be_decrypted_by_keys?(self['encrypted_data'], keys)
        end

        # 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.
        def needs_update?(keys)
          keys = parse_public_keys(keys)
          !can_be_decrypted_by?(keys) ||
            self['encrypted_data'].keys.count != keys.count
        end

        protected

        # Checks if encrypted data exists in the current Mash.
        #
        # @return [Boolean] `true` if there is encrypted data.
        def encrypted?
          key?('encrypted_data') && self['encrypted_data'].is_a?(Hash)
        end

        # 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.
        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

        # 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.
        def parse_public_key(key)
          key = pem_to_key(key)
          unless key.public?
            fail InvalidPublicKey, 'Invalid public key provided.'
          end
          key
        end

        # 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.
        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

        # 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.
        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

        # Converts an object to its JSON representation.
        #
        # @param o [Mixed] object to convert.
        # @return [String] JSON object as string.
        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

        # 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.
        def json_decode(o)
          YAJL_NAMESPACE::Parser.parse(o.to_s)
        rescue YAJL_NAMESPACE::ParseError => e
          raise DecryptionFailure, "#{e.class.name}: #{e}"
        end

        # Encodes Ruby `< 1.9.3` RSA key using X.509 format.
        #
        # In Ruby `< 1.9.3` RSA keys are in [PKCS#1]
        # (http://en.wikipedia.org/wiki/PKCS_1) format.
        #
        # In Ruby `>= 1.9.3` RSA keys are in [X.509]
        # (http://en.wikipedia.org/wiki/X.509) format (private keys in [PKCS#8]
        # (http://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
        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

        # 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
        def rsa_ensure_x509(rsa)
          RUBY_VERSION < '1.9.3' ? rsa_ensure_x509_ruby192(rsa) : rsa
        end

        # 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.
        def node_key(public_key)
          Digest::SHA1.hexdigest(rsa_ensure_x509(public_key).to_der)
        end

        # 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.
        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

        # 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.
        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

        # 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
        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

        # 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
        def rsa_decrypt_multi_key(enc_value, key)
          enc_value = enc_value[node_key(key.public_key)]
          rsa_decrypt_value(enc_value, key)
        end

        # 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
        def data_can_be_decrypted_by_key?(enc_value, key)
          enc_value.key?(node_key(key.public_key))
        end

        # 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.
        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
      end
    end
  end
end