# frozen_string_literal: truemoduleActiveRecordmoduleEncryption# This is the concern mixed in Active Record models to make them encryptable. It adds the +encrypts+# attribute declaration, as well as the API to encrypt and decrypt records.moduleEncryptableRecordextendActiveSupport::Concernincludeddoclass_attribute:encrypted_attributesvalidate:cant_modify_encrypted_attributes_when_frozen,if: ->{has_encrypted_attributes?&&ActiveRecord::Encryption.context.frozen_encryption?}endclass_methodsdo# Encrypts the +name+ attribute.## === Options## * <tt>:key_provider</tt> - A key provider to provide encryption and decryption keys. Defaults to# +ActiveRecord::Encryption.key_provider+.# * <tt>:key</tt> - A password to derive the key from. It's a shorthand for a +:key_provider+ that# serves derivated keys. Both options can't be used at the same time.# * <tt>:deterministic</tt> - By default, encryption is not deterministic. It will use a random# initialization vector for each encryption operation. This means that encrypting the same content# with the same key twice will generate different ciphertexts. When set to +true+, it will generate the# initialization vector based on the encrypted content. This means that the same content will generate# the same ciphertexts. This enables querying encrypted text with Active Record. Deterministic encryption# will use the oldest encryption scheme to encrypt new data by default. You can change this by setting# +deterministic: { fixed: false }+. That will make it use the newest encryption scheme for encrypting new# data.# * <tt>:downcase</tt> - When true, it converts the encrypted content to downcase automatically. This allows to# effectively ignore case when querying data. Notice that the case is lost. Use +:ignore_case+ if you are interested# in preserving it.# * <tt>:ignore_case</tt> - When true, it behaves like +:downcase+ but, it also preserves the original case in a specially# designated column +original_<name>+. When reading the encrypted content, the version with the original case is# served. But you can still execute queries that will ignore the case. This option can only be used when +:deterministic+# is true.# * <tt>:context_properties</tt> - Additional properties that will override +Context+ settings when this attribute is# encrypted and decrypted. E.g: +encryptor:+, +cipher:+, +message_serializer:+, etc.# * <tt>:previous</tt> - List of previous encryption schemes. When provided, they will be used in order when trying to read# the attribute. Each entry of the list can contain the properties supported by #encrypts. Also, when deterministic# encryption is used, they will be used to generate additional ciphertexts to check in the queries.defencrypts(*names,key_provider: nil,key: nil,deterministic: false,downcase: false,ignore_case: false,previous: [],**context_properties)self.encrypted_attributes||=Set.new# not using :default because the instance would be shared across classesscheme=scheme_forkey_provider: key_provider,key: key,deterministic: deterministic,downcase: downcase,\ignore_case: ignore_case,previous: previous,**context_propertiesnames.eachdo|name|encrypt_attributename,schemeendend# Returns the list of deterministic encryptable attributes in the model class.defdeterministic_encrypted_attributes@deterministic_encrypted_attributes||=encrypted_attributes&.find_alldo|attribute_name|type_for_attribute(attribute_name).deterministic?endend# Given a attribute name, it returns the name of the source attribute when it's a preserved one.defsource_attribute_from_preserved_attribute(attribute_name)attribute_name.to_s.sub(ORIGINAL_ATTRIBUTE_PREFIX,"")if/^#{ORIGINAL_ATTRIBUTE_PREFIX}/.match?(attribute_name)endprivatedefscheme_for(key_provider: nil,key: nil,deterministic: false,downcase: false,ignore_case: false,previous: [],**context_properties)ActiveRecord::Encryption::Scheme.new(key_provider: key_provider,key: key,deterministic: deterministic,downcase: downcase,ignore_case: ignore_case,**context_properties).tapdo|scheme|scheme.previous_schemes=global_previous_schemes_for(scheme)+Array.wrap(previous).collect{|scheme_config|ActiveRecord::Encryption::Scheme.new(**scheme_config)}endenddefglobal_previous_schemes_for(scheme)ActiveRecord::Encryption.config.previous_schemes.collectdo|previous_scheme|scheme.merge(previous_scheme)endenddefencrypt_attribute(name,attribute_scheme)encrypted_attributes<<name.to_symattributenamedo|cast_type|ActiveRecord::Encryption::EncryptedAttributeType.newscheme: attribute_scheme,cast_type: cast_typeendpreserve_original_encrypted(name)ifattribute_scheme.ignore_case?ActiveRecord::Encryption.encrypted_attribute_was_declared(self,name)enddefpreserve_original_encrypted(name)original_attribute_name="#{ORIGINAL_ATTRIBUTE_PREFIX}#{name}".to_symif!ActiveRecord::Encryption.config.support_unencrypted_data&&!column_names.include?(original_attribute_name.to_s)raiseErrors::Configuration,"To use :ignore_case for '#{name}' you must create an additional column named '#{original_attribute_name}'"endencryptsoriginal_attribute_nameoverride_accessors_to_preserve_originalname,original_attribute_nameenddefoverride_accessors_to_preserve_original(name,original_attribute_name)include(Module.newdodefine_methodnamedoif((value=super())&&encrypted_attribute?(name))||!ActiveRecord::Encryption.config.support_unencrypted_datasend(original_attribute_name)elsevalueendenddefine_method"#{name}="do|value|self.send"#{original_attribute_name}=",valuesuper(value)endend)enddefload_schema!superadd_length_validation_for_encrypted_columnsifActiveRecord::Encryption.config.validate_column_sizeenddefadd_length_validation_for_encrypted_columnsencrypted_attributes&.eachdo|attribute_name|validate_column_sizeattribute_nameendenddefvalidate_column_size(attribute_name)iflimit=columns_hash[attribute_name.to_s]&.limitvalidates_length_ofattribute_name,maximum: limitendendend# Returns whether a given attribute is encrypted or not.defencrypted_attribute?(attribute_name)ActiveRecord::Encryption.encryptor.encrypted?ciphertext_for(attribute_name)end# Returns the ciphertext for +attribute_name+.defciphertext_for(attribute_name)read_attribute_before_type_cast(attribute_name)end# Encrypts all the encryptable attributes and saves the changes.defencryptencrypt_attributesifhas_encrypted_attributes?end# Decrypts all the encryptable attributes and saves the changes.defdecryptdecrypt_attributesifhas_encrypted_attributes?endprivateORIGINAL_ATTRIBUTE_PREFIX="original_"defencrypt_attributesvalidate_encryption_allowedupdate_columnsbuild_encrypt_attribute_assignmentsenddefdecrypt_attributesvalidate_encryption_alloweddecrypt_attribute_assignments=build_decrypt_attribute_assignmentsActiveRecord::Encryption.without_encryption{update_columnsdecrypt_attribute_assignments}enddefvalidate_encryption_allowedraiseActiveRecord::Encryption::Errors::Configuration,"can't be modified because it is encrypted"ifActiveRecord::Encryption.context.frozen_encryption?enddefhas_encrypted_attributes?self.class.encrypted_attributes.present?enddefbuild_encrypt_attribute_assignmentsArray(self.class.encrypted_attributes).index_withdo|attribute_name|self[attribute_name]endenddefbuild_decrypt_attribute_assignmentsArray(self.class.encrypted_attributes).collectdo|attribute_name|type=type_for_attribute(attribute_name)encrypted_value=ciphertext_for(attribute_name)new_value=type.deserialize(encrypted_value)[attribute_name,new_value]end.to_henddefcant_modify_encrypted_attributes_when_frozenself.class&.encrypted_attributes.eachdo|attribute|errors.add(attribute.to_sym,"can't be modified because it is encrypted")ifchanged_attributes.include?(attribute)endendendendend