Last active
December 21, 2015 23:49
-
-
Save tstachl/6385210 to your computer and use it in GitHub Desktop.
Monkey patch field encryption into Mongoid.
This file contains hidden or 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
class Mongoid::Fields::Standard | |
# Is the field encrypted or not? | |
# | |
# @example Is the field encrypted? | |
# field.encrypted? | |
# | |
# @return [ true, false ] If the field is encrypted. | |
# | |
# @since 4.0.0 | |
def encrypted? | |
false | |
end | |
end | |
class Mongoid::Fields::Encrypted < Mongoid::Fields::Standard | |
# Demongoize the object based on the current locale. Will look in the | |
# hash for the current locale. | |
# | |
# @example Get the demongoized value. | |
# field.demongoize({ | |
# "value" => "encrypted_object", | |
# "salt" => "salt_used_to_encrypt", | |
# "iv" => "iv_used_to_encrypt" | |
# }) | |
# | |
# @param [ Hash ] object The hash of translations. | |
# | |
# @return [ Object ] The value for the current locale. | |
# | |
# @since 4.0.0 | |
def demongoize(object) | |
if object | |
type.demongoize(decrypt(object)) | |
end | |
end | |
# Is the field encrypted or not? | |
# | |
# @example Is the field encrypted? | |
# field.encrypted? | |
# | |
# @return [ true, false ] If the field is encrypted. | |
# | |
# @since 4.0.0 | |
def encrypted? | |
true | |
end | |
# Convert the provided object into a hash with encryption values. | |
# | |
# @example Encrypt the value. | |
# field.mongoize("testing") | |
# | |
# @param [ Object ] object The object to convert. | |
# | |
# @return [ Hash ] The encryption hash to store in the database. | |
# | |
# @since 4.0.0 | |
def mongoize(object) | |
encrypt(type.mongoize(object)) | |
end | |
private | |
# Decrypt the value from the provided object. | |
# | |
# @api private | |
# | |
# @example Decrypt the value. | |
# field.decrypt({ | |
# "value" => "encrypted_object", | |
# "salt" => "salt_used_to_encrypt", | |
# "iv" => "iv_used_to_encrypt" | |
# }) | |
# | |
# @param [ Hash ] object The object containing the encrypted value. | |
# | |
# @return [ Object ] The decrypted object. | |
# | |
# @since 4.0.0 | |
def decrypt(object) | |
Marshal.load(Encryptor.decrypt( | |
object.inject({}){ |obj, (k, v)| | |
obj[k.to_sym] = Base64.decode64(v) | |
obj | |
}.merge(key: ENV['ENCRYPTION_KEY']) | |
)) | |
end | |
# Encrypt the value from the provided object. | |
# | |
# @api private | |
# | |
# @example Encrypt the value. | |
# field.encrypt({ | |
# "value" => "encrypted_object", | |
# "salt" => "salt_used_to_encrypt", | |
# "iv" => "iv_used_to_encrypt" | |
# }) | |
# | |
# @param [ Hash ] object The object containing the encrypted value. | |
# | |
# @return [ Object ] The encrypted object. | |
# | |
# @since 4.0.0 | |
def encrypt(object) | |
cipher = OpenSSL::Cipher.new('aes-256-cbc') | |
value = Marshal.dump(object) | |
iv = cipher.random_iv.to_s | |
salt = cipher.random_key.to_s | |
key = ENV['ENCRYPTION_KEY'] | |
{ | |
value: Base64.encode64(Encryptor.encrypt(value: value, iv: iv, salt: salt, key: key)), | |
iv: Base64.encode64(iv), | |
salt: Base64.encode64(salt) | |
} | |
end | |
end | |
module Mongoid | |
module Fields | |
included do | |
class_attribute :aliased_fields | |
class_attribute :localized_fields | |
class_attribute :encrypted_fields | |
class_attribute :fields | |
class_attribute :pre_processed_defaults | |
class_attribute :post_processed_defaults | |
self.aliased_fields = { "id" => "_id" } | |
self.fields = {} | |
self.localized_fields = {} | |
self.encrypted_fields = {} | |
self.pre_processed_defaults = [] | |
self.post_processed_defaults = [] | |
field( | |
:_id, | |
default: ->{ Moped::BSON::ObjectId.new }, | |
pre_processed: true, | |
type: Moped::BSON::ObjectId | |
) | |
alias :id :_id | |
alias :id= :_id= | |
end | |
module ClassMethods | |
protected | |
# Create the field accessors. | |
# | |
# @example Generate the accessors. | |
# Person.create_accessors(:name, "name") | |
# person.name #=> returns the field | |
# person.name = "" #=> sets the field | |
# person.name? #=> Is the field present? | |
# person.name_before_type_cast #=> returns the field before type cast | |
# | |
# @param [ Symbol ] name The name of the field. | |
# @param [ Symbol ] meth The name of the accessor. | |
# @param [ Hash ] options The options. | |
# | |
# @since 2.0.0 | |
def create_accessors(name, meth, options = {}) | |
field = fields[name] | |
create_field_getter(name, meth, field) | |
create_field_getter_before_type_cast(name, meth) | |
create_field_setter(name, meth, field) | |
create_field_check(name, meth) | |
if options[:localize] | |
create_translations_getter(name, meth) | |
create_translations_setter(name, meth, field) | |
localized_fields[name] = field | |
end | |
if options[:encrypt] | |
create_encryption_getter(name, meth) | |
create_encryption_setter(name, meth, field) | |
encrypted_fields[name] = field | |
end | |
end | |
# Create the encryption getter method for the provided field. | |
# | |
# @example Create the encryption getter. | |
# Model.create_encryption_getter("name", "name") | |
# | |
# @param [ String ] name The name of the attribute. | |
# @param [ String ] meth The name of the method. | |
# | |
# @since 4.0.0 | |
def create_encryption_getter(name, meth) | |
generated_methods.module_eval do | |
re_define_method("#{meth}_encryption") do | |
(attributes[name] ||= {}).with_indifferent_access | |
end | |
alias_method :"#{meth}_e", :"#{meth}_encryption" | |
end | |
end | |
# Create the encryption setter method for the provided field. | |
# | |
# @example Create the encryption setter. | |
# Model.create_encryption_setter("name", "name") | |
# | |
# @param [ String ] name The name of the attribute. | |
# @param [ String ] meth The name of the method. | |
# @param [ Field ] field The field. | |
# | |
# @since 4.0.0 | |
def create_encryption_setter(name, meth, field) | |
generated_methods.module_eval do | |
re_define_method("#{meth}_encryption=") do |value| | |
attribute_will_change!(name) | |
if value | |
value.update_values do |_value| | |
field.type.mongoize(_value) | |
end | |
end | |
attributes[name] = value | |
end | |
alias_method :"#{meth}_e=", :"#{meth}_encryption=" | |
end | |
end | |
def field_for(name, options) | |
opts = options.merge(klass: self) | |
return Fields::Localized.new(name, opts) if options[:localize] | |
return Fields::Encrypted.new(name, opts) if options[:encrypt] | |
return Fields::ForeignKey.new(name, opts) if options[:identity] | |
Fields::Standard.new(name, opts) | |
end | |
end | |
end | |
end | |
Mongoid::Fields::Validators::Macro::OPTIONS.append(:encrypt) |
This file contains hidden or 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
class Setting | |
include Mongoid::Document | |
field :key, type: String | |
field :value, type: String, encrypt: true | |
end |
This file contains hidden or 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
setting = Setting.new(key: 'token', value: 'encrypted') | |
#<Setting:0x007fa3482bef38> { | |
# :_id => "521febe2e3ad05d100000002", | |
# :key => "token", | |
# :value => { | |
# :value => "NzqeKSoVCjgLj+/t+Uuer1saEAgrglrAZO7cQwh/gdQ=\n", | |
# :iv => "L9PG05eG87MoiDeftyjUMA==\n", | |
# :salt => "Or7x6+b+hD1bQFZyooTqUKaVkBHcQUigQZ+jRsFhiOI=\n" | |
# } | |
#} | |
setting.value == 'encrypted' #=> true | |
setting.value_encrypted == { | |
"value" => "NzqeKSoVCjgLj+/t+Uuer1saEAgrglrAZO7cQwh/gdQ=\n", | |
"iv" => "L9PG05eG87MoiDeftyjUMA==\n", | |
"salt" => "Or7x6+b+hD1bQFZyooTqUKaVkBHcQUigQZ+jRsFhiOI=\n" | |
} #=> true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment