Merge pull request #42044 from basecamp/fix-are-multiple-schemes
Fix: use previous encryption schemes when support_unencrypted_data is on
This commit is contained in:
commit
9dc692bdc0
@ -12,17 +12,18 @@ class EncryptedAttributeType < ::ActiveRecord::Type::Text
|
||||
|
||||
attr_reader :scheme, :cast_type
|
||||
|
||||
delegate :key_provider, :downcase?, :deterministic?, :with_context, :fixed?, to: :scheme
|
||||
delegate :key_provider, :downcase?, :deterministic?, :previous_schemes, :with_context, :fixed?, to: :scheme
|
||||
|
||||
# === Options
|
||||
#
|
||||
# * <tt>:scheme</tt> - An +Scheme+ with the encryption properties for this attribute.
|
||||
# * <tt>:cast_type</tt> - A type that will be used to serialize (before encrypting) and deserialize
|
||||
# (after decrypting). +ActiveModel::Type::String+ by default.
|
||||
def initialize(scheme:, cast_type: ActiveModel::Type::String.new)
|
||||
def initialize(scheme:, cast_type: ActiveModel::Type::String.new, previous_type: false)
|
||||
super()
|
||||
@scheme = scheme
|
||||
@cast_type = cast_type
|
||||
@previous_type = previous_type
|
||||
end
|
||||
|
||||
def deserialize(value)
|
||||
@ -42,26 +43,28 @@ def changed_in_place?(raw_old_value, new_value)
|
||||
old_value != new_value
|
||||
end
|
||||
|
||||
def previous_encrypted_types(include_clear: true) # :nodoc:
|
||||
@additional_encrypted_types ||= {} # Memoizing on support_unencrypted_data so that we can tweak it during tests
|
||||
@additional_encrypted_types["#{support_unencrypted_data?} #{include_clear}"] ||= previous_schemes(include_clear: include_clear).collect do |scheme|
|
||||
EncryptedAttributeType.new(scheme: scheme)
|
||||
end
|
||||
def previous_types_including_clean_text # :nodoc:
|
||||
@previous_types_including_clean_text ||= {} # Memoizing on support_unencrypted_data so that we can tweak it during tests
|
||||
@previous_types_including_clean_text[support_unencrypted_data?] ||= build_previous_types_for(previous_schemes_including_clean_text)
|
||||
end
|
||||
|
||||
private
|
||||
def serialize_with_oldest?
|
||||
@serialize_with_oldest ||= fixed? && previous_encrypted_types(include_clear: false).present?
|
||||
def previous_schemes_including_clean_text
|
||||
previous_schemes.including((clean_text_scheme if support_unencrypted_data?)).compact
|
||||
end
|
||||
|
||||
def serialize_with_oldest(value)
|
||||
previous_encrypted_types.first.serialize(value)
|
||||
def previous_types
|
||||
@previous_types ||= build_previous_types_for(previous_schemes)
|
||||
end
|
||||
|
||||
def serialize_with_current(value)
|
||||
casted_value = cast_type.serialize(value)
|
||||
casted_value = casted_value&.downcase if downcase?
|
||||
encrypt(casted_value.to_s) unless casted_value.nil?
|
||||
def build_previous_types_for(schemes)
|
||||
schemes.collect do |scheme|
|
||||
EncryptedAttributeType.new(scheme: scheme, previous_type: true)
|
||||
end
|
||||
end
|
||||
|
||||
def previous_type?
|
||||
@previous_type
|
||||
end
|
||||
|
||||
def decrypt(value)
|
||||
@ -69,7 +72,7 @@ def decrypt(value)
|
||||
encryptor.decrypt(value, **decryption_options) unless value.nil?
|
||||
end
|
||||
rescue ActiveRecord::Encryption::Errors::Base => error
|
||||
if previous_encrypted_types.blank?
|
||||
if previous_types.blank?
|
||||
handle_deserialize_error(error, value)
|
||||
else
|
||||
try_to_deserialize_with_previous_encrypted_types(value)
|
||||
@ -77,10 +80,10 @@ def decrypt(value)
|
||||
end
|
||||
|
||||
def try_to_deserialize_with_previous_encrypted_types(value)
|
||||
previous_encrypted_types.each.with_index do |type, index|
|
||||
previous_types_including_clean_text.each.with_index do |type, index|
|
||||
break type.deserialize(value)
|
||||
rescue ActiveRecord::Encryption::Errors::Base => error
|
||||
handle_deserialize_error(error, value) if index == previous_encrypted_types.length - 1
|
||||
handle_deserialize_error(error, value) if index == previous_types.length - 1
|
||||
end
|
||||
end
|
||||
|
||||
@ -92,12 +95,18 @@ def handle_deserialize_error(error, value)
|
||||
end
|
||||
end
|
||||
|
||||
def previous_schemes(include_clear: true)
|
||||
scheme.previous_schemes.including((clean_text_scheme if include_clear && support_unencrypted_data?)).compact
|
||||
def serialize_with_oldest?
|
||||
@serialize_with_oldest ||= fixed? && previous_types.present?
|
||||
end
|
||||
|
||||
def support_unencrypted_data?
|
||||
ActiveRecord::Encryption.config.support_unencrypted_data
|
||||
def serialize_with_oldest(value)
|
||||
previous_types_including_clean_text.first.serialize(value)
|
||||
end
|
||||
|
||||
def serialize_with_current(value)
|
||||
casted_value = cast_type.serialize(value)
|
||||
casted_value = casted_value&.downcase if downcase?
|
||||
encrypt(casted_value.to_s) unless casted_value.nil?
|
||||
end
|
||||
|
||||
def encrypt(value)
|
||||
@ -110,6 +119,10 @@ def encryptor
|
||||
ActiveRecord::Encryption.encryptor
|
||||
end
|
||||
|
||||
def support_unencrypted_data?
|
||||
ActiveRecord::Encryption.config.support_unencrypted_data && !previous_type?
|
||||
end
|
||||
|
||||
def encryption_options
|
||||
@encryption_options ||= { key_provider: key_provider, cipher_options: { deterministic: deterministic? } }.compact
|
||||
end
|
||||
|
@ -46,7 +46,7 @@ def process_encrypted_query_arguments(args, check_for_additional_values)
|
||||
if args.is_a?(Array) && (options = args.first).is_a?(Hash)
|
||||
self.deterministic_encrypted_attributes&.each do |attribute_name|
|
||||
type = type_for_attribute(attribute_name)
|
||||
if !type.previous_encrypted_types.empty? && value = options[attribute_name]
|
||||
if !type.previous_types_including_clean_text.empty? && value = options[attribute_name]
|
||||
options[attribute_name] = process_encrypted_query_argument(value, check_for_additional_values, type)
|
||||
end
|
||||
end
|
||||
@ -72,7 +72,7 @@ def process_encrypted_query_argument(value, check_for_additional_values, type)
|
||||
end
|
||||
|
||||
def additional_values_for(value, type)
|
||||
type.previous_encrypted_types.collect do |additional_type|
|
||||
type.previous_types_including_clean_text.collect do |additional_type|
|
||||
AdditionalValue.new(value, additional_type)
|
||||
end
|
||||
end
|
||||
|
@ -14,7 +14,7 @@ def validate_each(record, attribute, value)
|
||||
klass = record.class
|
||||
if klass.deterministic_encrypted_attributes&.each do |attribute_name|
|
||||
encrypted_type = klass.type_for_attribute(attribute_name)
|
||||
[ encrypted_type, *encrypted_type.previous_encrypted_types ].each do |type|
|
||||
[ encrypted_type, *encrypted_type.previous_types_including_clean_text ].each do |type|
|
||||
encrypted_value = type.serialize(value)
|
||||
ActiveRecord::Encryption.without_encryption do
|
||||
super(record, attribute, encrypted_value)
|
||||
|
@ -106,7 +106,7 @@ class ActiveRecord::Encryption::EncryptableRecordApiTest < ActiveRecord::Encrypt
|
||||
|
||||
test "encrypt attributes encrypted with a previous encryption scheme" do
|
||||
author = EncryptedAuthor.create!(name: "david")
|
||||
old_type = EncryptedAuthor.type_for_attribute(:name).previous_encrypted_types.first
|
||||
old_type = EncryptedAuthor.type_for_attribute(:name).previous_types_including_clean_text.first
|
||||
value_encrypted_with_old_type = old_type.serialize("dhh")
|
||||
ActiveRecord::Encryption.without_encryption do
|
||||
author.update!(name: value_encrypted_with_old_type)
|
||||
|
@ -50,8 +50,33 @@ class ActiveRecord::Encryption::EncryptionSchemesTest < ActiveRecord::Encryption
|
||||
encrypts :name
|
||||
end
|
||||
|
||||
assert_equal 2, encrypted_author_class.type_for_attribute(:name).previous_encrypted_types.count
|
||||
previous_type_1, previous_type_2 = encrypted_author_class.type_for_attribute(:name).previous_encrypted_types
|
||||
assert_equal 2, encrypted_author_class.type_for_attribute(:name).previous_types_including_clean_text.count
|
||||
previous_type_1, previous_type_2 = encrypted_author_class.type_for_attribute(:name).previous_types_including_clean_text
|
||||
|
||||
author = ActiveRecord::Encryption.without_encryption do
|
||||
encrypted_author_class.create name: previous_type_1.serialize("1")
|
||||
end
|
||||
assert_equal "0", author.reload.name
|
||||
|
||||
author = ActiveRecord::Encryption.without_encryption do
|
||||
encrypted_author_class.create name: previous_type_2.serialize("2")
|
||||
end
|
||||
assert_equal "1", author.reload.name
|
||||
end
|
||||
|
||||
test "use global previous schemes to decrypt data encrypted with previous schemes with unencrypted data" do
|
||||
ActiveRecord::Encryption.config.support_unencrypted_data = true
|
||||
ActiveRecord::Encryption.config.previous = [ { encryptor: TestEncryptor.new("0" => "1") }, { encryptor: TestEncryptor.new("1" => "2") } ]
|
||||
|
||||
# We want to evaluate .encrypts *after* tweaking the config property
|
||||
encrypted_author_class = Class.new(Author) do
|
||||
self.table_name = "authors"
|
||||
|
||||
encrypts :name
|
||||
end
|
||||
|
||||
assert_equal 3, encrypted_author_class.type_for_attribute(:name).previous_types_including_clean_text.count
|
||||
previous_type_1, previous_type_2 = encrypted_author_class.type_for_attribute(:name).previous_types_including_clean_text
|
||||
|
||||
author = ActiveRecord::Encryption.without_encryption do
|
||||
encrypted_author_class.create name: previous_type_1.serialize("1")
|
||||
@ -128,7 +153,7 @@ class EncryptedAuthor2 < Author
|
||||
|
||||
def create_author_with_name_encrypted_with_previous_scheme
|
||||
author = EncryptedAuthor.create!(name: "david")
|
||||
old_type = EncryptedAuthor.type_for_attribute(:name).previous_encrypted_types.first
|
||||
old_type = EncryptedAuthor.type_for_attribute(:name).previous_types_including_clean_text.first
|
||||
value_encrypted_with_old_type = old_type.serialize("dhh")
|
||||
ActiveRecord::Encryption.without_encryption do
|
||||
author.update!(name: value_encrypted_with_old_type)
|
||||
|
Loading…
Reference in New Issue
Block a user