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 | |
parent | ca77ec8471560de11ca1ca3244c8fdaa178741cc (diff) | |
download | chef-9bbc9b101d00c309b205f2fdfccc4c7a093d9d31.tar.gz |
Split EncryptedDataBagItem into multiple files
Diffstat (limited to 'lib/chef/encrypted_data_bag_item')
6 files changed, 431 insertions, 0 deletions
diff --git a/lib/chef/encrypted_data_bag_item/decryption_failure.rb b/lib/chef/encrypted_data_bag_item/decryption_failure.rb new file mode 100644 index 0000000000..47d263f197 --- /dev/null +++ b/lib/chef/encrypted_data_bag_item/decryption_failure.rb @@ -0,0 +1,22 @@ +# +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright 2010-2011 Opscode, Inc. +# 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. +# + +class Chef::EncryptedDataBagItem + class DecryptionFailure < StandardError + end +end diff --git a/lib/chef/encrypted_data_bag_item/decryptor.rb b/lib/chef/encrypted_data_bag_item/decryptor.rb new file mode 100644 index 0000000000..9ee38a12c4 --- /dev/null +++ b/lib/chef/encrypted_data_bag_item/decryptor.rb @@ -0,0 +1,201 @@ +# +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright 2010-2011 Opscode, Inc. +# 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 'yaml' +require 'yajl' +require 'openssl' +require 'base64' +require 'digest/sha2' +require 'chef/encrypted_data_bag_item' +require 'chef/encrypted_data_bag_item/unsupported_encrypted_data_bag_item_format' +require 'chef/encrypted_data_bag_item/unacceptable_encrypted_data_bag_item_format' +require 'chef/encrypted_data_bag_item/decryption_failure' +require 'chef/encrypted_data_bag_item/unsupported_cipher' + +class Chef::EncryptedDataBagItem + + #=== 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 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 + + 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 + end +end diff --git a/lib/chef/encrypted_data_bag_item/encryptor.rb b/lib/chef/encrypted_data_bag_item/encryptor.rb new file mode 100644 index 0000000000..f99c913c62 --- /dev/null +++ b/lib/chef/encrypted_data_bag_item/encryptor.rb @@ -0,0 +1,142 @@ +# +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright 2010-2011 Opscode, Inc. +# 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 'base64' +require 'digest/sha2' +require 'openssl' +require 'yajl' +require 'chef/encrypted_data_bag_item' +require 'chef/encrypted_data_bag_item/unsupported_encrypted_data_bag_item_format' + +class Chef::EncryptedDataBagItem + + # 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 +end diff --git a/lib/chef/encrypted_data_bag_item/unacceptable_encrypted_data_bag_item_format.rb b/lib/chef/encrypted_data_bag_item/unacceptable_encrypted_data_bag_item_format.rb new file mode 100644 index 0000000000..2f3b07c7f3 --- /dev/null +++ b/lib/chef/encrypted_data_bag_item/unacceptable_encrypted_data_bag_item_format.rb @@ -0,0 +1,22 @@ +# +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright 2010-2011 Opscode, Inc. +# 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. +# + +class Chef::EncryptedDataBagItem + class UnacceptableEncryptedDataBagItemFormat < StandardError + end +end diff --git a/lib/chef/encrypted_data_bag_item/unsupported_cipher.rb b/lib/chef/encrypted_data_bag_item/unsupported_cipher.rb new file mode 100644 index 0000000000..1df5cd5efd --- /dev/null +++ b/lib/chef/encrypted_data_bag_item/unsupported_cipher.rb @@ -0,0 +1,22 @@ +# +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright 2010-2011 Opscode, Inc. +# 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. +# + +class Chef::EncryptedDataBagItem + class UnsupportedCipher < StandardError + end +end diff --git a/lib/chef/encrypted_data_bag_item/unsupported_encrypted_data_bag_item_format.rb b/lib/chef/encrypted_data_bag_item/unsupported_encrypted_data_bag_item_format.rb new file mode 100644 index 0000000000..e7cf087307 --- /dev/null +++ b/lib/chef/encrypted_data_bag_item/unsupported_encrypted_data_bag_item_format.rb @@ -0,0 +1,22 @@ +# +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright 2010-2011 Opscode, Inc. +# 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. +# + +class Chef::EncryptedDataBagItem + class UnsupportedEncryptedDataBagItemFormat < StandardError + end +end |