diff options
author | John Keiser <jkeiser@opscode.com> | 2013-12-03 12:51:07 -0800 |
---|---|---|
committer | John Keiser <jkeiser@opscode.com> | 2013-12-03 12:51:07 -0800 |
commit | 9bbc9b101d00c309b205f2fdfccc4c7a093d9d31 (patch) | |
tree | b7ac0c7cf4f9191cb8690a94b5826c44d6ce2e89 /lib/chef/encrypted_data_bag_item.rb | |
parent | ca77ec8471560de11ca1ca3244c8fdaa178741cc (diff) | |
download | chef-9bbc9b101d00c309b205f2fdfccc4c7a093d9d31.tar.gz |
Split EncryptedDataBagItem into multiple files
Diffstat (limited to 'lib/chef/encrypted_data_bag_item.rb')
-rw-r--r-- | lib/chef/encrypted_data_bag_item.rb | 306 |
1 files changed, 3 insertions, 303 deletions
diff --git a/lib/chef/encrypted_data_bag_item.rb b/lib/chef/encrypted_data_bag_item.rb index 452a8224df..1f8d6cdf33 100644 --- a/lib/chef/encrypted_data_bag_item.rb +++ b/lib/chef/encrypted_data_bag_item.rb @@ -16,11 +16,10 @@ # limitations under the License. # -require 'base64' -require 'openssl' +require 'chef/config' require 'chef/data_bag_item' -require 'yaml' -require 'yajl' +require 'chef/encrypted_data_bag_item/decryptor' +require 'chef/encrypted_data_bag_item/encryptor' require 'open-uri' # An EncryptedDataBagItem represents a read-only data bag item where @@ -50,305 +49,6 @@ require 'open-uri' class Chef::EncryptedDataBagItem ALGORITHM = 'aes-256-cbc' - class UnacceptableEncryptedDataBagItemFormat < StandardError - end - - class UnsupportedEncryptedDataBagItemFormat < StandardError - end - - class DecryptionFailure < StandardError - end - - class UnsupportedCipher < StandardError - end - - # Implementation class for converting plaintext data bag item values to an - # encrypted value, including any necessary wrappers and metadata. - module Encryptor - - # "factory" method that creates an encryptor object with the proper class - # for the desired encrypted data bag format version. - # - # +Chef::Config[:data_bag_encrypt_version]+ determines which version is used. - def self.new(value, secret, iv=nil) - format_version = Chef::Config[:data_bag_encrypt_version] - case format_version - when 1 - Version1Encryptor.new(value, secret, iv) - when 2 - Version2Encryptor.new(value, secret, iv) - else - raise UnsupportedEncryptedDataBagItemFormat, - "Invalid encrypted data bag format version `#{format_version}'. Supported versions are '1', '2'" - end - end - - class Version1Encryptor - attr_reader :key - attr_reader :plaintext_data - - # Create a new Encryptor for +data+, which will be encrypted with the given - # +key+. - # - # === Arguments: - # * data: An object of any type that can be serialized to json - # * key: A String representing the desired passphrase - # * iv: The optional +iv+ parameter is intended for testing use only. When - # *not* supplied, Encryptor will use OpenSSL to generate a secure random - # IV, which is what you want. - def initialize(plaintext_data, key, iv=nil) - @plaintext_data = plaintext_data - @key = key - @iv = iv && Base64.decode64(iv) - end - - # Returns a wrapped and encrypted version of +plaintext_data+ suitable for - # using as the value in an encrypted data bag item. - def for_encrypted_item - { - "encrypted_data" => encrypted_data, - "iv" => Base64.encode64(iv), - "version" => 1, - "cipher" => ALGORITHM - } - end - - # Generates or returns the IV. - def iv - # Generated IV comes from OpenSSL::Cipher::Cipher#random_iv - # This gets generated when +openssl_encryptor+ gets created. - openssl_encryptor if @iv.nil? - @iv - end - - # Generates (and memoizes) an OpenSSL::Cipher::Cipher object and configures - # it for the specified iv and encryption key. - def openssl_encryptor - @openssl_encryptor ||= begin - encryptor = OpenSSL::Cipher::Cipher.new(ALGORITHM) - encryptor.encrypt - @iv ||= encryptor.random_iv - encryptor.iv = @iv - encryptor.key = Digest::SHA256.digest(key) - encryptor - end - end - - # Encrypts and Base64 encodes +serialized_data+ - def encrypted_data - @encrypted_data ||= begin - enc_data = openssl_encryptor.update(serialized_data) - enc_data << openssl_encryptor.final - Base64.encode64(enc_data) - end - end - - # Wraps the data in a single key Hash (JSON Object) and converts to JSON. - # The wrapper is required because we accept values (such as Integers or - # Strings) that do not produce valid JSON when serialized without the - # wrapper. - def serialized_data - Yajl::Encoder.encode(:json_wrapper => plaintext_data) - end - end - - class Version2Encryptor < Version1Encryptor - - # Returns a wrapped and encrypted version of +plaintext_data+ suitable for - # using as the value in an encrypted data bag item. - def for_encrypted_item - { - "encrypted_data" => encrypted_data, - "hmac" => hmac, - "iv" => Base64.encode64(iv), - "version" => 2, - "cipher" => ALGORITHM - } - end - - # Generates an HMAC-SHA2-256 of the encrypted data (encrypt-then-mac) - def hmac - @hmac ||= begin - digest = OpenSSL::Digest::Digest.new("sha256") - raw_hmac = OpenSSL::HMAC.digest(digest, key, encrypted_data) - Base64.encode64(raw_hmac) - end - end - - end - end - - #=== Decryptor - # For backwards compatibility, Chef implements decryption/deserialization for - # older encrypted data bag item formats in addition to the current version. - # Each decryption/deserialization strategy is implemented as a class in this - # namespace. For convenience the factory method +Decryptor.for()+ can be used - # to create an instance of the appropriate strategy for the given encrypted - # data bag value. - module Decryptor - - # Detects the encrypted data bag item format version and instantiates a - # decryptor object for that version. Call #for_decrypted_item on the - # resulting object to decrypt and deserialize it. - def self.for(encrypted_value, key) - format_version = format_version_of(encrypted_value) - assert_format_version_acceptable!(format_version) - case format_version - when 2 - Version2Decryptor.new(encrypted_value, key) - when 1 - Version1Decryptor.new(encrypted_value, key) - when 0 - Version0Decryptor.new(encrypted_value, key) - else - raise UnsupportedEncryptedDataBagItemFormat, - "This version of chef does not support encrypted data bag item format version '#{format_version}'" - end - end - - def self.format_version_of(encrypted_value) - if encrypted_value.respond_to?(:key?) - encrypted_value["version"] - else - 0 - end - end - - def self.assert_format_version_acceptable!(format_version) - unless format_version.kind_of?(Integer) and format_version >= Chef::Config[:data_bag_decrypt_minimum_version] - raise UnacceptableEncryptedDataBagItemFormat, - "The encrypted data bag item has format version `#{format_version}', " + - "but the config setting 'data_bag_decrypt_minimum_version' requires version `#{Chef::Config[:data_bag_decrypt_minimum_version]}'" - end - end - - class Version1Decryptor - - attr_reader :encrypted_data - attr_reader :key - - def initialize(encrypted_data, key) - @encrypted_data = encrypted_data - @key = key - end - - def for_decrypted_item - Yajl::Parser.parse(decrypted_data)["json_wrapper"] - rescue Yajl::ParseError - # convert to a DecryptionFailure error because the most likely scenario - # here is that the decryption step was unsuccessful but returned bad - # data rather than raising an error. - raise DecryptionFailure, "Error decrypting data bag value. Most likely the provided key is incorrect" - end - - def encrypted_bytes - Base64.decode64(@encrypted_data["encrypted_data"]) - end - - def iv - Base64.decode64(@encrypted_data["iv"]) - end - - def decrypted_data - @decrypted_data ||= begin - plaintext = openssl_decryptor.update(encrypted_bytes) - plaintext << openssl_decryptor.final - rescue OpenSSL::Cipher::CipherError => e - raise DecryptionFailure, "Error decrypting data bag value: '#{e.message}'. Most likely the provided key is incorrect" - end - end - - def openssl_decryptor - @openssl_decryptor ||= begin - assert_valid_cipher! - d = OpenSSL::Cipher::Cipher.new(ALGORITHM) - d.decrypt - d.key = Digest::SHA256.digest(key) - d.iv = iv - d - end - end - - def assert_valid_cipher! - # In the future, chef may support configurable ciphers. For now, only - # aes-256-cbc is supported. - requested_cipher = @encrypted_data["cipher"] - unless requested_cipher == ALGORITHM - raise UnsupportedCipher, - "Cipher '#{requested_cipher}' is not supported by this version of Chef. Available ciphers: ['#{ALGORITHM}']" - end - end - - end - - class Version2Decryptor < Version1Decryptor - - def decrypted_data - validate_hmac! unless @decrypted_data - super - end - - def validate_hmac! - digest = OpenSSL::Digest::Digest.new("sha256") - raw_hmac = OpenSSL::HMAC.digest(digest, key, @encrypted_data["encrypted_data"]) - - if candidate_hmac_matches?(raw_hmac) - true - else - raise DecryptionFailure, "Error decrypting data bag value: invalid hmac. Most likely the provided key is incorrect" - end - end - - private - - def candidate_hmac_matches?(expected_hmac) - return false unless @encrypted_data["hmac"] - expected_bytes = expected_hmac.bytes.to_a - candidate_hmac_bytes = Base64.decode64(@encrypted_data["hmac"]).bytes.to_a - valid = expected_bytes.size ^ candidate_hmac_bytes.size - expected_bytes.zip(candidate_hmac_bytes) { |x, y| valid |= x ^ y.to_i } - valid == 0 - end - end - - class Version0Decryptor - - attr_reader :encrypted_data - attr_reader :key - - def initialize(encrypted_data, key) - @encrypted_data = encrypted_data - @key = key - end - - def for_decrypted_item - YAML.load(decrypted_data) - end - - def decrypted_data - @decrypted_data ||= begin - plaintext = openssl_decryptor.update(encrypted_bytes) - plaintext << openssl_decryptor.final - rescue OpenSSL::Cipher::CipherError => e - raise DecryptionFailure, "Error decrypting data bag value: '#{e.message}'. Most likely the provided key is incorrect" - end - end - - def encrypted_bytes - Base64.decode64(@encrypted_data) - end - - def openssl_decryptor - @openssl_decryptor ||= begin - d = OpenSSL::Cipher::Cipher.new(ALGORITHM) - d.decrypt - d.pkcs5_keyivgen(key) - d - end - end - end - end - def initialize(enc_hash, secret) @enc_hash = enc_hash @secret = secret |