test: cover OrmEncryptionRotationTest and teardown
This commit is contained in:
1 parent
ad90868078
commit
ff3be9780d
1 file changed
+159
@@ -0,0 +1,159 @@
|
||||
require 'minitest/autorun'
|
||||
require 'prism'
|
||||
require 'tmpdir'
|
||||
|
||||
# Key rotation for Prism::Orm::Encryption: retiring an application secret
|
||||
# (a suspected leak, a scheduled policy) should not make every already-
|
||||
# encrypted row unreadable the moment the new secret goes live. These tests
|
||||
# encrypt a row under an OLD secret, then simulate the moment of rotation
|
||||
# (primary_key flips to a new secret, the old one moves to previous_keys),
|
||||
# and check that reads/queries against the old row keep working while
|
||||
# writes immediately start using the new key.
|
||||
class OrmEncryptionRotationTest < Minitest::Test
|
||||
OLD_SECRET = 'rotation-old-secret-do-not-use-in-prod'.freeze
|
||||
NEW_SECRET = 'rotation-new-secret-do-not-use-in-prod'.freeze
|
||||
|
||||
def setup
|
||||
@dir = Dir.mktmpdir('prism-encryption-rotation-store')
|
||||
@store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore'))
|
||||
@store.create_table(:patients, %w[id name ssn notes])
|
||||
end
|
||||
|
||||
def teardown
|
||||
FileUtils.remove_entry(@dir)
|
||||
Prism::Orm::Encryption.primary_key = nil
|
||||
Prism::Orm::Encryption.previous_keys = []
|
||||
Object.send(:remove_const, :RotatingPatient) if Object.const_defined?(:RotatingPatient)
|
||||
end
|
||||
|
||||
def define_patient_class
|
||||
klass = Class.new(Prism::Orm::Model) do
|
||||
table_name :patients
|
||||
encrypts :ssn, deterministic: true
|
||||
encrypts :notes
|
||||
end
|
||||
klass.store = @store
|
||||
Object.const_set(:RotatingPatient, klass)
|
||||
klass
|
||||
end
|
||||
|
||||
def test_a_row_encrypted_under_the_old_key_is_unreadable_without_rotation_configured
|
||||
Prism::Orm::Encryption.primary_key = OLD_SECRET
|
||||
old_class = define_patient_class
|
||||
patient = old_class.create!(name: 'Alice', ssn: '123-45-6789', notes: 'pre-rotation note')
|
||||
|
||||
Object.send(:remove_const, :RotatingPatient)
|
||||
Prism::Orm::Encryption.primary_key = NEW_SECRET
|
||||
new_class = define_patient_class
|
||||
|
||||
error = assert_raises(Prism::Orm::Encryption::Error) { new_class.find(patient.id).notes }
|
||||
assert_match(/wrong key/, error.message)
|
||||
end
|
||||
|
||||
def test_a_row_encrypted_under_the_old_key_stays_readable_once_rotation_is_configured
|
||||
Prism::Orm::Encryption.primary_key = OLD_SECRET
|
||||
old_class = define_patient_class
|
||||
patient = old_class.create!(name: 'Alice', ssn: '123-45-6789', notes: 'pre-rotation note')
|
||||
|
||||
Object.send(:remove_const, :RotatingPatient)
|
||||
Prism::Orm::Encryption.primary_key = NEW_SECRET
|
||||
Prism::Orm::Encryption.previous_keys = [OLD_SECRET]
|
||||
new_class = define_patient_class
|
||||
|
||||
reloaded = new_class.find(patient.id)
|
||||
assert_equal '123-45-6789', reloaded.ssn
|
||||
assert_equal 'pre-rotation note', reloaded.notes
|
||||
end
|
||||
|
||||
def test_a_row_re_saved_after_rotation_is_re_encrypted_under_the_new_key
|
||||
Prism::Orm::Encryption.primary_key = OLD_SECRET
|
||||
old_class = define_patient_class
|
||||
patient = old_class.create!(name: 'Alice', ssn: '123-45-6789', notes: 'pre-rotation note')
|
||||
|
||||
Object.send(:remove_const, :RotatingPatient)
|
||||
Prism::Orm::Encryption.primary_key = NEW_SECRET
|
||||
Prism::Orm::Encryption.previous_keys = [OLD_SECRET]
|
||||
new_class = define_patient_class
|
||||
|
||||
record = new_class.find(patient.id)
|
||||
record.notes = 'still the same note, just re-saved'
|
||||
record.save!
|
||||
|
||||
# Once re-saved, the OLD key alone (rotation not configured on THIS
|
||||
# class) must no longer be able to decrypt it - proving the write path
|
||||
# switched to the new key rather than continuing to use whichever key
|
||||
# happened to work on read.
|
||||
Object.send(:remove_const, :RotatingPatient)
|
||||
Prism::Orm::Encryption.primary_key = OLD_SECRET
|
||||
Prism::Orm::Encryption.previous_keys = []
|
||||
old_key_only_class = define_patient_class
|
||||
error = assert_raises(Prism::Orm::Encryption::Error) { old_key_only_class.find(patient.id).notes }
|
||||
assert_match(/wrong key/, error.message)
|
||||
end
|
||||
|
||||
def test_where_on_a_deterministic_column_finds_rows_encrypted_under_either_key
|
||||
Prism::Orm::Encryption.primary_key = OLD_SECRET
|
||||
old_class = define_patient_class
|
||||
stale = old_class.create!(name: 'Stale', ssn: '555-55-5555', notes: 'n')
|
||||
|
||||
Object.send(:remove_const, :RotatingPatient)
|
||||
Prism::Orm::Encryption.primary_key = NEW_SECRET
|
||||
Prism::Orm::Encryption.previous_keys = [OLD_SECRET]
|
||||
new_class = define_patient_class
|
||||
fresh = new_class.create!(name: 'Fresh', ssn: '555-55-5555', notes: 'n')
|
||||
|
||||
found = new_class.where(ssn: '555-55-5555').to_a.map(&:id).sort
|
||||
assert_equal [stale.id, fresh.id].sort, found
|
||||
end
|
||||
|
||||
def test_find_by_on_a_deterministic_column_matches_a_row_still_encrypted_under_the_old_key
|
||||
Prism::Orm::Encryption.primary_key = OLD_SECRET
|
||||
old_class = define_patient_class
|
||||
stale = old_class.create!(name: 'Stale', ssn: '999-99-9999', notes: 'n')
|
||||
|
||||
Object.send(:remove_const, :RotatingPatient)
|
||||
Prism::Orm::Encryption.primary_key = NEW_SECRET
|
||||
Prism::Orm::Encryption.previous_keys = [OLD_SECRET]
|
||||
new_class = define_patient_class
|
||||
|
||||
found = new_class.find_by(ssn: '999-99-9999')
|
||||
assert_equal stale.id, found.id
|
||||
end
|
||||
|
||||
def test_per_attribute_previous_keys_overrides_the_global_rotation_list
|
||||
Prism::Orm::Encryption.primary_key = NEW_SECRET
|
||||
Prism::Orm::Encryption.previous_keys = ['a-secret-that-was-never-used']
|
||||
|
||||
scoped_old_class = Class.new(Prism::Orm::Model) do
|
||||
table_name :patients
|
||||
encrypts :notes, key: OLD_SECRET
|
||||
end
|
||||
scoped_old_class.store = @store
|
||||
patient = scoped_old_class.create!(name: 'Alice', notes: 'scoped secret note')
|
||||
|
||||
scoped_new_class = Class.new(Prism::Orm::Model) do
|
||||
table_name :patients
|
||||
encrypts :notes, key: NEW_SECRET, previous_keys: [OLD_SECRET]
|
||||
end
|
||||
scoped_new_class.store = @store
|
||||
|
||||
assert_equal 'scoped secret note', scoped_new_class.find(patient.id).notes
|
||||
end
|
||||
|
||||
def test_a_tampered_ciphertext_still_raises_even_with_rotation_configured
|
||||
Prism::Orm::Encryption.primary_key = OLD_SECRET
|
||||
old_class = define_patient_class
|
||||
patient = old_class.create!(name: 'Alice', ssn: '123-45-6789', notes: 'note')
|
||||
row = @store.find(:patients, patient.id)
|
||||
tampered = row.dup
|
||||
tampered['notes'] = row['notes'].sub(/^./) { |c| c == 'A' ? 'B' : 'A' }
|
||||
@store.update(:patients, patient.id, tampered)
|
||||
|
||||
Object.send(:remove_const, :RotatingPatient)
|
||||
Prism::Orm::Encryption.primary_key = NEW_SECRET
|
||||
Prism::Orm::Encryption.previous_keys = [OLD_SECRET]
|
||||
new_class = define_patient_class
|
||||
|
||||
assert_raises(Prism::Orm::Encryption::Error) { new_class.find(patient.id).notes }
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user