diff --git a/app/services/encryption_service.rb b/app/services/encryption_service.rb index 8949d54a7..3d0dbb92e 100644 --- a/app/services/encryption_service.rb +++ b/app/services/encryption_service.rb @@ -7,6 +7,10 @@ class EncryptionService password = Rails.application.secrets.secret_key_base key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, len) @encryptor = ActiveSupport::MessageEncryptor.new(key) + + # Remove after all encrypted attributes have been rotated. + legacy_key = ActiveSupport::KeyGenerator.new(password, hash_digest_class: OpenSSL::Digest::SHA1).generate_key(salt, len) + @encryptor.rotate legacy_key end def encrypt(value) diff --git a/spec/services/encryption_service_spec.rb b/spec/services/encryption_service_spec.rb index 91540cc43..051c48cb1 100644 --- a/spec/services/encryption_service_spec.rb +++ b/spec/services/encryption_service_spec.rb @@ -41,4 +41,89 @@ describe EncryptionService do it { expect { subject }.to raise_exception StandardError } end end + + describe "key rotation" do + let(:password) { Rails.application.secrets.secret_key_base } + let(:salt) { Rails.application.secrets.encryption_service_salt } + let(:len) { ActiveSupport::MessageEncryptor.key_len } + let(:value) { "Sensitive information" } + + let(:legacy_key) do + ActiveSupport::KeyGenerator.new(password, hash_digest_class: OpenSSL::Digest::SHA1) + .generate_key(salt, len) + end + + let(:new_key) do + ActiveSupport::KeyGenerator.new(password) + .generate_key(salt, len) + end + + let(:legacy_encryptor) { ActiveSupport::MessageEncryptor.new(legacy_key) } + let(:new_encryptor) { ActiveSupport::MessageEncryptor.new(new_key) } + + describe "#decrypt" do + subject { EncryptionService.new.decrypt(encrypted_value) } + + context "with a value encrypted using the legacy SHA1-based key" do + let(:encrypted_value) { legacy_encryptor.encrypt_and_sign(value) } + + it "successfully decrypts the value" do + expect(subject).to eq(value) + end + end + + context "with a value encrypted using the new SHA256-based key" do + let(:encrypted_value) { new_encryptor.encrypt_and_sign(value) } + + it "successfully decrypts the value" do + expect(subject).to eq(value) + end + end + end + + describe "transition from legacy to new encryption" do + let(:legacy_service) do + legacy_encryption_service = EncryptionService.new + legacy_encryption_service.instance_variable_set(:@encryptor, legacy_encryptor) + legacy_encryption_service + end + + let(:new_service) { EncryptionService.new } + + it "can decrypt values encrypted with the legacy key" do + legacy_encrypted = legacy_service.encrypt(value) + expect(new_service.decrypt(legacy_encrypted)).to eq(value) + end + + it "uses the new key for new encryptions" do + new_encrypted = new_service.encrypt(value) + expect { legacy_encryptor.decrypt_and_verify(new_encrypted) } + .to raise_error(ActiveSupport::MessageEncryptor::InvalidMessage) + expect(new_encryptor.decrypt_and_verify(new_encrypted)).to eq(value) + end + end + + describe "backwards compatibility" do + let(:value) { "Important data" } + let(:old_service) do # Test with a service encrypting data without rotation mechanism + Class.new do + def initialize(key) + @encryptor = ActiveSupport::MessageEncryptor.new(key) + end + + def encrypt(value) + @encryptor.encrypt_and_sign(value) + end + end + end + + it "can decrypt values from a hypothetical old version without rotation" do + old_key = ActiveSupport::KeyGenerator.new(password, hash_digest_class: OpenSSL::Digest::SHA1) + .generate_key(salt, len) + old_encrypted = old_service.new(old_key).encrypt(value) + + expect(EncryptionService.new.decrypt(old_encrypted)).to eq(value) + end + end + end end