Files
prism/test/test_orm_encryption.rb
T

150 lines
5.8 KiB
Ruby

require 'minitest/autorun'
require 'prism'
require 'tmpdir'
class OrmEncryptionTest < Minitest::Test
def setup
@dir = Dir.mktmpdir('prism-encryption-store')
@store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore'))
@store.create_table(:patients, %w[id name ssn notes])
Prism::Orm::Encryption.primary_key = 'test-primary-key-do-not-use-in-prod'
patient_class = Class.new(Prism::Orm::Model) do
table_name :patients
encrypts :ssn, deterministic: true
encrypts :notes
end
patient_class.store = @store
Object.const_set(:EncryptedPatient, patient_class)
end
def teardown
FileUtils.remove_entry(@dir)
Prism::Orm::Encryption.primary_key = nil
Object.send(:remove_const, :EncryptedPatient) if Object.const_defined?(:EncryptedPatient)
end
def test_reading_back_the_attribute_returns_plaintext
patient = EncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'allergic to penicillin')
assert_equal '123-45-6789', patient.ssn
assert_equal 'allergic to penicillin', patient.notes
end
def test_the_stored_column_holds_ciphertext_not_plaintext
patient = EncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'secret note')
raw_row = @store.find(:patients, patient.id)
refute_equal '123-45-6789', raw_row['ssn']
refute_equal 'secret note', raw_row['notes']
refute_includes raw_row['ssn'], '123-45-6789'
end
def test_plaintext_survives_a_reload_from_disk
patient = EncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'note')
reloaded = EncryptedPatient.find(patient.id)
assert_equal '123-45-6789', reloaded.ssn
assert_equal 'note', reloaded.notes
end
def test_non_deterministic_column_produces_different_ciphertext_each_time
a = EncryptedPatient.create!(name: 'A', ssn: '111-11-1111', notes: 'same note')
b = EncryptedPatient.create!(name: 'B', ssn: '222-22-2222', notes: 'same note')
raw_a = @store.find(:patients, a.id)
raw_b = @store.find(:patients, b.id)
refute_equal raw_a['notes'], raw_b['notes']
end
def test_deterministic_column_produces_the_same_ciphertext_for_the_same_plaintext
a = EncryptedPatient.create!(name: 'A', ssn: '999-99-9999', notes: 'x')
b = EncryptedPatient.create!(name: 'B', ssn: '999-99-9999', notes: 'y')
raw_a = @store.find(:patients, a.id)
raw_b = @store.find(:patients, b.id)
assert_equal raw_a['ssn'], raw_b['ssn']
end
def test_where_on_a_deterministic_column_matches_by_plaintext
match = EncryptedPatient.create!(name: 'Match', ssn: '555-55-5555', notes: 'n')
EncryptedPatient.create!(name: 'Other', ssn: '666-66-6666', notes: 'n')
found = EncryptedPatient.where(ssn: '555-55-5555').to_a
assert_equal [match.id], found.map(&:id)
end
def test_find_by_on_a_deterministic_column_matches_by_plaintext
match = EncryptedPatient.create!(name: 'Match', ssn: '777-77-7777', notes: 'n')
found = EncryptedPatient.find_by(ssn: '777-77-7777')
assert_equal match.id, found.id
end
def test_querying_a_non_deterministic_column_by_plaintext_raises
error = assert_raises(Prism::Orm::Encryption::Error) { EncryptedPatient.where(notes: 'anything').to_a }
assert_match(/not deterministic/, error.message)
end
def test_nil_round_trips_as_nil
patient = EncryptedPatient.create!(name: 'Nil', ssn: nil, notes: nil)
assert_nil patient.ssn
assert_nil patient.notes
raw_row = @store.find(:patients, patient.id)
assert_nil raw_row['ssn']
reloaded = EncryptedPatient.find(patient.id)
assert_nil reloaded.ssn
end
def test_a_tampered_ciphertext_raises_on_read_instead_of_returning_garbage
patient = EncryptedPatient.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)
reloaded = EncryptedPatient.find(patient.id)
assert_raises(Prism::Orm::Encryption::Error) { reloaded.notes }
end
def test_encrypted_attributes_are_hidden_from_json_by_default
patient = EncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'note')
hash = patient.serializable_hash
refute_includes hash.keys, 'ssn'
refute_includes hash.keys, 'notes'
assert_equal 'Alice', hash['name']
end
def test_an_explicit_only_can_still_opt_a_hidden_column_back_into_json
patient = EncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: 'note')
hash = patient.serializable_hash(only: [:ssn])
assert_equal patient['ssn'], hash['ssn']
end
def test_two_different_attributes_on_the_same_record_get_independent_keys
patient = EncryptedPatient.create!(name: 'Alice', ssn: '123-45-6789', notes: '123-45-6789')
row = @store.find(:patients, patient.id)
refute_equal row['ssn'], row['notes']
end
def test_a_column_scoped_key_overrides_the_shared_primary_key
scoped_class = Class.new(Prism::Orm::Model) do
table_name :patients
encrypts :notes, key: 'a-completely-different-secret'
end
scoped_class.store = @store
record = scoped_class.create!(name: 'Bob', notes: 'private')
assert_equal 'private', record.notes
default_key_class = Class.new(Prism::Orm::Model) do
table_name :patients
encrypts :notes
end
default_key_class.store = @store
assert_raises(Prism::Orm::Encryption::Error) { default_key_class.find(record.id).notes }
end
def test_encrypts_without_any_key_configured_raises_a_clear_error
Prism::Orm::Encryption.primary_key = nil
unconfigured_class = Class.new(Prism::Orm::Model) do
table_name :patients
encrypts :notes
end
unconfigured_class.store = @store
error = assert_raises(Prism::Orm::Encryption::Error) { unconfigured_class.new(notes: 'x') }
assert_match(/primary_key=/, error.message)
end
end