From 67fe30df3a5700927788b363fbcbf9423ceeb3d2 Mon Sep 17 00:00:00 2001 From: Xabier de Zuazo Date: Sun, 6 Jul 2014 21:07:42 +0200 Subject: [CHEF-5356-gcm] If the requirements to use Encryted Data Bags 3 are not met, give a meaningful error message --- lib/chef/encrypted_data_bag_item/assertions.rb | 54 ++++++++ lib/chef/encrypted_data_bag_item/decryptor.rb | 28 ++-- .../encrypted_data_bag_item_assertions.rb | 37 ++++++ lib/chef/encrypted_data_bag_item/encryptor.rb | 5 + spec/unit/encrypted_data_bag_item_spec.rb | 141 +++++++++++++++------ 5 files changed, 206 insertions(+), 59 deletions(-) create mode 100644 lib/chef/encrypted_data_bag_item/assertions.rb create mode 100644 lib/chef/encrypted_data_bag_item/encrypted_data_bag_item_assertions.rb diff --git a/lib/chef/encrypted_data_bag_item/assertions.rb b/lib/chef/encrypted_data_bag_item/assertions.rb new file mode 100644 index 0000000000..6e90008523 --- /dev/null +++ b/lib/chef/encrypted_data_bag_item/assertions.rb @@ -0,0 +1,54 @@ +# +# Author:: Xabier de Zuazo () +# 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. +# + +class Chef::EncryptedDataBagItem + + class EncryptedDataBagRequirementsFailure < StandardError + end + + module Assertions + + def 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 + + def assert_valid_cipher!(requested_cipher, algorithm) + # In the future, chef may support configurable ciphers. For now, only + # aes-256-cbc and aes-256-gcm are supported. + unless requested_cipher == algorithm + raise UnsupportedCipher, + "Cipher '#{requested_cipher}' is not supported by this version of Chef. Available ciphers: ['#{ALGORITHM}', '#{AEAD_ALGORITHM}']" + end + end + + def assert_aead_requirements_met!(algorithm) + unless OpenSSL::Cipher::Cipher.method_defined?(:auth_data=) + raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires Ruby >= 1.9" + end + unless OpenSSL::Cipher::Cipher.ciphers.include?(algorithm) + raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires an OpenSSL version with \"#{algorithm}\" algorithm support" + end + end + + end + +end diff --git a/lib/chef/encrypted_data_bag_item/decryptor.rb b/lib/chef/encrypted_data_bag_item/decryptor.rb index 82d04c0d67..b269868c53 100644 --- a/lib/chef/encrypted_data_bag_item/decryptor.rb +++ b/lib/chef/encrypted_data_bag_item/decryptor.rb @@ -26,6 +26,7 @@ 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' +require 'chef/encrypted_data_bag_item/assertions' class Chef::EncryptedDataBagItem @@ -37,6 +38,7 @@ class Chef::EncryptedDataBagItem # to create an instance of the appropriate strategy for the given encrypted # data bag value. module Decryptor + extend Chef::EncryptedDataBagItem::Assertions # Detects the encrypted data bag item format version and instantiates a # decryptor object for that version. Call #for_decrypted_item on the @@ -67,15 +69,8 @@ class Chef::EncryptedDataBagItem 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 + include Chef::EncryptedDataBagItem::Assertions attr_reader :encrypted_data attr_reader :key @@ -155,7 +150,7 @@ class Chef::EncryptedDataBagItem def openssl_decryptor @openssl_decryptor ||= begin - assert_valid_cipher! + assert_valid_cipher!(@encrypted_data["cipher"], algorithm) d = OpenSSL::Cipher::Cipher.new(algorithm) d.decrypt # We must set key before iv: https://bugs.ruby-lang.org/issues/8221 @@ -165,15 +160,6 @@ class Chef::EncryptedDataBagItem end end - def assert_valid_cipher! - # In the future, chef may support configurable ciphers. For now, only - # aes-256-cbc and aes-256-gcm are 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}', '#{AEAD_ALGORITHM}']" - end - end end class Version2Decryptor < Version1Decryptor @@ -208,6 +194,12 @@ class Chef::EncryptedDataBagItem class Version3Decryptor < Version1Decryptor + def initialize(encrypted_data, key) + super + assert_aead_requirements_met!(algorithm) + end + + # Returns the used decryption algorithm def algorithm AEAD_ALGORITHM diff --git a/lib/chef/encrypted_data_bag_item/encrypted_data_bag_item_assertions.rb b/lib/chef/encrypted_data_bag_item/encrypted_data_bag_item_assertions.rb new file mode 100644 index 0000000000..4d116715f6 --- /dev/null +++ b/lib/chef/encrypted_data_bag_item/encrypted_data_bag_item_assertions.rb @@ -0,0 +1,37 @@ +# +# Author:: Xabier de Zuazo () +# 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. +# + +class Chef::EncryptedDataBagItem + + class EncryptedDataBagRequirementsFailure < StandardError + end + + module Assertions + + def assert_requirements_met! + unless OpenSSL::Cipher::Cipher.method_defined?(:auth_data=) + raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires Ruby >= 1.9" + end + unless OpenSSL::Cipher::Cipher.ciphers.include?(algorithm) + raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires an OpenSSL version with \"#{algorithm}\" algorithm support" + end + end + + end + +end diff --git a/lib/chef/encrypted_data_bag_item/encryptor.rb b/lib/chef/encrypted_data_bag_item/encryptor.rb index a3f3011c21..5b4eaa7c98 100644 --- a/lib/chef/encrypted_data_bag_item/encryptor.rb +++ b/lib/chef/encrypted_data_bag_item/encryptor.rb @@ -23,6 +23,7 @@ require 'ffi_yajl' require 'chef/encrypted_data_bag_item' require 'chef/encrypted_data_bag_item/unsupported_encrypted_data_bag_item_format' require 'chef/encrypted_data_bag_item/encryption_failure' +require 'chef/encrypted_data_bag_item/assertions' class Chef::EncryptedDataBagItem @@ -53,6 +54,8 @@ class Chef::EncryptedDataBagItem attr_reader :key attr_reader :plaintext_data + include Chef::EncryptedDataBagItem::Assertions + # Create a new Encryptor for +data+, which will be encrypted with the given # +key+. # @@ -149,9 +152,11 @@ class Chef::EncryptedDataBagItem end class Version3Encryptor < Version1Encryptor + include Chef::EncryptedDataBagItem::Assertions def initialize(plaintext_data, key, iv=nil) super + assert_aead_requirements_met!(algorithm) @auth_tag = nil end diff --git a/spec/unit/encrypted_data_bag_item_spec.rb b/spec/unit/encrypted_data_bag_item_spec.rb index 68379e2f17..53fe8cd778 100644 --- a/spec/unit/encrypted_data_bag_item_spec.rb +++ b/spec/unit/encrypted_data_bag_item_spec.rb @@ -92,34 +92,59 @@ describe Chef::EncryptedDataBagItem::Encryptor do end end - describe "when using version 3 format", - :if => (RUBY_VERSION >= "2" and OpenSSL::OPENSSL_VERSION_NUMBER >= 10001000) do - + describe "when using version 3 format" do before do Chef::Config[:data_bag_encrypt_version] = 3 end - it "creates a version 3 encryptor" do - encryptor.should be_a_instance_of(Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor) - end + context "on supported platforms", + :if => (RUBY_VERSION >= "2" and OpenSSL::OPENSSL_VERSION_NUMBER >= 10001000) do - it "generates different authentication tags" do - encryptor3 = Chef::EncryptedDataBagItem::Encryptor.new(plaintext_data, key) - encryptor.for_encrypted_item # required to generate the auth_tag - encryptor3.for_encrypted_item - encryptor.auth_tag.should_not eq(encryptor3.auth_tag) - end + it "creates a version 3 encryptor" do + encryptor.should be_a_instance_of(Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor) + end - it "includes the auth_tag in the envelope" do - final_data = encryptor.for_encrypted_item - final_data["auth_tag"].should eq(Base64::encode64(encryptor.auth_tag)) - end + it "generates different authentication tags" do + encryptor3 = Chef::EncryptedDataBagItem::Encryptor.new(plaintext_data, key) + encryptor.for_encrypted_item # required to generate the auth_tag + encryptor3.for_encrypted_item + encryptor.auth_tag.should_not eq(encryptor3.auth_tag) + end - it "throws an error if auth tag is read before encrypting the data" do - lambda { encryptor.auth_tag }.should raise_error(Chef::EncryptedDataBagItem::EncryptionFailure) - end + it "includes the auth_tag in the envelope" do + final_data = encryptor.for_encrypted_item + final_data["auth_tag"].should eq(Base64::encode64(encryptor.auth_tag)) + end - end + it "throws an error if auth tag is read before encrypting the data" do + lambda { encryptor.auth_tag }.should raise_error(Chef::EncryptedDataBagItem::EncryptionFailure) + end + + end # context on supported platforms + + context "on unsupported platforms" do + + context "on platforms with old Ruby", + :if => RUBY_VERSION < "2" do + + it "throws an error warning about the Ruby version" do + lambda { encryptor }.should raise_error(Chef::EncryptedDataBagItem::EncryptedDataBagRequirementsFailure, /requires Ruby/) + end + + end # context on platforms with old Ruby + + context "on platforms with old OpenSSL", + :if => OpenSSL::OPENSSL_VERSION_NUMBER < 10001000 do + + it "throws an error warning about the OpenSSL version" do + lambda { encryptor }.should raise_error(Chef::EncryptedDataBagItem::EncryptedDataBagRequirementsFailure, /requires an OpenSSL/) + end + + end # context on platforms with old OpenSSL + + end # context on unsupported platforms + + end # when using version 3 format end @@ -130,34 +155,68 @@ describe Chef::EncryptedDataBagItem::Decryptor do let(:encryption_key) { "passwd" } let(:decryption_key) { encryption_key } - context "when decrypting a version 3 (JSON+aes-256-gcm+random iv+auth tag) encrypted value", - :if => (RUBY_VERSION >= "2" and OpenSSL::OPENSSL_VERSION_NUMBER >= 10001000) do + context "when decrypting a version 3 (JSON+aes-256-gcm+random iv+auth tag) encrypted value" do - let(:encrypted_value) do - Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor.new(plaintext_data, encryption_key).for_encrypted_item - end + context "on supported platforms", + :if => (RUBY_VERSION >= "2" and OpenSSL::OPENSSL_VERSION_NUMBER >= 10001000) do - let(:bogus_auth_tag) { "bogus_auth_tag" } + let(:encrypted_value) do + Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor.new(plaintext_data, encryption_key).for_encrypted_item + end - it "decrypts the encrypted value" do - decryptor.decrypted_data.should eq({"json_wrapper" => plaintext_data}.to_json) - end + let(:bogus_auth_tag) { "bogus_auth_tag" } - it "unwraps the encrypted data and returns it" do - decryptor.for_decrypted_item.should eq plaintext_data - end + it "decrypts the encrypted value" do + decryptor.decrypted_data.should eq({"json_wrapper" => plaintext_data}.to_json) + end - it "rejects the data if the authentication tag is wrong" do - encrypted_value["auth_tag"] = bogus_auth_tag - lambda { decryptor.for_decrypted_item }.should raise_error(Chef::EncryptedDataBagItem::DecryptionFailure) - end + it "unwraps the encrypted data and returns it" do + decryptor.for_decrypted_item.should eq plaintext_data + end - it "rejects the data if the authentication tag is missing" do - encrypted_value.delete("auth_tag") - lambda { decryptor.for_decrypted_item }.should raise_error(Chef::EncryptedDataBagItem::DecryptionFailure) - end + it "rejects the data if the authentication tag is wrong" do + encrypted_value["auth_tag"] = bogus_auth_tag + lambda { decryptor.for_decrypted_item }.should raise_error(Chef::EncryptedDataBagItem::DecryptionFailure) + end - end + it "rejects the data if the authentication tag is missing" do + encrypted_value.delete("auth_tag") + lambda { decryptor.for_decrypted_item }.should raise_error(Chef::EncryptedDataBagItem::DecryptionFailure) + end + + end # context on supported platforms + + context "on unsupported platforms" do + let(:encrypted_value) do + { + "encrypted_data" => "", + "iv" => "", + "version" => 3, + "cipher" => "aes-256-cbc", + } + end + + context "on platforms with old Ruby", + :if => RUBY_VERSION < "2" do + + it "throws an error warning about the Ruby version" do + lambda { decryptor }.should raise_error(Chef::EncryptedDataBagItem::EncryptedDataBagRequirementsFailure, /requires Ruby/) + end + + end # context on platforms with old Ruby + + context "on platforms with old OpenSSL", + :if => OpenSSL::OPENSSL_VERSION_NUMBER < 10001000 do + + it "throws an error warning about the OpenSSL version" do + lambda { decryptor }.should raise_error(Chef::EncryptedDataBagItem::EncryptedDataBagRequirementsFailure, /requires an OpenSSL/) + end + + end # context on unsupported platforms + + end # context on platforms with old OpenSSL + + end # context when decrypting a version 3 context "when decrypting a version 2 (JSON+aes-256-cbc+hmac-sha256+random iv) encrypted value" do let(:encrypted_value) do -- cgit v1.2.1