module ActiveRecord::Encryption::EncryptableRecord
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.
Constants
- ORIGINAL_ATTRIBUTE_PREFIX
Public Instance Methods
# File lib/active_record/encryption/encryptable_record.rb, line 127 def add_length_validation_for_encrypted_columns encrypted_attributes&.each do |attribute_name| validate_column_size attribute_name end end
Returns the ciphertext for attribute_name
.
# File lib/active_record/encryption/encryptable_record.rb, line 146 def ciphertext_for(attribute_name) read_attribute_before_type_cast(attribute_name) end
Decrypts all the encryptable attributes and saves the changes.
# File lib/active_record/encryption/encryptable_record.rb, line 156 def decrypt decrypt_attributes if has_encrypted_attributes? end
Returns the list of deterministic encryptable attributes in the model class.
# File lib/active_record/encryption/encryptable_record.rb, line 56 def deterministic_encrypted_attributes @deterministic_encrypted_attributes ||= encrypted_attributes&.find_all do |attribute_name| type_for_attribute(attribute_name).deterministic? end end
Encrypts all the encryptable attributes and saves the changes.
# File lib/active_record/encryption/encryptable_record.rb, line 151 def encrypt encrypt_attributes if has_encrypted_attributes? end
# File lib/active_record/encryption/encryptable_record.rb, line 82 def encrypt_attribute(name, attribute_scheme) encrypted_attributes << name.to_sym attribute name do |cast_type| ActiveRecord::Encryption::EncryptedAttributeType.new scheme: attribute_scheme, cast_type: cast_type end preserve_original_encrypted(name) if attribute_scheme.ignore_case? ActiveRecord::Encryption.encrypted_attribute_was_declared(self, name) end
Returns whether a given attribute is encrypted or not.
# File lib/active_record/encryption/encryptable_record.rb, line 141 def encrypted_attribute?(attribute_name) ActiveRecord::Encryption.encryptor.encrypted? ciphertext_for(attribute_name) end
Encrypts the name
attribute.
Options¶ ↑
-
:key_provider
- A key provider to provide encryption and decryption keys. Defaults toActiveRecord::Encryption.key_provider
. -
:key
- 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. -
:deterministic
- 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 totrue
, 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. -
:downcase
- 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. -
:ignore_case
- 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. -
:context_properties
- Additional properties that will overrideContext
settings when this attribute is encrypted and decrypted. E.g:encryptor:
,cipher:
,message_serializer:
, etc. -
:previous
- 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 byencrypts
. Also, when deterministic encryption is used, they will be used to generate additional ciphertexts to check in the queries.
# File lib/active_record/encryption/encryptable_record.rb, line 45 def encrypts(*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 classes scheme = scheme_for key_provider: key_provider, key: key, deterministic: deterministic, downcase: downcase, \ ignore_case: ignore_case, previous: previous, **context_properties names.each do |name| encrypt_attribute name, scheme end end
# File lib/active_record/encryption/encryptable_record.rb, line 76 def global_previous_schemes_for(scheme) ActiveRecord::Encryption.config.previous_schemes.collect do |previous_scheme| scheme.merge(previous_scheme) end end
# File lib/active_record/encryption/encryptable_record.rb, line 121 def load_schema! super add_length_validation_for_encrypted_columns if ActiveRecord::Encryption.config.validate_column_size end
# File lib/active_record/encryption/encryptable_record.rb, line 104 def override_accessors_to_preserve_original(name, original_attribute_name) include(Module.new do define_method name do if ((value = super()) && encrypted_attribute?(name)) || !ActiveRecord::Encryption.config.support_unencrypted_data send(original_attribute_name) else value end end define_method "#{name}=" do |value| self.send "#{original_attribute_name}=", value super(value) end end) end
# File lib/active_record/encryption/encryptable_record.rb, line 93 def preserve_original_encrypted(name) original_attribute_name = "#{ORIGINAL_ATTRIBUTE_PREFIX}#{name}".to_sym if !ActiveRecord::Encryption.config.support_unencrypted_data && !column_names.include?(original_attribute_name.to_s) raise Errors::Configuration, "To use :ignore_case for '#{name}' you must create an additional column named '#{original_attribute_name}'" end encrypts original_attribute_name override_accessors_to_preserve_original name, original_attribute_name end
# File lib/active_record/encryption/encryptable_record.rb, line 68 def scheme_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).tap do |scheme| scheme.previous_schemes = global_previous_schemes_for(scheme) + Array.wrap(previous).collect { |scheme_config| ActiveRecord::Encryption::Scheme.new(**scheme_config) } end end
Given a attribute name, it returns the name of the source attribute when it’s a preserved one.
# File lib/active_record/encryption/encryptable_record.rb, line 63 def source_attribute_from_preserved_attribute(attribute_name) attribute_name.to_s.sub(ORIGINAL_ATTRIBUTE_PREFIX, "") if /^#{ORIGINAL_ATTRIBUTE_PREFIX}/.match?(attribute_name) end
# File lib/active_record/encryption/encryptable_record.rb, line 133 def validate_column_size(attribute_name) if limit = columns_hash[attribute_name.to_s]&.limit validates_length_of attribute_name, maximum: limit end end
Private Instance Methods
# File lib/active_record/encryption/encryptable_record.rb, line 190 def build_decrypt_attribute_assignments Array(self.class.encrypted_attributes).collect do |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_h end
# File lib/active_record/encryption/encryptable_record.rb, line 184 def build_encrypt_attribute_assignments Array(self.class.encrypted_attributes).index_with do |attribute_name| self[attribute_name] end end
# File lib/active_record/encryption/encryptable_record.rb, line 199 def cant_modify_encrypted_attributes_when_frozen self.class&.encrypted_attributes.each do |attribute| errors.add(attribute.to_sym, "can't be modified because it is encrypted") if changed_attributes.include?(attribute) end end
# File lib/active_record/encryption/encryptable_record.rb, line 169 def decrypt_attributes validate_encryption_allowed decrypt_attribute_assignments = build_decrypt_attribute_assignments ActiveRecord::Encryption.without_encryption { update_columns decrypt_attribute_assignments } end
# File lib/active_record/encryption/encryptable_record.rb, line 163 def encrypt_attributes validate_encryption_allowed update_columns build_encrypt_attribute_assignments end
# File lib/active_record/encryption/encryptable_record.rb, line 180 def has_encrypted_attributes? self.class.encrypted_attributes.present? end
# File lib/active_record/encryption/encryptable_record.rb, line 176 def validate_encryption_allowed raise ActiveRecord::Encryption::Errors::Configuration, "can't be modified because it is encrypted" if ActiveRecord::Encryption.context.frozen_encryption? end