summaryrefslogtreecommitdiff
path: root/lib/chef/encrypted_data_bag_item
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
parentca77ec8471560de11ca1ca3244c8fdaa178741cc (diff)
downloadchef-9bbc9b101d00c309b205f2fdfccc4c7a093d9d31.tar.gz
Split EncryptedDataBagItem into multiple files
Diffstat (limited to 'lib/chef/encrypted_data_bag_item')
-rw-r--r--lib/chef/encrypted_data_bag_item/decryption_failure.rb22
-rw-r--r--lib/chef/encrypted_data_bag_item/decryptor.rb201
-rw-r--r--lib/chef/encrypted_data_bag_item/encryptor.rb142
-rw-r--r--lib/chef/encrypted_data_bag_item/unacceptable_encrypted_data_bag_item_format.rb22
-rw-r--r--lib/chef/encrypted_data_bag_item/unsupported_cipher.rb22
-rw-r--r--lib/chef/encrypted_data_bag_item/unsupported_encrypted_data_bag_item_format.rb22
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