Skip to content

Instantly share code, notes, and snippets.

@papes1ns
Last active March 20, 2022 13:58
Show Gist options
  • Save papes1ns/b23d65717189014e1691683416b3b0d7 to your computer and use it in GitHub Desktop.
Save papes1ns/b23d65717189014e1691683416b3b0d7 to your computer and use it in GitHub Desktop.
Adds hashing and encryption capabilities to ActiveRecord models.
# Nathan Papes 2020
#
# Adds hashing and encryption capabilities to ActiveRecord models.
# This concern was built to conform with GDPR requirements.
module GDPR_Security
extend ActiveSupport::Concern
class_methods do
# Adds the getters and setters for encryting and decrypting attributes.
# For example:
#
# class User < ActiveRecord::Base
# include GDPR_Security
# attr_encrypted :email, :ip_address, :user_agent
# end
#
# With this example the following setters will be created:
# * encrypt_email => stores value in encrypted_email
# * encrypt_ip_address => stores value in encrypted_ip_address
# * encrypt_user_agent => stores value in encrypted_user_agent
#
# The following getters will be creatd as well:
# * decrypted_email => reads value in encrypted_email
# * decrypted_ip_address => reads value in encrypted_ip_address
# * decrypted_user_agent => reads value in encrypted_user_agent
#
# Note: this module assumes you already have the columns for the encrypted
# values in your database.
#
def attr_encrypted(*attributes)
attributes.each do |attribute|
define_method("encrypt_#{attribute}") do |value|
return if value.nil?
send("encrypted_#{attribute}=", EncryptionService.encrypt(value))
end
define_method("decrypted_#{attribute}") do
value = send("encrypted_#{attribute}")
return if value.nil?
EncryptionService.decrypt(value)
end
end
end
# Adds the getters and setters for hashing attributes
# For example:
#
# class User < ActiveRecord::Base
# include GDPR_Security
# attr_hasher :email
# end
#
# With this example the following setters will be created:
# * hash_email => stores value in hashed_email
#
# The following getters will be creatd as well:
# * where_hashes(hash)
#
# To query for a user by email, see the example below:
# User.where_hashes({email: "[email protected]"})
#
# This will hash the value and look for a match in the hashed column in
# the database.
#
# Note: this module assumes you already have the columns for the hashed
# values in your database.
#
def attr_hasher(*attributes)
attributes.each do |attribute|
define_method("hash_#{attribute}") do |value|
return if value.nil?
send("hashed_#{attribute}=", HashingService.perform_hash(value))
end
end
end
# Returns ActiveRecord::Relation
# args
# 0: hash - AND together key, value pairs. Key is attr and value is non-hashed value
def where_hashes(hash)
where( Hash[ hash.map{|k,v| ["hashed_#{k}", HashingService.perform_hash(v)] } ] )
end
end
# Convenience service built on top of Rails built in encryption classes
class EncryptionService
@@keygen ||= ActiveSupport::KeyGenerator.new(ENV["SECRET_KEY_BASE"])
@@len ||= ActiveSupport::MessageEncryptor.key_len
# Returns encrypted attribute.
# Salt can be added on a per attribute basis or a global salt can be used.
# To enable global salt, ensure the SECRET_SALT env var is set.
def self.encrypt(text,options={})
text = text.to_s unless text.is_a?(String)
# add random salt if one is not provided
options[:randomize_salt] = true if not ENV["SECRET_SALT"]
if options[:randomize_salt]
rand_salt = SecureRandom.hex(len)
key = @@keygen.generate_key(rand_salt, @@len)
else
# CachingKeyGenerator is only effiecient if the params passed to
# generate_key method were used previously.
# see https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/activesupport/lib/active_support/key_generator.rb#L37
key = ActiveSupport::CachingKeyGenerator.new(@@keygen).generate_key(ENV["SECRET_SALT"], @@len)
end
crypt = ActiveSupport::MessageEncryptor.new(key)
encrypted_data = crypt.encrypt_and_sign(text)
# returns string in format of salt$$signed_data if random salt is used
[rand_salt, encrypted_data].compact.join("$$")
end
# Returns decrypted attribute.
# Tries to use the salt on the string, if there is none, it will default
# to use the global salt in SECRET_SALT env var.
def self.decrypt(text)
cipher_elements = text.split("$$")
len = ActiveSupport::MessageEncryptor.key_len
if cipher_elements.length == 2
salt = cipher_elements[0]
data = cipher_elements[1]
key = @@keygen.generate_key(cipher_elements[0], @@len)
else
data = cipher_elements[0]
key = ActiveSupport::CachingKeyGenerator.new(@@keygen).generate_key(ENV["SECRET_SALT"], @@len)
end
crypt = ActiveSupport::MessageEncryptor.new(key)
crypt.decrypt_and_verify(data)
end
end
# Simple service to hash values.
class HashingService
def self.perform_hash(value)
# args
# 0: value to hash
# 1: secret salt
# 2: interations
# 3: length
OpenSSL::PKCS5.pbkdf2_hmac_sha1(value, ENV["SECRET_SALT"] || ("F"*16), 256, 64).unpack("H*")[0]
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment