2024-04-29 00:17:15 +02:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2021-06-10 16:52:51 +02:00
|
|
|
describe EncryptionService do
|
|
|
|
describe "#encrypt" do
|
|
|
|
subject { EncryptionService.new.encrypt(value) }
|
|
|
|
|
|
|
|
context "with a nil value" do
|
|
|
|
let(:value) { nil }
|
|
|
|
|
|
|
|
it { expect(subject).to be_nil }
|
|
|
|
end
|
|
|
|
|
|
|
|
context "with a string value" do
|
|
|
|
let(:value) { "The quick brown fox jumps over the lazy dog" }
|
|
|
|
|
|
|
|
it { expect(subject).to be_instance_of(String) }
|
|
|
|
it { expect(subject).to be_present }
|
|
|
|
it { expect(subject).not_to eq(value) }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "#decrypt" do
|
|
|
|
subject { EncryptionService.new.decrypt(encrypted_value) }
|
|
|
|
|
|
|
|
context "with a nil value" do
|
|
|
|
let(:encrypted_value) { nil }
|
|
|
|
|
|
|
|
it { expect(subject).to be_nil }
|
|
|
|
end
|
|
|
|
|
|
|
|
context "with a string value" do
|
|
|
|
let (:value) { "The quick brown fox jumps over the lazy dog" }
|
|
|
|
let(:encrypted_value) { EncryptionService.new.encrypt(value) }
|
|
|
|
|
|
|
|
it { expect(subject).to eq(value) }
|
|
|
|
end
|
|
|
|
|
|
|
|
context "with an invalid value" do
|
|
|
|
let(:encrypted_value) { "Gur dhvpx oebja sbk whzcf bire gur ynml qbt" }
|
|
|
|
|
|
|
|
it { expect { subject }.to raise_exception StandardError }
|
|
|
|
end
|
|
|
|
end
|
2024-08-22 16:59:50 +02:00
|
|
|
|
|
|
|
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
|
2021-06-10 16:52:51 +02:00
|
|
|
end
|