Last active
March 20, 2022 13:58
-
-
Save papes1ns/b23d65717189014e1691683416b3b0d7 to your computer and use it in GitHub Desktop.
Adds hashing and encryption capabilities to ActiveRecord models.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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