Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for set_attribute_was patch issue and Rails 7 support #434

Merged
merged 7 commits into from
Mar 28, 2023
6 changes: 1 addition & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ env:
- ACTIVERECORD=6.1.7
- ACTIVERECORD=7.0.4
jobs:
fast_finish: true
allow_failures:
- env: ACTIVERECORD=6.0.6
- env: ACTIVERECORD=6.1.7
- env: ACTIVERECORD=7.0.4
fast_finish: false
exclude:
- rvm: 2.6.10
env: ACTIVERECORD=7.0.4
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ It is recommended that you implement a strategy to insure that you do not mix th
attr_encrypted :ssn, key: :encryption_key, v2_gcm_iv: is_decrypting?(:ssn)

def is_decrypting?(attribute)
encrypted_attributes[attribute][:operation] == :decrypting
attr_encrypted_encrypted_attributes[attribute][:operation] == :decrypting
end
end

Expand Down
2 changes: 1 addition & 1 deletion attr_encrypted.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Gem::Specification.new do |s|
s.add_development_dependency('activerecord-jdbcsqlite3-adapter')
s.add_development_dependency('jdbc-sqlite3', '< 3.8.7') # 3.8.7 is nice and broke
else
s.add_development_dependency('sqlite3')
s.add_development_dependency('sqlite3', '= 1.5.4')
end
s.add_development_dependency('dm-sqlite-adapter')
s.add_development_dependency('pry')
Expand Down
56 changes: 28 additions & 28 deletions lib/attr_encrypted.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def self.extended(base) # :nodoc:
base.class_eval do
include InstanceMethods
attr_writer :attr_encrypted_options
@attr_encrypted_options, @encrypted_attributes = {}, {}
@attr_encrypted_options, @attr_encrypted_encrypted_attributes = {}, {}
end
end

Expand Down Expand Up @@ -160,11 +160,11 @@ def attr_encrypted(*attributes)
end

define_method(attribute) do
instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", attr_encrypted_decrypt(attribute, send(encrypted_attribute_name)))
end

define_method("#{attribute}=") do |value|
send("#{encrypted_attribute_name}=", encrypt(attribute, value))
send("#{encrypted_attribute_name}=", attr_encrypted_encrypt(attribute, value))
instance_variable_set("@#{attribute}", value)
end

Expand All @@ -173,7 +173,7 @@ def attr_encrypted(*attributes)
value.respond_to?(:empty?) ? !value.empty? : !!value
end

encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name)
self.attr_encrypted_encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name)
end
end

Expand Down Expand Up @@ -223,7 +223,7 @@ def attr_encrypted_default_options
# User.attr_encrypted?(:name) # false
# User.attr_encrypted?(:email) # true
def attr_encrypted?(attribute)
encrypted_attributes.has_key?(attribute.to_sym)
attr_encrypted_encrypted_attributes.has_key?(attribute.to_sym)
end

# Decrypts a value for the attribute specified
Expand All @@ -234,9 +234,9 @@ def attr_encrypted?(attribute)
# attr_encrypted :email
# end
#
# email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
def decrypt(attribute, encrypted_value, options = {})
options = encrypted_attributes[attribute.to_sym].merge(options)
# email = User.attr_encrypted_decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
def attr_encrypted_decrypt(attribute, encrypted_value, options = {})
options = attr_encrypted_encrypted_attributes[attribute.to_sym].merge(options)
if options[:if] && !options[:unless] && not_empty?(encrypted_value)
encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value))
Expand All @@ -260,9 +260,9 @@ def decrypt(attribute, encrypted_value, options = {})
# attr_encrypted :email
# end
#
# encrypted_email = User.encrypt(:email, 'test@example.com')
def encrypt(attribute, value, options = {})
options = encrypted_attributes[attribute.to_sym].merge(options)
# encrypted_email = User.attr_encrypted_encrypt(:email, 'test@example.com')
def attr_encrypted_encrypt(attribute, value, options = {})
options = attr_encrypted_encrypted_attributes[attribute.to_sym].merge(options)
if options[:if] && !options[:unless] && (options[:allow_empty_value] || not_empty?(value))
value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(value: value))
Expand All @@ -286,9 +286,9 @@ def not_empty?(value)
# attr_encrypted :email, key: 'my secret key'
# end
#
# User.encrypted_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } }
def encrypted_attributes
@encrypted_attributes ||= superclass.encrypted_attributes.dup
# User.attr_encrypted_encrypted_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } }
def attr_encrypted_encrypted_attributes
@attr_encrypted_encrypted_attributes ||= superclass.attr_encrypted_encrypted_attributes.dup
end

# Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
Expand All @@ -303,7 +303,7 @@ def encrypted_attributes
# User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
def method_missing(method, *arguments, &block)
if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3)
send($1, $3, *arguments)
send("attr_encrypted_#{$1}", $3, *arguments)
else
super
end
Expand All @@ -325,10 +325,10 @@ module InstanceMethods
#
# @user = User.new('some-secret-key')
# @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
def decrypt(attribute, encrypted_value)
encrypted_attributes[attribute.to_sym][:operation] = :decrypting
encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value)
self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
def attr_encrypted_decrypt(attribute, encrypted_value)
attr_encrypted_encrypted_attributes[attribute.to_sym][:operation] = :decrypting
attr_encrypted_encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value)
self.class.attr_encrypted_decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
end

# Encrypts a value for the attribute specified using options evaluated in the current object's scope
Expand All @@ -345,20 +345,20 @@ def decrypt(attribute, encrypted_value)
# end
#
# @user = User.new('some-secret-key')
# @user.encrypt(:email, 'test@example.com')
def encrypt(attribute, value)
encrypted_attributes[attribute.to_sym][:operation] = :encrypting
encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(value)
self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
# @user.attr_encrypted_encrypt(:email, 'test@example.com')
def attr_encrypted_encrypt(attribute, value)
attr_encrypted_encrypted_attributes[attribute.to_sym][:operation] = :encrypting
attr_encrypted_encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(value)
self.class.attr_encrypted_encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
end

# Copies the class level hash of encrypted attributes with virtual attribute names as keys
# and their corresponding options as values to the instance
#
def encrypted_attributes
@encrypted_attributes ||= begin
def attr_encrypted_encrypted_attributes
@attr_encrypted_encrypted_attributes ||= begin
duplicated= {}
self.class.encrypted_attributes.map { |key, value| duplicated[key] = value.dup }
self.class.attr_encrypted_encrypted_attributes.map { |key, value| duplicated[key] = value.dup }
duplicated
end
end
Expand All @@ -368,7 +368,7 @@ def encrypted_attributes
# Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
def evaluated_attr_encrypted_options_for(attribute)
evaluated_options = Hash.new
attributes = encrypted_attributes[attribute.to_sym]
attributes = attr_encrypted_encrypted_attributes[attribute.to_sym]
attribute_option_value = attributes[:attribute]

[:if, :unless, :value_present, :allow_empty_value].each do |option|
Expand Down
16 changes: 8 additions & 8 deletions lib/attr_encrypted/adapters/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def self.extended(base) # :nodoc:
alias_method :reload_without_attr_encrypted, :reload
def reload(*args, &block)
result = reload_without_attr_encrypted(*args, &block)
self.class.encrypted_attributes.keys.each do |attribute_name|
self.class.attr_encrypted_encrypted_attributes.keys.each do |attribute_name|
instance_variable_set("@#{attribute_name}", nil)
end
result
Expand All @@ -29,8 +29,8 @@ class << self
def perform_attribute_assignment(method, new_attributes, *args)
return if new_attributes.blank?

send method, new_attributes.reject { |k, _| self.class.encrypted_attributes.key?(k.to_sym) }, *args
send method, new_attributes.reject { |k, _| !self.class.encrypted_attributes.key?(k.to_sym) }, *args
send method, new_attributes.reject { |k, _| self.class.attr_encrypted_encrypted_attributes.key?(k.to_sym) }, *args
send method, new_attributes.reject { |k, _| !self.class.attr_encrypted_encrypted_attributes.key?(k.to_sym) }, *args
end
private :perform_attribute_assignment

Expand All @@ -56,7 +56,7 @@ def attr_encrypted(*attrs)
options = attrs.extract_options!
attr = attrs.pop
attribute attr
options.merge! encrypted_attributes[attr]
options.merge! attr_encrypted_encrypted_attributes[attr]

