module Decidim::RecordEncryptor
A concern that provides attribute encryption e.g. to active record models.
Use this e.g. in models as follows:
class Example < ApplicationRecord
include Decidim::RecordEncryptor encrypt_attribute :name, type: :string encrypt_attribute :metadata, type: :hash
end
Public Instance Methods
encrypt_attribute(attribute, type:)
click to toggle source
Public: Defines an attribute that should be encrypted
# File lib/decidim/record_encryptor.rb, line 28 def encrypt_attribute(attribute, type:) self.encrypted_attributes ||= [] raise "The attribute #{attribute} is already defined as encrypted" if encrypted_attributes.include?(attribute) encrypted_attributes << attribute # Defines the suffix for the encrypt and decrypt methods. E.g. when # the `type` is `:hash`, method `decrypt_hash_values` would be called # for decryption and `encrypt_hash_values` would be called for # encryption. method_suffix = begin case type when :hash "hash_values" else "value" end end # Dynamically defines the getter and setter for the encrypted attribute. # E.g. when called as `encrypt_attribute :name, type: :string`, this # would define the following methods: # # def name # decrypt_value(super) # end # # def name=(value) # super(encrypt_value(value)) # end class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{attribute} return @#{attribute}_decrypted if instance_variable_defined?(:@#{attribute}_decrypted) encrypted_value = begin if defined?(super) super elsif instance_variable_defined?(:@#{attribute}) @#{attribute} end end @#{attribute}_decrypted = decrypt_#{method_suffix}(encrypted_value) end def #{attribute}=(value) remove_instance_variable(:@#{attribute}_decrypted) if instance_variable_defined?(:@#{attribute}_decrypted) encrypted_value = encrypt_#{method_suffix}(value) if defined?(super) super(encrypted_value) else @#{attribute} = encrypted_value end end RUBY end
Private Instance Methods
decrypt_hash_values(hash)
click to toggle source
# File lib/decidim/record_encryptor.rb, line 116 def decrypt_hash_values(hash) return hash unless hash.is_a?(Hash) hash.transform_values do |value| # If the value is not a String, it is likely a legacy unencrypted hash # value. Also, `ActiveSupport::JSON.decode` expects the value passed to # it to be a String. Otherwise it would raise a TypeError. next value unless value.is_a?(String) decrypted_value = decrypt_value(value) # When handling legacy unencrypted hash values, the decrypted values # could not be valid JSON strings. They could be normal strings that # cannot be JSON decoded. begin ActiveSupport::JSON.decode(decrypted_value) rescue TypeError "" rescue JSON::ParserError decrypted_value end end end
decrypt_value(value)
click to toggle source
# File lib/decidim/record_encryptor.rb, line 104 def decrypt_value(value) Decidim::AttributeEncryptor.decrypt(value) rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature # Support for legacy unencrypted values. This is necessary e.g. when # migrating the original unencrypted values to encrypted values. value end
encrypt_hash_values(hash)
click to toggle source
# File lib/decidim/record_encryptor.rb, line 140 def encrypt_hash_values(hash) return hash unless hash.is_a?(Hash) # The values are stored in JSON encoded format in order to match the # PostgreSQL adapter's default functionality as you can see at: # https://git.io/JkdYJ hash.transform_values { |value| encrypt_value(ActiveSupport::JSON.encode(value)) } end
encrypt_value(value)
click to toggle source
# File lib/decidim/record_encryptor.rb, line 112 def encrypt_value(value) Decidim::AttributeEncryptor.encrypt(value) end
ensure_encrypted_attributes()
click to toggle source
Re-assign the encrypted attributes before save so they are also saved when they are modified without calling the accessors. This could happen e.g. for hashes which are modified directly as follows:
record = Example.find(1) record.metadata["foo"] = "bar" record.save!
This will also clear the cached attributes during saving so that next time they are accessed, they will be updated according to the stored values.
# File lib/decidim/record_encryptor.rb, line 98 def ensure_encrypted_attributes self.class.encrypted_attributes.each do |attr| send("#{attr}=", send(attr)) end end