lib/lockbox/model.rb in lockbox-0.2.5 vs lib/lockbox/model.rb in lockbox-0.3.0
- old
+ new
@@ -1,6 +1,6 @@
-class Lockbox
+module Lockbox
module Model
def attached_encrypted(attribute, **options)
warn "[lockbox] DEPRECATION WARNING: Use encrypts_attached instead"
encrypts_attached(attribute, **options)
end
@@ -10,11 +10,11 @@
name = name.to_sym
class_eval do
@lockbox_attachments ||= {}
- unless respond_to?(:lockbox_attachments)
+ if @lockbox_attachments.empty?
def self.lockbox_attachments
parent_attachments =
if superclass.respond_to?(:lockbox_attachments)
superclass.lockbox_attachments
else
@@ -29,11 +29,11 @@
@lockbox_attachments[name] = options
end
end
end
- def encrypts(*attributes, encode: true, **options)
+ def encrypts(*attributes, **options)
# support objects
# case options[:type]
# when Date
# options[:type] = :date
# when Time
@@ -48,21 +48,15 @@
# options[:type] = :integer
# when Float
# options[:type] = :float
# end
- raise ArgumentError, "Unknown type: #{options[:type]}" unless [nil, :string, :boolean, :date, :datetime, :time, :integer, :float, :binary, :json, :hash].include?(options[:type])
+ custom_type = options[:type].respond_to?(:serialize) && options[:type].respond_to?(:deserialize)
+ raise ArgumentError, "Unknown type: #{options[:type]}" unless custom_type || [nil, :string, :boolean, :date, :datetime, :time, :integer, :float, :binary, :json, :hash].include?(options[:type])
- attribute_type =
- case options[:type]
- when nil, :json, :hash
- :string
- when :integer
- ActiveModel::Type::Integer.new(limit: 8)
- else
- options[:type]
- end
+ activerecord = defined?(ActiveRecord::Base) && self < ActiveRecord::Base
+ raise ArgumentError, "Type not supported yet with Mongoid" if options[:type] && !activerecord
attributes.each do |name|
# add default options
encrypted_attribute = "#{name}_ciphertext"
@@ -74,38 +68,30 @@
name = name.to_sym
options[:attribute] = name.to_s
options[:encrypted_attribute] = encrypted_attribute
- class_method_name = "generate_#{encrypted_attribute}"
+ options[:encode] = true unless options.key?(:encode)
- class_eval do
- if options[:migrating]
- before_validation do
- send("#{name}=", send(original_name)) if send("#{original_name}_changed?")
- end
- end
+ encrypt_method_name = "generate_#{encrypted_attribute}"
+ decrypt_method_name = "decrypt_#{encrypted_attribute}"
+ class_eval do
@lockbox_attributes ||= {}
- unless respond_to?(:lockbox_attributes)
+ if @lockbox_attributes.empty?
def self.lockbox_attributes
parent_attributes =
if superclass.respond_to?(:lockbox_attributes)
superclass.lockbox_attributes
else
{}
end
parent_attributes.merge(@lockbox_attributes || {})
end
- end
- raise "Duplicate encrypted attribute: #{original_name}" if lockbox_attributes[original_name]
- @lockbox_attributes[original_name] = options.merge(encode: encode)
-
- if @lockbox_attributes.size == 1
# use same approach as activerecord serialization
def serializable_hash(options = nil)
options = options.try(:dup) || {}
options[:except] = Array(options[:except])
@@ -121,57 +107,83 @@
"#{k}: #{respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : v.inspect}"
end
"#<#{self.class} #{inspection.join(", ")}>"
end
- # needed for in-place modifications
- # assigned attributes are encrypted on assignment
- # and then again here
- before_save do
- self.class.lockbox_attributes.each do |_, lockbox_attribute|
- attribute = lockbox_attribute[:attribute]
+ if activerecord
+ # needed for in-place modifications
+ # assigned attributes are encrypted on assignment
+ # and then again here
+ before_save do
+ self.class.lockbox_attributes.each do |_, lockbox_attribute|
+ attribute = lockbox_attribute[:attribute]
- if changes.include?(attribute)
- type = (self.class.try(:attribute_types) || {})[attribute]
- if type && type.is_a?(ActiveRecord::Type::Serialized)
+ if attribute_changed_in_place?(attribute)
send("#{attribute}=", send(attribute))
end
end
end
- end
-
- if defined?(Mongoid::Document) && included_modules.include?(Mongoid::Document)
+ else
def reload
self.class.lockbox_attributes.each do |_, v|
instance_variable_set("@#{v[:attribute]}", nil)
end
super
end
end
end
- serialize name, JSON if options[:type] == :json
- serialize name, Hash if options[:type] == :hash
+ raise "Duplicate encrypted attribute: #{original_name}" if lockbox_attributes[original_name]
+ @lockbox_attributes[original_name] = options
- if respond_to?(:attribute)
- attribute name, attribute_type
+ if activerecord
+ # preference:
+ # 1. type option
+ # 2. existing virtual attribute
+ # 3. default to string (which can later be overridden)
+ if options[:type]
+ attribute_type =
+ case options[:type]
+ when :json, :hash
+ :string
+ when :integer
+ ActiveModel::Type::Integer.new(limit: 8)
+ else
+ options[:type]
+ end
- define_method("#{name}?") do
- send("#{encrypted_attribute}?")
+ attribute name, attribute_type
+
+ serialize name, JSON if options[:type] == :json
+ serialize name, Hash if options[:type] == :hash
+ elsif !attributes_to_define_after_schema_loads.key?(name.to_s)
+ attribute name, :string
end
+
+ define_method("#{name}_was") do
+ send(name) # writes attribute when not already set
+ super()
+ end
+
+ # restore ciphertext as well
+ define_method("restore_#{name}!") do
+ super()
+ send("restore_#{encrypted_attribute}!")
+ end
+
+ if ActiveRecord::VERSION::STRING >= "5.1"
+ define_method("#{name}_in_database") do
+ send(name) # writes attribute when not already set
+ super()
+ end
+ end
else
+ # keep this module dead simple
+ # Mongoid uses changed_attributes to calculate keys to update
+ # so we shouldn't mess with it
m = Module.new do
define_method("#{name}=") do |val|
- prev_val = instance_variable_get("@#{name}")
-
- unless val == prev_val
- # custom attribute_will_change! method
- unless changed_attributes.key?(name.to_s)
- changed_attributes[name.to_s] = prev_val.__deep_copy__
- end
- end
-
instance_variable_set("@#{name}", val)
end
define_method(name) do
instance_variable_get("@#{name}")
@@ -181,14 +193,36 @@
include m
alias_method "#{name}_changed?", "#{encrypted_attribute}_changed?"
define_method "#{name}_was" do
- attribute_was(name.to_s)
+ ciphertext = send("#{encrypted_attribute}_was")
+ self.class.send(decrypt_method_name, ciphertext, context: self)
end
+
+ define_method "#{name}_change" do
+ ciphertexts = send("#{encrypted_attribute}_change")
+ ciphertexts.map { |v| self.class.send(decrypt_method_name, v, context: self) } if ciphertexts
+ end
+
+ define_method "reset_#{name}!" do
+ instance_variable_set("@#{name}", nil)
+ send("reset_#{encrypted_attribute}!")
+ send(name)
+ end
+
+ define_method "reset_#{name}_to_default!" do
+ instance_variable_set("@#{name}", nil)
+ send("reset_#{encrypted_attribute}_to_default!")
+ send(name)
+ end
end
+ define_method("#{name}?") do
+ send("#{encrypted_attribute}?")
+ end
+
define_method("#{name}=") do |message|
original_message = message
# decrypt first for dirty tracking
# don't raise error if can't decrypt previous
@@ -197,79 +231,46 @@
rescue Lockbox::DecryptionError
nil
end
# set ciphertext
- ciphertext = self.class.send(class_method_name, message, context: self)
+ ciphertext = self.class.send(encrypt_method_name, message, context: self)
send("#{encrypted_attribute}=", ciphertext)
super(original_message)
end
define_method(name) do
message = super()
unless message
ciphertext = send(encrypted_attribute)
- message =
- if ciphertext.nil? || (ciphertext == "" && !options[:padding])
- ciphertext
- else
- ciphertext = Base64.decode64(ciphertext) if encode
- table = self.class.respond_to?(:table_name) ? self.class.table_name : self.class.collection_name.to_s
- Lockbox::Utils.build_box(self, options, table, encrypted_attribute).decrypt(ciphertext)
+ message = self.class.send(decrypt_method_name, ciphertext, context: self)
+
+ if activerecord
+ # set previous attribute on first decrypt
+ if @attributes[name.to_s]
+ @attributes[name.to_s].instance_variable_set("@value_before_type_cast", message)
end
- unless message.nil?
- case options[:type]
- when :boolean
- message = message == "t"
- when :date
- message = ActiveRecord::Type::Date.new.deserialize(message)
- when :datetime
- message = ActiveRecord::Type::DateTime.new.deserialize(message)
- when :time
- message = ActiveRecord::Type::Time.new.deserialize(message)
- when :integer
- message = ActiveRecord::Type::Integer.new(limit: 8).deserialize(message.unpack("q>").first)
- when :float
- message = ActiveRecord::Type::Float.new.deserialize(message.unpack("G").first)
- when :string
- message.force_encoding(Encoding::UTF_8)
- when :binary
- # do nothing
- # decrypt returns binary string
+ # cache
+ if respond_to?(:_write_attribute, true)
+ _write_attribute(name, message) if !@attributes.frozen?
else
- type = (self.class.try(:attribute_types) || {})[name.to_s]
- if type && type.is_a?(ActiveRecord::Type::Serialized)
- message = type.deserialize(message)
- else
- # default to string if not serialized
- message.force_encoding(Encoding::UTF_8)
- end
+ raw_write_attribute(name, message) if !@attributes.frozen?
end
- end
-
- # set previous attribute on first decrypt
- @attributes[name.to_s].instance_variable_set("@value_before_type_cast", message) if @attributes[name.to_s]
-
- # cache
- if respond_to?(:_write_attribute, true)
- _write_attribute(name, message)
- elsif respond_to?(:raw_write_attribute)
- raw_write_attribute(name, message)
else
instance_variable_set("@#{name}", message)
end
end
message
end
# for fixtures
- define_singleton_method class_method_name do |message, **opts|
- table = respond_to?(:table_name) ? table_name : collection_name.to_s
+ define_singleton_method encrypt_method_name do |message, **opts|
+ table = activerecord ? table_name : collection_name.to_s
unless message.nil?
case options[:type]
when :boolean
message = ActiveRecord::Type::Boolean.new.serialize(message)
@@ -300,21 +301,64 @@
when :string, :binary
# do nothing
# encrypt will convert to binary
else
type = (try(:attribute_types) || {})[name.to_s]
- if type && type.is_a?(ActiveRecord::Type::Serialized)
- message = type.serialize(message)
- end
+ message = type.serialize(message) if type
end
end
if message.nil? || (message == "" && !options[:padding])
message
else
ciphertext = Lockbox::Utils.build_box(opts[:context], options, table, encrypted_attribute).encrypt(message)
- ciphertext = Base64.strict_encode64(ciphertext) if encode
+ ciphertext = Base64.strict_encode64(ciphertext) if options[:encode]
ciphertext
+ end
+ end
+
+ define_singleton_method decrypt_method_name do |ciphertext, **opts|
+ message =
+ if ciphertext.nil? || (ciphertext == "" && !options[:padding])
+ ciphertext
+ else
+ ciphertext = Base64.decode64(ciphertext) if options[:encode]
+ table = activerecord ? table_name : collection_name.to_s
+ Lockbox::Utils.build_box(opts[:context], options, table, encrypted_attribute).decrypt(ciphertext)
+ end
+
+ unless message.nil?
+ case options[:type]
+ when :boolean
+ message = message == "t"
+ when :date
+ message = ActiveRecord::Type::Date.new.deserialize(message)
+ when :datetime
+ message = ActiveRecord::Type::DateTime.new.deserialize(message)
+ when :time
+ message = ActiveRecord::Type::Time.new.deserialize(message)
+ when :integer
+ message = ActiveRecord::Type::Integer.new(limit: 8).deserialize(message.unpack("q>").first)
+ when :float
+ message = ActiveRecord::Type::Float.new.deserialize(message.unpack("G").first)
+ when :string
+ message.force_encoding(Encoding::UTF_8)
+ when :binary
+ # do nothing
+ # decrypt returns binary string
+ else
+ type = (try(:attribute_types) || {})[name.to_s]
+ message = type.deserialize(message) if type
+ message.force_encoding(Encoding::UTF_8) if !type || type.is_a?(ActiveModel::Type::String)
+ end
+ end
+
+ message
+ end
+
+ if options[:migrating]
+ before_validation do
+ send("#{name}=", send(original_name)) if send("#{original_name}_changed?")
end
end
end
end
end