define_method("#{attr}_was") do
attribute_was(attr)
Expand All @@ -76,10 +76,10 @@ def attr_encrypted(*attrs)
# attributes are handled, @attributes[attr].value is nil which
# breaks attribute_was. Setting it here returns us to the expected
# behavior.
if Gem::Requirement.new('>= 5.2').satisfied_by?(RAILS_VERSION)
if RAILS_VERSION >= Gem::Version.new(5.2)
# This is needed support attribute_was before a record has
# been saved
set_attribute_was(attr, __send__(attr)) if value != __send__(attr)
@attributes.write_cast_value(attr.to_s, __send__(attr)) if value != __send__(attr)
# This is needed to support attribute_was after a record has
# been saved
@attributes.write_from_user(attr.to_s, value) if value != __send__(attr)
Expand Down Expand Up @@ -131,10 +131,10 @@ def method_missing_with_attr_encrypted(method, *args, &block)
if match = /^(find|scoped)_(all_by|by)_([_a-zA-Z]\w*)$/.match(method.to_s)
attribute_names = match.captures.last.split('_and_')
attribute_names.each_with_index do |attribute, index|
if attr_encrypted?(attribute) && encrypted_attributes[attribute.to_sym][:mode] == :single_iv_and_salt
if attr_encrypted?(attribute) && attr_encrypted_encrypted_attributes[attribute.to_sym][:mode] == :single_iv_and_salt
args[index] = send("encrypt_#{attribute}", args[index])
warn "DEPRECATION WARNING: This feature will be removed in the next major release."
attribute_names[index] = encrypted_attributes[attribute.to_sym][:attribute]
attribute_names[index] = attr_encrypted_encrypted_attributes[attribute.to_sym][:attribute]
end
end
method = "#{match.captures[0]}_#{match.captures[1]}_#{attribute_names.join('_and_')}".to_sym
Expand Down
10 changes: 5 additions & 5 deletions test/active_record_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class Account < ActiveRecord::Base
attr_encrypted :password, key: :password_encryption_key

def encrypting?(attr)
encrypted_attributes[attr][:operation] == :encrypting
attr_encrypted_encrypted_attributes[attr][:operation] == :encrypting
end

def password_encryption_key
Expand Down Expand Up @@ -278,14 +278,14 @@ def test_should_allow_proc_based_mode
@person = PersonWithProcMode.create(email: 'test@example.com', credentials: 'password123')

# Email is :per_attribute_iv_and_salt
assert_equal @person.class.encrypted_attributes[:email][:mode].class, Proc
assert_equal @person.class.encrypted_attributes[:email][:mode].call, :per_attribute_iv_and_salt
assert_equal @person.class.attr_encrypted_encrypted_attributes[:email][:mode].class, Proc
assert_equal @person.class.attr_encrypted_encrypted_attributes[:email][:mode].call, :per_attribute_iv_and_salt
refute_nil @person.encrypted_email_salt
refute_nil @person.encrypted_email_iv

# Credentials is :single_iv_and_salt
assert_equal @person.class.encrypted_attributes[:credentials][:mode].class, Proc
assert_equal @person.class.encrypted_attributes[:credentials][:mode].call, :single_iv_and_salt
assert_equal @person.class.attr_encrypted_encrypted_attributes[:credentials][:mode].class, Proc
assert_equal @person.class.attr_encrypted_encrypted_attributes[:credentials][:mode].call, :single_iv_and_salt
assert_nil @person.encrypted_credentials_salt
assert_nil @person.encrypted_credentials_iv
end
Expand Down
24 changes: 12 additions & 12 deletions test/attr_encrypted_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,19 @@ def setup
end

def test_should_store_email_in_encrypted_attributes
assert User.encrypted_attributes.include?(:email)
assert User.attr_encrypted_encrypted_attributes.include?(:email)
end

def test_should_not_store_salt_in_encrypted_attributes
refute User.encrypted_attributes.include?(:salt)
refute User.attr_encrypted_encrypted_attributes.include?(:salt)
end

def test_attr_encrypted_should_return_true_for_email
assert User.attr_encrypted?('email')
end

def test_attr_encrypted_should_not_use_the_same_attribute_name_for_two_attributes_in_the_same_line
refute_equal User.encrypted_attributes[:email][:attribute], User.encrypted_attributes[:without_encoding][:attribute]
refute_equal User.attr_encrypted_encrypted_attributes[:email][:attribute], User.attr_encrypted_encrypted_attributes[:without_encoding][:attribute]
end

def test_attr_encrypted_should_return_false_for_salt
Expand Down Expand Up @@ -154,7 +154,7 @@ def test_should_decrypt_email
def test_should_decrypt_email_when_reading
@user = User.new
assert_nil @user.email
options = @user.encrypted_attributes[:email]
options = @user.attr_encrypted_encrypted_attributes[:email]
iv = @user.send(:generate_iv, options[:algorithm])
encoded_iv = [iv].pack(options[:encode_iv])
salt = SecureRandom.random_bytes
Expand Down Expand Up @@ -223,7 +223,7 @@ def test_should_use_options_found_in_the_attr_encrypted_options_attribute
end

