# frozen_string_literal: true module ActiveRecord module Encryption # An ActiveModel::Type::Value that encrypts/decrypts strings of text. # # This is the central piece that connects the encryption system with +encrypts+ declarations in the # model classes. Whenever you declare an attribute as encrypted, it configures an +EncryptedAttributeType+ # for that attribute. class EncryptedAttributeType < ::ActiveRecord::Type::Text include ActiveModel::Type::Helpers::Mutable attr_reader :scheme, :cast_type delegate :key_provider, :downcase?, :deterministic?, :previous_schemes, :with_context, :fixed?, to: :scheme delegate :accessor, to: :cast_type # === Options # # * :scheme - A +Scheme+ with the encryption properties for this attribute. # * :cast_type - 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, previous_type: false, default: nil) super() @scheme = scheme @cast_type = cast_type @previous_type = previous_type @default = default end def cast(value) cast_type.cast(value) end def deserialize(value) cast_type.deserialize decrypt(value) end def serialize(value) if serialize_with_oldest? serialize_with_oldest(value) else serialize_with_current(value) end end def encrypted?(value) with_context { encryptor.encrypted? value } end def changed_in_place?(raw_old_value, new_value) old_value = raw_old_value.nil? ? nil : deserialize(raw_old_value) old_value != new_value end def previous_types # :nodoc: @previous_types ||= {} # Memoizing on support_unencrypted_data so that we can tweak it during tests @previous_types[support_unencrypted_data?] ||= build_previous_types_for(previous_schemes_including_clean_text) end def support_unencrypted_data? ActiveRecord::Encryption.config.support_unencrypted_data && scheme.support_unencrypted_data? && !previous_type? end private def previous_schemes_including_clean_text previous_schemes.including((clean_text_scheme if support_unencrypted_data?)).compact end def previous_types_without_clean_text @previous_types_without_clean_text ||= build_previous_types_for(previous_schemes) end 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) with_context do unless value.nil? if @default && @default == value value else encryptor.decrypt(value, **decryption_options) end end end rescue ActiveRecord::Encryption::Errors::Base => error if previous_types_without_clean_text.blank? handle_deserialize_error(error, value) else try_to_deserialize_with_previous_encrypted_types(value) end end def try_to_deserialize_with_previous_encrypted_types(value) previous_types.each.with_index do |type, index| break type.deserialize(value) rescue ActiveRecord::Encryption::Errors::Base => error handle_deserialize_error(error, value) if index == previous_types.length - 1 end end def handle_deserialize_error(error, value) if error.is_a?(Errors::Decryption) && support_unencrypted_data? value else raise error end end def serialize_with_oldest? @serialize_with_oldest ||= fixed? && previous_types_without_clean_text.present? end def serialize_with_oldest(value) previous_types.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) with_context do encryptor.encrypt(value, **encryption_options) end end def encryptor ActiveRecord::Encryption.encryptor end def encryption_options @encryption_options ||= { key_provider: key_provider, cipher_options: { deterministic: deterministic? } }.compact end def decryption_options @decryption_options ||= { key_provider: key_provider }.compact end def clean_text_scheme @clean_text_scheme ||= ActiveRecord::Encryption::Scheme.new(downcase: downcase?, encryptor: ActiveRecord::Encryption::NullEncryptor.new) end end end end