summaryrefslogtreecommitdiff
path: root/lib/chef/encrypted_data_bag_item.rb
diff options
context:
space:
mode:
authorJohn Keiser <jkeiser@opscode.com>2013-12-03 12:51:07 -0800
committerJohn Keiser <jkeiser@opscode.com>2013-12-03 12:51:07 -0800
commit9bbc9b101d00c309b205f2fdfccc4c7a093d9d31 (patch)
treeb7ac0c7cf4f9191cb8690a94b5826c44d6ce2e89 /lib/chef/encrypted_data_bag_item.rb
parentca77ec8471560de11ca1ca3244c8fdaa178741cc (diff)
downloadchef-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.rb306
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