cloudfoundry/cloud_controller_ng

View on GitHub
spec/unit/lib/cloud_controller/encryptor_spec.rb

Summary

Maintainability
C
1 day
Test Coverage
require 'spec_helper'

module VCAP::CloudController
  RSpec.describe Encryptor do
    let(:salt) { Encryptor.generate_salt }
    let(:encryption_iterations) { Encryptor::ENCRYPTION_ITERATIONS }

    describe 'generating some salt' do
      it 'returns a short, random string' do
        expect(salt.length).to be(16)
        expect(salt).not_to eql(Encryptor.generate_salt)
      end
    end

    describe 'encrypting a string' do
      let(:input) { 'i-am-the-input' }
      let!(:encrypted_string) { Encryptor.encrypt(input, salt) }

      it 'returns an encrypted string' do
        expect(encrypted_string).to match(/\w+/)
        expect(encrypted_string).not_to include(input)
      end

      it 'is deterministic' do
        expect(Encryptor.encrypt(input, salt)).to eql(encrypted_string)
      end

      it 'depends on the salt' do
        expect(Encryptor.encrypt(input, Encryptor.generate_salt)).not_to eql(encrypted_string)
      end

      it 'does not encrypt null values' do
        expect(Encryptor.encrypt(nil, salt)).to be_nil
      end

      context 'when database_encryption_keys has been set' do
        let(:salt) { 'FFFFFFFFFFFFFFFF' }
        let(:encrypted_death_string) { 'UsFVj9hjohvzOwlJQ4tqHA==' }
        let(:encrypted_legacy_string) { 'a6FHdu9k3+CCSjvzIX+i7w==' }

        before do
          Encryptor.db_encryption_key = 'legacy-crypto-key'
          Encryptor.database_encryption_keys = {
            foo: 'fooencryptionkey',
            death: 'headbangingdeathmetalkey'
          }
        end

        context 'when the label is found in the hash' do
          it 'encrypts using the value corresponding to the label' do
            allow(Encryptor).to receive(:current_encryption_key_label).and_return('death')
            expect(Encryptor.encrypt(input, salt)).to eql(encrypted_death_string)
          end
        end

        context 'when the label is not found in the hash' do
          it 'encrypts using current db_encryption_key when the label is not nil' do
            allow(Encryptor).to receive(:current_encryption_key_label).and_return('Inigo Montoya')
            expect(Encryptor.encrypt(input, salt)).to eql(encrypted_legacy_string)
          end

          it 'encrypts using current db_encryption_key when the label is nil' do
            allow(Encryptor).to receive(:current_encryption_key_label).and_return(nil)
            expect(Encryptor.encrypt(input, salt)).to eql(encrypted_legacy_string)
          end
        end
      end

      context 'when database_encryption_keys has not been set' do
        let(:salt) { 'FFFFFFFFFFFFFFFF' }
        let(:encrypted_legacy_string) { 'a6FHdu9k3+CCSjvzIX+i7w==' }

        before do
          Encryptor.db_encryption_key = 'legacy-crypto-key'
          Encryptor.database_encryption_keys = nil
          allow(Encryptor).to receive(:current_encryption_key_label).and_return('foo')
        end

        it 'encrypts using db_encryption_key' do
          expect(Encryptor.encrypt(input, salt)).to eql(encrypted_legacy_string)
        end
      end
    end

    describe '.pbkdf2_hmac_iterations=' do
      after do
        Encryptor.pbkdf2_hmac_iterations = Encryptor::ENCRYPTION_ITERATIONS
      end

      context 'when set to a value higher than Encryptor::ENCRYPTION_ITERATIONS' do
        it 'sets pbkdf2_hmac_iterations' do
          Encryptor.pbkdf2_hmac_iterations = 38_000
          expect(Encryptor.pbkdf2_hmac_iterations).to eq 38_000
        end
      end

      context 'when set to a value lower than Encryptor::ENCRYPTION_ITERATIONS' do
        it 'remains set to ENCRYPTION_ITERATIONS' do
          Encryptor.pbkdf2_hmac_iterations = 1
          expect(Encryptor.pbkdf2_hmac_iterations).to eq Encryptor::ENCRYPTION_ITERATIONS
        end
      end
    end

    describe '.decrypt' do
      let(:unencrypted_string) { 'some-string' }

      before do
        Encryptor.db_encryption_key = 'legacy-crypto-key'
        Encryptor.database_encryption_keys = {}
      end

      it 'returns the original string' do
        encrypted_string = Encryptor.encrypt(unencrypted_string, salt)
        expect(Encryptor.decrypt(encrypted_string, salt, iterations: encryption_iterations)).to eq(unencrypted_string)
      end

      it 'returns nil if the encrypted string is nil' do
        expect(Encryptor.decrypt(nil, salt, iterations: encryption_iterations)).to be_nil
      end

      context 'when database_encryption_keys is configured' do
        before do
          Encryptor.database_encryption_keys = {
            foo: 'fooencryptionkey',
            death: 'headbangingdeathmetalkey'
          }
        end

        context 'when no encryption key label is passed' do
          before do
            allow(Encryptor).to receive(:current_encryption_key_label).and_return(nil)
          end

          it 'decrypts using #db_encryption_key' do
            encrypted_string = Encryptor.encrypt(unencrypted_string, salt)
            expect(Encryptor).to receive(:db_encryption_key).and_call_original.at_least(:once)
            expect(Encryptor.decrypt(encrypted_string, salt, iterations: encryption_iterations)).to eq(unencrypted_string)
          end
        end

        context 'when encryption was done using a labelled key' do
          before do
            allow(Encryptor).to receive(:current_encryption_key_label).and_return('foo')
          end

          it 'decrypts using the key specified by the passed label' do
            encrypted_string = Encryptor.encrypt(unencrypted_string, salt)
            expect(Encryptor.decrypt(encrypted_string, salt, label: 'foo', iterations: encryption_iterations)).to eq(unencrypted_string)
          end

          context 'when no key label is passed for decryption' do
            it 'fails to decrypt the encrypted string successfully' do
              encrypted_string = Encryptor.encrypt(unencrypted_string, salt)

              result = begin
                Encryptor.decrypt(encrypted_string, salt, iterations: encryption_iterations)
              rescue OpenSSL::Cipher::CipherError => e
                e.message
              end

              expect(result).not_to eq(unencrypted_string)
            end
          end

          context 'when the wrong label is passed for decryption' do
            it 'fails to decrypt the encrypted string successfully' do
              encrypted_string = Encryptor.encrypt(unencrypted_string, salt)

              result = begin
                Encryptor.decrypt(encrypted_string, salt, label: 'death', iterations: encryption_iterations)
              rescue OpenSSL::Cipher::CipherError => e
                e.message
              end

              expect(result).not_to eq(unencrypted_string)
            end
          end
        end
      end

      context 'when the salt is only 8 bytes (legacy mode)' do
        let(:salt) { SecureRandom.hex(4).to_s }

        it 'decrypts correctly' do
          encrypted_string = Encryptor.encrypt(unencrypted_string, salt)
          expect(Encryptor.decrypt(encrypted_string, salt, iterations: encryption_iterations)).to eq(unencrypted_string)
        end
      end
    end
  end

  RSpec.describe Encryptor::FieldEncryptor do
    let(:base_class) do
      Class.new do
        include VCAP::CloudController::Encryptor::FieldEncryptor
        def self.columns
          raise '<dynamic class>.columns: not implemented'
        end

        def self.name
          'BaseClass'
        end

        def self.table_name
          :table_name
        end

        def db; end
      end
    end

    let(:db) { double(Sequel::Database) }

    before do
      allow(base_class).to receive(:columns) { columns }
      allow(db).to receive(:transaction).and_yield
      base_class.send :attr_accessor, *columns # emulate Sequel super methods
    end

    describe '#set_field_as_encrypted' do
      context 'model does not have the salt column' do
        let(:columns) { %i[id name size encryption_key_label] }

        context 'default name' do
          it 'raises an error' do
            expect do
              base_class.send :set_field_as_encrypted, :name
            end.to raise_error(RuntimeError, /salt/)
          end
        end

        context 'explicit name' do
          it 'raises an error' do
            expect do
              base_class.send :set_field_as_encrypted, :name, salt: :foobar
            end.to raise_error(RuntimeError, /foobar/)
          end
        end
      end

      context 'model has the salt column' do
        let(:columns) { %i[id name size salt encryption_key_label encryption_iterations] }

        it 'does not raise an error' do
          expect do
            base_class.send :set_field_as_encrypted, :name
          end.not_to raise_error
        end

        it 'creates a salt generation method' do
          base_class.send :set_field_as_encrypted, :name
          expect(base_class.instance_methods).to include(:generate_salt)
        end

        it 'stores its classname' do
          base_class.send :set_field_as_encrypted, :name

          expect(Encryptor.encrypted_classes).to include(base_class.name)
        end

        context 'explicit name' do
          let(:columns) { %i[id name size foobar encryption_key_label encryption_iterations] }

          it 'does not raise an error' do
            expect do
              base_class.send :set_field_as_encrypted, :name, salt: :foobar
            end.not_to raise_error
          end

          it 'creates a salt generation method' do
            base_class.send :set_field_as_encrypted, :name, salt: :foobar
            expect(base_class.instance_methods).to include(:generate_foobar)
          end
        end
      end

      context 'model does not have the "encryption_key_label" column' do
        let(:columns) { %i[id name salt size] }

        it 'raises an error' do
          expect do
            base_class.send :set_field_as_encrypted, :name
          end.to raise_error(RuntimeError, /encryption_key_label/)
        end
      end

      context 'model does not have the "encryption_iterations" column' do
        let(:columns) { %i[id name salt encryption_key_label] }

        it 'raises and error' do
          expect { base_class.send :set_field_as_encrypted, :name }.to raise_error(RuntimeError, /encryption_iterations/)
        end
      end
    end

    describe 'field-specific methods' do
      let(:columns) { %i[sekret salt encryption_key_label encryption_iterations] }
      let(:model_class) do
        Class.new(base_class) do
          set_field_as_encrypted :sekret

          def self.name
            'ModelClass'
          end
        end
      end
      let(:subject) { model_class.new }
      let(:default_key) { 'somerandomkey' }
      let(:encryption_iterations) { Encryptor::ENCRYPTION_ITERATIONS }

      before do
        subject.encryption_iterations = encryption_iterations
        allow(subject).to receive(:db) { db }

        Encryptor.db_encryption_key = default_key
      end

      describe 'salt generation method' do
        context 'salt is not set' do
          it 'updates the salt using Encryptor' do
            [nil, ''].each do |unset_value|
              expect(Encryptor).to receive(:generate_salt).and_return('new salt')
              subject.salt = unset_value
              subject.generate_salt
              expect(subject.salt).to eq 'new salt'
            end
          end
        end

        context 'salt is set' do
          it 'does not update the salt' do
            subject.salt = 'old salt'
            subject.generate_salt
            expect(subject.salt).to eq 'old salt'
          end
        end
      end

      describe 'encryption iteration' do
        it 'updates to the newest iteration value' do
          allow(Encryptor).to receive(:pbkdf2_hmac_iterations).and_return(2048)
          subject.encryption_iterations = 2048
          subject.salt = 'some salt'
          expect(Encryptor).to receive(:encrypt).with('hello', 'some salt')

          allow(Encryptor).to receive(:pbkdf2_hmac_iterations).and_return(100_001)
          subject.sekret = 'hello'
          expect(subject.encryption_iterations).to eq 100_001
        end
      end

      describe 'decryption' do
        it 'decrypts by passing the salt and the underlying value to Encryptor' do
          subject.salt = 'asdf'
          subject.sekret_without_encryption = 'underlying'
          expect(Encryptor).to receive(:decrypt).with('underlying', 'asdf', iterations: encryption_iterations, label: nil).and_return('unencrypted')
          expect(subject.sekret).to eq 'unencrypted'
        end
      end

      describe 'encryption' do
        it 'calls the salt generation method' do
          expect(subject).to receive(:generate_salt).and_call_original
          subject.sekret = 'foobar'
        end

        context 'blank value' do
          before do
            subject.sekret = 'notanilvalue'
          end

          context 'when the value is nil' do
            it 'stores a default nil value without trying to encrypt' do
              expect(subject.sekret_without_encryption).not_to be_nil
              expect do
                subject.sekret = nil
              end.to change(subject, :sekret_without_encryption).to(nil)
            end
          end

          context 'when the value is blank' do
            it 'stores a default nil value without trying to encrypt' do
              expect(subject.sekret_without_encryption).not_to be_nil
              expect do
                subject.sekret = ''
              end.to change(subject, :sekret_without_encryption).to(nil)
            end
          end
        end

        context 'non-blank value' do
          let(:salt) { Encryptor.generate_salt }
          let(:unencrypted_string) { 'unencrypted' }

          before do
            Encryptor.database_encryption_keys = {
              foo: 'fooencryptionkey',
              bar: 'headbangingdeathmetalkey'
            }
          end

          it 'encrypts by passing the value and salt to Encryptor' do
            subject.salt = salt
            expect(Encryptor).to receive(:encrypt).with('unencrypted', salt).and_return('encrypted')
            subject.sekret = unencrypted_string
            expect(subject.sekret_without_encryption).to eq 'encrypted'
          end

          it 'encrypts using the default database_encryption_keys' do
            subject.salt = salt
            subject.sekret = unencrypted_string
            expect(Encryptor.decrypt(subject.sekret_without_encryption, subject.salt, iterations: encryption_iterations)).to eq(unencrypted_string)
          end

          context 'model has a value for encryption_key_label' do
            let(:columns) { %i[sekret salt encryption_key_label encryption_iterations] }

            before do
              allow(Encryptor).to receive(:current_encryption_key_label).and_return('foo')
              subject.salt = salt
              subject.encryption_key_label = 'foo'
              subject.sekret = unencrypted_string
              expect(subject.sekret).to eq(unencrypted_string)
            end

            it 'encrypts using the key corresponding to the label' do
              subject.salt = salt
              expect(Encryptor).to receive(:encrypt).with(unencrypted_string, salt).and_call_original
              subject.sekret = unencrypted_string
              expect(Encryptor.decrypt(subject.sekret_without_encryption, salt, label: 'foo', iterations: encryption_iterations)).to eq(unencrypted_string)
            end
          end
        end
      end

      describe 'key rotation' do
        let(:salt) { Encryptor.generate_salt }
        let(:unencrypted_string) { 'unencrypted' }

        before do
          Encryptor.database_encryption_keys = {
            foo: 'fooencryptionkey',
            bar: 'headbangingdeathmetalkey'
          }
          allow(Encryptor).to receive(:current_encryption_key_label).and_return('foo')
          subject.sekret = unencrypted_string
          expect(subject.sekret).to eq(unencrypted_string)
          allow(Encryptor).to receive(:current_encryption_key_label).and_return('bar')
        end

        it 'updates encryption_key_label in the record when encrypting' do
          expect(subject.encryption_key_label).to eq('foo')
          subject.sekret = 'nu'
          expect(subject.sekret).to eq('nu')
          expect(subject.encryption_key_label).to eq('bar')
        end

        context 'and the field is serialized (and/or has other alias method chains)' do
          let(:serialized_model) do
            Class.new(base_class) do
              set_field_as_encrypted :sekret

              def self.name
                'SerializedModelClass'
              end

              def sekret_with_serialization
                Oj.load(sekret_without_serialization)
              end

              def sekret_with_serialization=(sekret)
                self.sekret_without_serialization = Oj.dump(sekret)
              end

              alias_method 'sekret_without_serialization', 'sekret'
              alias_method 'sekret', 'sekret_with_serialization'
              alias_method 'sekret_without_serialization=', 'sekret='
              alias_method 'sekret=', 'sekret_with_serialization='
            end
          end

          let(:subject) { serialized_model.new }
          let(:unencrypted_string) { { 'foo' => 'bar' } }

          it 're-encrypts the field after it has been serialized (the encryptor only works for strings, not hashes)' do
            subject.sekret = { 'foo' => 'bar' }
            expect(subject.sekret_without_serialization).to eq(%({"foo":"bar"}))
            expect(subject.sekret).to eq({ 'foo' => 'bar' })
            expect(subject.encryption_key_label).to eq(Encryptor.current_encryption_key_label)
          end
        end

        context 'and the model has another encrypted field' do
          let(:columns) { %i[sekret salt sekret2 sekret2_salt encryption_key_label encryption_iterations] }
          let(:unencrypted_string2) { 'announce presence with authority' }
          let(:multi_field_class) do
            Class.new(base_class) do
              set_field_as_encrypted :sekret
              set_field_as_encrypted :sekret2, { salt: 'sekret2_salt' }

              def db; end

              def self.name
                'MultiFieldClass'
              end
            end
          end

          let(:subject) { multi_field_class.new }

          before do
            allow(Encryptor).to receive(:current_encryption_key_label).and_return('foo')
            subject.sekret = unencrypted_string
            subject.sekret2 = unencrypted_string2
          end

          it 're-encrypts that field with the new key' do
            allow(Encryptor).to receive(:current_encryption_key_label).and_return('bar')
            subject.sekret = 'nu'

            expect(subject.encryption_key_label).to eq('bar')
            expect(Encryptor.decrypt(subject.sekret_without_encryption, subject.salt, label: 'bar', iterations: encryption_iterations)).to eq('nu')
            expect(Encryptor.decrypt(subject.sekret2_without_encryption, subject.sekret2_salt, label: 'bar', iterations: encryption_iterations)).to eq(unencrypted_string2)
          end
        end

        context 'and the model is a subclass of the class with encrypted fields' do
          let(:columns) { %i[sekret salt sekret2 sekret2_salt encryption_key_label encryption_iterations] }
          let(:unencrypted_string2) { 'announce presence with authority' }

          let(:sti_class_parent) do
            Class.new(base_class) do
              set_field_as_encrypted :sekret
              set_field_as_encrypted :sekret2, { salt: 'sekret2_salt' }

              def db; end

              def self.name
                'StiClassParent'
              end
            end
          end

          let(:sti_class_child) do
            Class.new(sti_class_parent) do
              def self.name
                'StiClassChild'
              end
            end
          end

          let(:subject) { sti_class_child.new }

          before do
            allow(Encryptor).to receive(:encrypt).and_call_original
            allow(Encryptor).to receive(:current_encryption_key_label).and_return('foo')
            subject.sekret = unencrypted_string
            subject.sekret2 = unencrypted_string2
            allow(Encryptor).to receive(:current_encryption_key_label).and_return('bar')
          end

          it 're-encrypts all fields from the superclass' do
            expect(subject.encryption_key_label).to eq('foo')
            subject.sekret = 'nu'

            expect(subject.sekret).to eq('nu')

            expect(subject.encryption_key_label).to eq('bar')
            expect(Encryptor.decrypt(subject.sekret_without_encryption, subject.salt, label: 'bar', iterations: encryption_iterations)).to eq('nu')
            expect(Encryptor.decrypt(subject.sekret2_without_encryption, subject.sekret2_salt, label: 'bar', iterations: encryption_iterations)).to eq(unencrypted_string2)
          end
        end
      end

      describe 'pbkdf2_hmac iterations' do
        let(:salt) { Encryptor.generate_salt }
        let(:unencrypted_string) { 'unencrypted' }

        before do
          Encryptor.database_encryption_keys = {
            foo: 'fooencryptionkey'
          }
          allow(Encryptor).to receive(:current_encryption_key_label).and_return('foo')
          allow(Encryptor).to receive(:pbkdf2_hmac_iterations).and_return(2048)
          subject.sekret = unencrypted_string
          expect(subject.sekret).to eq(unencrypted_string)
          allow(Encryptor).to receive(:pbkdf2_hmac_iterations).and_return(100_001)
        end

        it 'updates encryption_iterations in the record when encrypting' do
          expect(subject.encryption_iterations).to eq(2048)
          subject.sekret = 'nu'
          expect(subject.sekret).to eq('nu')
          expect(subject.encryption_iterations).to eq(100_001)
        end

        context 'and the model has another encrypted field' do
          let(:columns) { %i[sekret salt sekret2 sekret2_salt encryption_key_label encryption_iterations] }
          let(:unencrypted_string2) { 'announce presence with authority' }
          let(:multi_field_class) do
            Class.new(base_class) do
              set_field_as_encrypted :sekret
              set_field_as_encrypted :sekret2, { salt: 'sekret2_salt' }

              def db; end

              def self.name
                'MultiFieldClass'
              end
            end
          end

          let(:subject) { multi_field_class.new }

          before do
            allow(Encryptor).to receive(:pbkdf2_hmac_iterations).and_return(2048)
            subject.sekret = unencrypted_string
            subject.sekret2 = unencrypted_string2
          end

          it 're-encrypts all fields with the new iteration count' do
            allow(Encryptor).to receive(:pbkdf2_hmac_iterations).and_return(100_001)
            subject.sekret = 'nu'

            expect(subject.encryption_iterations).to eq(100_001)

            expect(Encryptor.decrypt(subject.sekret_without_encryption, subject.salt, label: 'foo', iterations: 100_001)).to eq('nu')
            expect(Encryptor.decrypt(subject.sekret2_without_encryption, subject.sekret2_salt, label: 'foo', iterations: 100_001)).to eq(unencrypted_string2)
          end
        end

        context 'and the model is a subclass of the class with encrypted fields' do
          let(:columns) { %i[sekret salt sekret2 sekret2_salt encryption_key_label encryption_iterations] }
          let(:unencrypted_string2) { 'announce presence with authority' }

          let(:sti_class_parent) do
            Class.new(base_class) do
              set_field_as_encrypted :sekret
              set_field_as_encrypted :sekret2, { salt: 'sekret2_salt' }

              def db; end

              def self.name
                'StiClassParent'
              end
            end
          end

          let(:sti_class_child) do
            Class.new(sti_class_parent) do
              def self.name
                'StiClassChild'
              end
            end
          end

          let(:subject) { sti_class_child.new }

          before do
            allow(Encryptor).to receive(:encrypt).and_call_original
            allow(Encryptor).to receive(:pbkdf2_hmac_iterations).and_return(2048)
            subject.sekret = unencrypted_string
            subject.sekret2 = unencrypted_string2
            allow(Encryptor).to receive(:pbkdf2_hmac_iterations).and_return(100_001)
          end

          it 're-encrypts all fields from the superclass' do
            expect(subject.encryption_iterations).to eq(2048)
            subject.sekret = 'nu'

            expect(subject.sekret).to eq('nu')

            expect(subject.encryption_iterations).to eq(100_001)
            expect(Encryptor.decrypt(subject.sekret_without_encryption, subject.salt, label: 'foo', iterations: 100_001)).to eq('nu')
            expect(Encryptor.decrypt(subject.sekret2_without_encryption, subject.sekret2_salt, label: 'foo', iterations: 100_001)).to eq(unencrypted_string2)
          end
        end
      end

      describe 'alternative storage column is specified' do
        let(:columns) { %i[sekret salt encrypted_sekret encryption_key_label encryption_iterations] }

        let(:model_class) do
          Class.new(base_class) do
            set_field_as_encrypted :sekret, { column: :encrypted_sekret }
          end
        end

        it 'stores the encrypted value in that column' do
          expect(subject.encrypted_sekret).to be_nil
          subject.sekret = 'asdf'
          expect(subject.encrypted_sekret).not_to be_nil
          expect(subject.sekret).to eq 'asdf'
        end
      end
    end
  end
end