Skip to content

Instantly share code, notes, and snippets.

@tstachl
Last active December 21, 2015 23:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tstachl/6385210 to your computer and use it in GitHub Desktop.
Save tstachl/6385210 to your computer and use it in GitHub Desktop.
Monkey patch field encryption into Mongoid.
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)
class Setting
include Mongoid::Document
field :key, type: String
field :value, type: String, encrypt: true
end
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