summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordanielsdeleo <dan@opscode.com>2012-11-15 13:40:49 -0800
committerdanielsdeleo <dan@opscode.com>2012-11-15 13:40:49 -0800
commit1c5ffa6bc1a0eae130b5174a5d10c7e65667198f (patch)
tree46dee4d18feee10bc7b879da53b85a2d6e7d9eaf
parente20f4d327bfef1f14e019f9004975085802f7603 (diff)
parent30a0b8e78596dd75df0f20b5c9852439f4798c16 (diff)
downloadchef-1c5ffa6bc1a0eae130b5174a5d10c7e65667198f.tar.gz
Merge branch 'CHEF-3392-10-stable' into 10-stable
-rw-r--r--chef/lib/chef/encrypted_data_bag_item.rb127
-rw-r--r--chef/spec/unit/encrypted_data_bag_item_spec.rb172
-rw-r--r--chef/spec/unit/knife/data_bag_create_spec.rb5
-rw-r--r--chef/spec/unit/knife/data_bag_edit_spec.rb13
-rw-r--r--chef/spec/unit/knife/data_bag_from_file_spec.rb16
5 files changed, 320 insertions, 13 deletions
diff --git a/chef/lib/chef/encrypted_data_bag_item.rb b/chef/lib/chef/encrypted_data_bag_item.rb
index 048ab8d57e..c20d67c92d 100644
--- a/chef/lib/chef/encrypted_data_bag_item.rb
+++ b/chef/lib/chef/encrypted_data_bag_item.rb
@@ -50,6 +50,126 @@ class Chef::EncryptedDataBagItem
DEFAULT_SECRET_FILE = "/etc/chef/encrypted_data_bag_secret"
ALGORITHM = 'aes-256-cbc'
+ class UnsupportedEncryptedDataBagItemFormat < StandardError
+ end
+
+ class DecryptionFailure < StandardError
+ 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)
+ case format_version_of(encrypted_value)
+ 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
+
+ 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"]
+ 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
+ d = OpenSSL::Cipher::Cipher.new(ALGORITHM)
+ d.decrypt
+ d.key = Digest::SHA256.digest(key)
+ d.iv = iv
+ d
+ end
+ 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
@@ -60,7 +180,7 @@ class Chef::EncryptedDataBagItem
if key == "id" || value.nil?
value
else
- self.class.decrypt_value(value, @secret)
+ Decryptor.for(value, @secret).for_decrypted_item
end
end
@@ -88,7 +208,6 @@ class Chef::EncryptedDataBagItem
end
def self.load(data_bag, name, secret = nil)
- path = "data/#{data_bag}/#{name}"
raw_hash = Chef::DataBagItem.load(data_bag, name)
secret = secret || self.load_secret
self.new(raw_hash, secret)
@@ -98,10 +217,6 @@ class Chef::EncryptedDataBagItem
Base64.encode64(self.cipher(:encrypt, value.to_yaml, key))
end
- def self.decrypt_value(value, key)
- YAML.load(self.cipher(:decrypt, Base64.decode64(value), key))
- end
-
def self.load_secret(path=nil)
path = path || Chef::Config[:encrypted_data_bag_secret] || DEFAULT_SECRET_FILE
secret = case path
diff --git a/chef/spec/unit/encrypted_data_bag_item_spec.rb b/chef/spec/unit/encrypted_data_bag_item_spec.rb
index 0b052b56c6..ce12528804 100644
--- a/chef/spec/unit/encrypted_data_bag_item_spec.rb
+++ b/chef/spec/unit/encrypted_data_bag_item_spec.rb
@@ -19,6 +19,141 @@
require 'spec_helper'
require 'chef/encrypted_data_bag_item'
+module Version0Encryptor
+ def self.encrypt_value(plaintext_data, key)
+ data = plaintext_data.to_yaml
+
+ cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
+ cipher.encrypt
+ cipher.pkcs5_keyivgen(key)
+ encrypted_bytes = cipher.update(data)
+ encrypted_bytes << cipher.final
+ Base64.encode64(encrypted_bytes)
+ end
+end
+
+# Encryption/serialization code from Chef 11.
+class Version1Encryptor
+ ALGORITHM = "aes-256-cbc"
+
+ 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
+ }
+ 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
+ Chef::JSONCompat.to_json(:json_wrapper => plaintext_data)
+ end
+end
+
+
+describe Chef::EncryptedDataBagItem::Decryptor do
+ context "when decrypting a version 1 (JSON+aes-256-cbc+random iv) encrypted value" do
+ before do
+ @encryptor = Version1Encryptor.new({"foo" => "bar"}, "passwd")
+ @encrypted_value = @encryptor.for_encrypted_item
+
+ @decryptor = Chef::EncryptedDataBagItem::Decryptor.for(@encrypted_value, "passwd")
+ end
+
+ it "selects the correct strategy for version 1" do
+ @decryptor.should be_a_kind_of Chef::EncryptedDataBagItem::Decryptor::Version1Decryptor
+ end
+
+ it "decrypts the encrypted value" do
+ @decryptor.decrypted_data.should == {"json_wrapper" => {"foo" => "bar"}}.to_json
+ end
+
+ it "unwraps the encrypted data and returns it" do
+ @decryptor.for_decrypted_item.should == {"foo" => "bar"}
+ end
+
+ context "and the provided key is incorrect" do
+ before do
+ @decryptor = Chef::EncryptedDataBagItem::Decryptor.for(@encrypted_value, "wrong-passwd")
+ end
+
+ it "raises a sensible error" do
+ lambda { @decryptor.for_decrypted_item }.should raise_error(Chef::EncryptedDataBagItem::DecryptionFailure)
+ end
+ end
+
+ end
+
+ context "when decrypting a version 0 (YAML+aes-256-cbc+no iv) encrypted value" do
+ before do
+ @encrypted_value = Version0Encryptor.encrypt_value({"foo" => "bar"}, "passwd")
+
+ @decryptor = Chef::EncryptedDataBagItem::Decryptor.for(@encrypted_value, "passwd")
+ end
+
+ it "selects the correct strategy for version 0" do
+ @decryptor.should be_a_kind_of(Chef::EncryptedDataBagItem::Decryptor::Version0Decryptor)
+ end
+
+ it "decrypts the encrypted value" do
+ @decryptor.for_decrypted_item.should == {"foo" => "bar"}
+ end
+ end
+end
+
describe Chef::EncryptedDataBagItem do
before(:each) do
@secret = "abc123SECRET"
@@ -33,6 +168,10 @@ describe Chef::EncryptedDataBagItem do
describe "encrypting" do
+ it "uses version 0 encryption/serialization" do
+ @enc_data["greeting"].should == Version0Encryptor.encrypt_value(@plain_data["greeting"], @secret)
+ end
+
it "should not encrypt the 'id' key" do
@enc_data["id"].should == "item_name"
end
@@ -53,12 +192,7 @@ describe Chef::EncryptedDataBagItem do
end
end
- describe "decrypting" do
- before(:each) do
- @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data,
- @secret)
- @eh = Chef::EncryptedDataBagItem.new(@enc_data, @secret)
- end
+ shared_examples_for "a decrypted data bag item" do
it "doesn't try to decrypt 'id'" do
@eh["id"].should == @plain_data["id"]
@@ -81,6 +215,32 @@ describe Chef::EncryptedDataBagItem do
end
end
+ describe "decrypting" do
+ before(:each) do
+ @enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data,
+ @secret)
+ @eh = Chef::EncryptedDataBagItem.new(@enc_data, @secret)
+ end
+
+ it_behaves_like "a decrypted data bag item"
+ end
+
+ describe "when decrypting a version 1 (Chef 11.x) data bag item" do
+ before do
+ @enc_data = @plain_data.inject({}) do |encrypted, (key, value)|
+ if key == "id"
+ encrypted["id"] = value
+ else
+ encrypted[key] = Version1Encryptor.new(value, @secret).for_encrypted_item
+ end
+ encrypted
+ end
+ @eh = Chef::EncryptedDataBagItem.new(@enc_data, @secret)
+ end
+
+ it_behaves_like "a decrypted data bag item"
+ end
+
describe "loading" do
it "should defer to Chef::DataBagItem.load" do
Chef::DataBagItem.stub(:load).with(:the_bag, "my_codes").and_return(@enc_data)
diff --git a/chef/spec/unit/knife/data_bag_create_spec.rb b/chef/spec/unit/knife/data_bag_create_spec.rb
index 8639052612..2cfe1656eb 100644
--- a/chef/spec/unit/knife/data_bag_create_spec.rb
+++ b/chef/spec/unit/knife/data_bag_create_spec.rb
@@ -75,6 +75,11 @@ describe Chef::Knife::DataBagCreate do
@knife.should_receive(:create_object).and_yield(@plain_data)
data_bag_item = Chef::DataBagItem.from_hash(@enc_data)
data_bag_item.data_bag("sudoing_admins")
+
+ # Random IV is used each time the data bag item is encrypted, so values
+ # will not be equal if we re-encrypt.
+ Chef::EncryptedDataBagItem.should_receive(:encrypt_data_bag_item).and_return(@enc_data)
+
@rest.should_receive(:post_rest).with("data", {'name' => 'sudoing_admins'}).ordered
@rest.should_receive(:post_rest).with("data/sudoing_admins", data_bag_item).ordered
end
diff --git a/chef/spec/unit/knife/data_bag_edit_spec.rb b/chef/spec/unit/knife/data_bag_edit_spec.rb
index d0222364e3..65042f695a 100644
--- a/chef/spec/unit/knife/data_bag_edit_spec.rb
+++ b/chef/spec/unit/knife/data_bag_edit_spec.rb
@@ -59,6 +59,19 @@ describe Chef::Knife::DataBagEdit do
@enc_edited_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@edited_data,
@secret)
Chef::DataBagItem.stub!(:load).with('bag_name', 'item_name').and_return(@enc_data)
+
+ # Random IV is used each time the data bag item is encrypted, so values
+ # will not be equal if we encrypt same value twice.
+ Chef::EncryptedDataBagItem.should_receive(:encrypt_data_bag_item).and_return(@enc_edited_data)
+
+ @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test")
+ @secret_file.puts(@secret)
+ @secret_file.flush
+ end
+
+ after do
+ @secret_file.close
+ @secret_file.unlink
end
it "decrypts and encrypts via --secret" do
diff --git a/chef/spec/unit/knife/data_bag_from_file_spec.rb b/chef/spec/unit/knife/data_bag_from_file_spec.rb
index 74c8a9513a..f90086c063 100644
--- a/chef/spec/unit/knife/data_bag_from_file_spec.rb
+++ b/chef/spec/unit/knife/data_bag_from_file_spec.rb
@@ -142,6 +142,19 @@ describe Chef::Knife::DataBagFromFile do
@secret = "abc123SECRET"
@enc_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(@plain_data,
@secret)
+
+ # Random IV is used each time the data bag item is encrypted, so values
+ # will not be equal if we re-encrypt.
+ Chef::EncryptedDataBagItem.should_receive(:encrypt_data_bag_item).and_return(@enc_data)
+
+ @secret_file = Tempfile.new("encrypted_data_bag_secret_file_test")
+ @secret_file.puts(@secret)
+ @secret_file.flush
+ end
+
+ after do
+ @secret_file.close
+ @secret_file.unlink
end
it "encrypts values when given --secret" do
@@ -177,7 +190,8 @@ describe Chef::Knife::DataBagFromFile do
it "prints help if given no arguments" do
@knife.instance_variable_set(:@name_args, [])
lambda { @knife.run }.should raise_error(SystemExit)
- @stdout.string.should match(/^knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] \(options\)/)
+ expected = 'knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)'
+ @stdout.string.should include(expected)
end
end