diff --git a/app/services/encryption/encryptors/pii_encryptor.rb b/app/services/encryption/encryptors/pii_encryptor.rb index ca91c09c2db..34cd16bb96a 100644 --- a/app/services/encryption/encryptors/pii_encryptor.rb +++ b/app/services/encryption/encryptors/pii_encryptor.rb @@ -36,7 +36,12 @@ def self.extract_encrypted_data(parsed_json) def initialize(password) @password = password @aes_cipher = AesCipher.new - @kms_client = KmsClient.new + @single_region_kms_client = KmsClient.new( + kms_key_id: IdentityConfig.store.aws_kms_key_id, + ) + @multi_region_kms_client = KmsClient.new( + kms_key_id: IdentityConfig.store.aws_kms_multi_region_key_id, + ) end def encrypt(plaintext, user_uuid: nil) @@ -44,13 +49,26 @@ def encrypt(plaintext, user_uuid: nil) cost = IdentityConfig.store.scrypt_cost aes_encryption_key = scrypt_password_digest(salt: salt, cost: cost) aes_encrypted_ciphertext = aes_cipher.encrypt(plaintext, aes_encryption_key) - kms_encrypted_ciphertext = kms_client.encrypt( + single_region_kms_encrypted_ciphertext = single_region_kms_client.encrypt( aes_encrypted_ciphertext, kms_encryption_context(user_uuid: user_uuid) ) + single_region_ciphertext = Ciphertext.new( + single_region_kms_encrypted_ciphertext, salt, cost + ).to_s + + multi_region_ciphertext = nil + if IdentityConfig.store.aws_kms_multi_region_write_enabled + multi_region_kms_encrypted_ciphertext = multi_region_kms_client.encrypt( + aes_encrypted_ciphertext, kms_encryption_context(user_uuid: user_uuid) + ) + multi_region_ciphertext = Ciphertext.new( + multi_region_kms_encrypted_ciphertext, salt, cost + ).to_s + end RegionalCiphertextPair.new( - single_region_ciphertext: Ciphertext.new(kms_encrypted_ciphertext, salt, cost).to_s, - multi_region_ciphertext: nil, + single_region_ciphertext: single_region_ciphertext, + multi_region_ciphertext: multi_region_ciphertext, ) end @@ -58,7 +76,7 @@ def decrypt(ciphertext_pair, user_uuid: nil) ciphertext_string = ciphertext_pair.single_region_ciphertext ciphertext = Ciphertext.parse_from_string(ciphertext_string) - aes_encrypted_ciphertext = kms_client.decrypt( + aes_encrypted_ciphertext = single_region_kms_client.decrypt( ciphertext.encrypted_data, kms_encryption_context(user_uuid: user_uuid) ) aes_encryption_key = scrypt_password_digest(salt: ciphertext.salt, cost: ciphertext.cost) @@ -67,7 +85,7 @@ def decrypt(ciphertext_pair, user_uuid: nil) private - attr_reader :password, :aes_cipher, :kms_client + attr_reader :password, :aes_cipher, :single_region_kms_client, :multi_region_kms_client def kms_encryption_context(user_uuid:) { diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index 95ce3cbb70e..0d2f203b532 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -118,6 +118,41 @@ expect(profile.encrypted_pii).to_not match 'Jane' expect(profile.encrypted_pii).to_not match(ssn) expect(profile.encrypted_pii).to_not match(ssn.tr('-', '')) + + expect(profile.encrypted_pii_recovery).to be_present + expect(profile.encrypted_pii_multi_region).to be_nil + expect(profile.encrypted_pii_recovery_multi_region).to be_nil + end + + context 'with aws_kms_multi_region_write_enabled set to true' do + before do + allow(IdentityConfig.store).to receive(:aws_kms_multi_region_write_enabled).and_return(true) + end + + it 'encrypts pii and stores the multi region ciphertext' do + expect(profile.encrypted_pii).to be_nil + expect(profile.encrypted_pii_recovery).to be_nil + expect(profile.encrypted_pii_multi_region).to be_nil + expect(profile.encrypted_pii_recovery_multi_region).to be_nil + + profile.encrypt_pii(pii, user.password) + + expect(profile.encrypted_pii).to be_present + expect(profile.encrypted_pii).to_not match 'Jane' + expect(profile.encrypted_pii).to_not match(ssn) + + expect(profile.encrypted_pii_recovery).to be_present + expect(profile.encrypted_pii_recovery).to_not match 'Jane' + expect(profile.encrypted_pii_recovery).to_not match(ssn) + + expect(profile.encrypted_pii_multi_region).to be_present + expect(profile.encrypted_pii_multi_region).to_not match 'Jane' + expect(profile.encrypted_pii_multi_region).to_not match(ssn) + + expect(profile.encrypted_pii_recovery_multi_region).to be_present + expect(profile.encrypted_pii_recovery_multi_region).to_not match 'Jane' + expect(profile.encrypted_pii_recovery_multi_region).to_not match(ssn) + end end it 'generates new personal key' do diff --git a/spec/services/encryption/encryptors/pii_encryptor_spec.rb b/spec/services/encryption/encryptors/pii_encryptor_spec.rb index 162b6482c41..f8435cb7410 100644 --- a/spec/services/encryption/encryptors/pii_encryptor_spec.rb +++ b/spec/services/encryption/encryptors/pii_encryptor_spec.rb @@ -38,13 +38,12 @@ expect(scrypt_password).to receive(:digest).and_return(scrypt_digest) expect(SCrypt::Password).to receive(:new).and_return(scrypt_password) - cipher = instance_double(Encryption::AesCipher) - expect(Encryption::AesCipher).to receive(:new).and_return(cipher) + cipher = subject.send(:aes_cipher) expect(cipher).to receive(:encrypt). with(plaintext, decoded_scrypt_digest). and_return('aes_ciphertext') - expect(subject.send(:kms_client)).to receive(:encrypt). + expect(subject.send(:single_region_kms_client)).to receive(:encrypt). with('aes_ciphertext', { 'context' => 'pii-encryption', 'user_uuid' => 'uuid-123-abc' }). and_return('kms_ciphertext') @@ -61,6 +60,64 @@ ) expect(ciphertext_multi_region).to eq(nil) end + + context 'with aws_kms_multi_region_write_enabled set to true' do + it 'returns a single region and multi-region KMS ciphertext' do + allow(IdentityConfig.store).to receive(:aws_kms_multi_region_write_enabled).and_return(true) + + salt = '0' * 64 + allow(SecureRandom).to receive(:hex).and_call_original + allow(SecureRandom).to receive(:hex).once.with(32).and_return(salt) + + scrypt_digest = '31' * 32 # hex_encode('1111..') + decoded_scrypt_digest = '1' * 32 + + scrypt_password = instance_double(SCrypt::Password) + expect(scrypt_password).to receive(:digest).and_return(scrypt_digest) + expect(SCrypt::Password).to receive(:new).and_return(scrypt_password) + + cipher = subject.send(:aes_cipher) + expect(cipher).to receive(:encrypt). + with(plaintext, decoded_scrypt_digest). + and_return('aes_ciphertext') + + single_region_kms_client = subject.send(:single_region_kms_client) + multi_region_kms_client = subject.send(:multi_region_kms_client) + + expect(single_region_kms_client.kms_key_id).to eq( + IdentityConfig.store.aws_kms_key_id, + ) + expect(multi_region_kms_client.kms_key_id).to eq( + IdentityConfig.store.aws_kms_multi_region_key_id, + ) + + expect(single_region_kms_client).to receive(:encrypt). + with('aes_ciphertext', { 'context' => 'pii-encryption', 'user_uuid' => 'uuid-123-abc' }). + and_return('single_region_kms_ciphertext') + expect(multi_region_kms_client).to receive(:encrypt). + with('aes_ciphertext', { 'context' => 'pii-encryption', 'user_uuid' => 'uuid-123-abc' }). + and_return('multi_region_kms_ciphertext') + + ciphertext_single_region, ciphertext_multi_region = subject.encrypt( + plaintext, user_uuid: 'uuid-123-abc' + ) + + expect(ciphertext_single_region).to eq( + { + encrypted_data: Base64.strict_encode64('single_region_kms_ciphertext'), + salt: salt, + cost: '800$8$1$', + }.to_json, + ) + expect(ciphertext_multi_region).to eq( + { + encrypted_data: Base64.strict_encode64('multi_region_kms_ciphertext'), + salt: salt, + cost: '800$8$1$', + }.to_json, + ) + end + end end describe '#decrypt' do @@ -90,14 +147,12 @@ expect(scrypt_password).to receive(:digest).and_return(scrypt_digest) expect(SCrypt::Password).to receive(:new).and_return(scrypt_password) - kms_client = instance_double(Encryption::KmsClient) - expect(Encryption::KmsClient).to receive(:new).and_return(kms_client) + kms_client = subject.send(:single_region_kms_client) expect(kms_client).to receive(:decrypt). with('kms_ciphertext', { 'context' => 'pii-encryption', 'user_uuid' => 'uuid-123-abc' }). and_return('aes_ciphertext') - cipher = instance_double(Encryption::AesCipher) - expect(Encryption::AesCipher).to receive(:new).and_return(cipher) + cipher = subject.send(:aes_cipher) expect(cipher).to receive(:decrypt). with('aes_ciphertext', decoded_scrypt_digest). and_return(plaintext)