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:
Rafael França 2021-04-21 17:46:44 -04:00 committed by GitHub
commit 9dc692bdc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 67 additions and 29 deletions

@ -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)