def test_should_inherit_encrypted_attributes
assert_equal [User.encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, Admin.encrypted_attributes.keys.collect { |key| key.to_s }.sort
assert_equal [User.attr_encrypted_encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, Admin.attr_encrypted_encrypted_attributes.keys.collect { |key| key.to_s }.sort
end

def test_should_inherit_attr_encrypted_options
Expand All @@ -233,7 +233,7 @@ def test_should_inherit_attr_encrypted_options

def test_should_not_inherit_unrelated_attributes
assert SomeOtherClass.attr_encrypted_options.empty?
assert SomeOtherClass.encrypted_attributes.empty?
assert SomeOtherClass.attr_encrypted_encrypted_attributes.empty?
end

def test_should_evaluate_a_symbol_option
Expand Down Expand Up @@ -304,7 +304,7 @@ def test_should_encrypt_empty_with_truthy_allow_empty_value_option
end

def test_should_work_with_aliased_attr_encryptor
assert User.encrypted_attributes.include?(:aliased)
assert User.attr_encrypted_encrypted_attributes.include?(:aliased)
end

def test_should_always_reset_options
Expand Down Expand Up @@ -381,12 +381,12 @@ def test_should_decrypt_second_record
@user2 = User.new
@user2.email = 'test@example.com'

assert_equal 'test@example.com', @user1.decrypt(:email, @user1.encrypted_email)
assert_equal 'test@example.com', @user1.attr_encrypted_decrypt(:email, @user1.encrypted_email)
end

def test_should_specify_the_default_algorithm
assert YetAnotherClass.encrypted_attributes[:email][:algorithm]
assert_equal YetAnotherClass.encrypted_attributes[:email][:algorithm], 'aes-256-gcm'
assert YetAnotherClass.attr_encrypted_encrypted_attributes[:email][:algorithm]
assert_equal YetAnotherClass.attr_encrypted_encrypted_attributes[:email][:algorithm], 'aes-256-gcm'
end

def test_should_not_encode_iv_when_encode_iv_is_false
Expand Down Expand Up @@ -475,8 +475,8 @@ def test_encrypted_attributes_state_is_not_shared

another_user = User.new

assert_equal :encrypting, user.encrypted_attributes[:ssn][:operation]
assert_nil another_user.encrypted_attributes[:ssn][:operation]
assert_equal :encrypting, user.attr_encrypted_encrypted_attributes[:ssn][:operation]
assert_nil another_user.attr_encrypted_encrypted_attributes[:ssn][:operation]
end

def test_should_not_by_default_generate_key_when_attribute_is_empty
Expand Down
12 changes: 6 additions & 6 deletions test/legacy_attr_encrypted_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,19 @@ def self.call(object)
class LegacyAttrEncryptedTest < Minitest::Test

def test_should_store_email_in_encrypted_attributes
assert LegacyUser.encrypted_attributes.include?(:email)
assert LegacyUser.attr_encrypted_encrypted_attributes.include?(:email)
end

def test_should_not_store_salt_in_encrypted_attributes
assert !LegacyUser.encrypted_attributes.include?(:salt)
assert !LegacyUser.attr_encrypted_encrypted_attributes.include?(:salt)
end

def test_attr_encrypted_should_return_true_for_email
assert LegacyUser.attr_encrypted?('email')
end

def test_attr_encrypted_should_not_use_the_same_attribute_name_for_two_attributes_in_the_same_line
refute_equal LegacyUser.encrypted_attributes[:email][:attribute], LegacyUser.encrypted_attributes[:without_encoding][:attribute]
refute_equal LegacyUser.attr_encrypted_encrypted_attributes[:email][:attribute], LegacyUser.attr_encrypted_encrypted_attributes[:without_encoding][:attribute]
end

def test_attr_encrypted_should_return_false_for_salt
Expand Down Expand Up @@ -201,7 +201,7 @@ def test_should_use_options_found_in_the_attr_encrypted_options_attribute
end

def test_should_inherit_encrypted_attributes
assert_equal [LegacyUser.encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, LegacyAdmin.encrypted_attributes.keys.collect { |key| key.to_s }.sort
assert_equal [LegacyUser.attr_encrypted_encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, LegacyAdmin.attr_encrypted_encrypted_attributes.keys.collect { |key| key.to_s }.sort
end

def test_should_inherit_attr_encrypted_options
Expand All @@ -211,7 +211,7 @@ def test_should_inherit_attr_encrypted_options

def test_should_not_inherit_unrelated_attributes
assert LegacySomeOtherClass.attr_encrypted_options.empty?
assert LegacySomeOtherClass.encrypted_attributes.empty?
assert LegacySomeOtherClass.attr_encrypted_encrypted_attributes.empty?
end

def test_should_evaluate_a_symbol_option
Expand Down Expand Up @@ -268,7 +268,7 @@ def test_should_not_encrypt_with_true_unless
end

def test_should_work_with_aliased_attr_encryptor
assert LegacyUser.encrypted_attributes.include?(:aliased)
assert LegacyUser.attr_encrypted_encrypted_attributes.include?(:aliased)
end

def test_should_always_reset_options
Expand Down