From fe1d39132a83878fe65f076f3d351d6c0f463d18 Mon Sep 17 00:00:00 2001 From: "twan.maus" Date: Fri, 9 Mar 2018 14:20:25 +0100 Subject: [PATCH 01/17] Optional update encrypted attributes only when values changed --- README.md | 13 ++++++++++++- lib/attr_encrypted.rb | 20 +++++++++++++++++-- test/attr_encrypted_test.rb | 39 +++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 74853f1d..36a79e1a 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,8 @@ The following are the default options used by `attr_encrypted`: decrypt_method: 'decrypt', mode: :per_attribute_iv, algorithm: 'aes-256-gcm', - allow_empty_value: false + allow_empty_value: false, + update_unchanged: true ``` All of the aforementioned options are explained in depth below. @@ -322,6 +323,16 @@ You may want to encrypt empty strings or nil so as to not reveal which records a end ``` +### The `:update_unchanged` option + +You may want to only update changed attributes each time the record is saved. + +```ruby + class User + attr_encrypted :email, key: 'some secret key', marshal: true, update_unchanged: false + end +``` + ## ORMs diff --git a/lib/attr_encrypted.rb b/lib/attr_encrypted.rb index 3895b721..ef9b2d51 100644 --- a/lib/attr_encrypted.rb +++ b/lib/attr_encrypted.rb @@ -104,6 +104,9 @@ def self.extended(base) # :nodoc: # allow_empty_value: Attributes which have nil or empty string values will not be encrypted unless this option # has a truthy value. # + # update_unchanged: Attributes which have unchanged values will be encrypted again on each update. + # Defaults to true. + # # You can specify your own default options # # class User @@ -162,8 +165,10 @@ def attr_encrypted(*attributes) end define_method("#{attribute}=") do |value| - send("#{encrypted_attribute_name}=", encrypt(attribute, value)) - instance_variable_set("@#{attribute}", value) + if should_update_encrypted_attribute?(attribute, value) + send("#{encrypted_attribute_name}=", encrypt(attribute, value)) + instance_variable_set("@#{attribute}", value) + end end define_method("#{attribute}?") do @@ -204,6 +209,7 @@ def attr_encrypted_default_options mode: :per_attribute_iv, algorithm: 'aes-256-gcm', allow_empty_value: false, + update_unchanged: true } end @@ -359,6 +365,16 @@ def encrypted_attributes protected + # Determine if unchanged attribute needs to be updated again + def should_update_encrypted_attribute?(attribute, value) + if encrypted_attributes[attribute.to_sym][:update_unchanged] + return true + else + old_value = instance_variable_get("@#{attribute}") + return old_value.nil? || old_value != value + end + end + # 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 diff --git a/test/attr_encrypted_test.rb b/test/attr_encrypted_test.rb index 3fc131b1..6c65a674 100644 --- a/test/attr_encrypted_test.rb +++ b/test/attr_encrypted_test.rb @@ -466,4 +466,43 @@ def test_should_not_by_default_generate_iv_when_attribute_is_empty user.with_true_if = nil assert_nil user.encrypted_with_true_if_iv end + + def test_should_not_generate_iv_if_same_value_when_option_is_false + user = User.new + User.encrypted_attributes[:email][:update_unchanged] = false + assert_nil user.encrypted_email_iv + user.email = 'thing@thing.com' + refute_nil(old_value = user.encrypted_email_iv) + user.email = 'thing@thing.com' + assert_equal old_value, user.encrypted_email_iv + end + + def test_should_generate_iv_if_same_value_when_option_is_true + user = User.new + User.encrypted_attributes[:email][:update_unchanged] = true + assert_nil user.encrypted_email_iv + user.email = 'thing@thing.com' + refute_nil(old_value = user.encrypted_email_iv) + user.email = 'thing@thing.com' + refute_equal old_value, user.encrypted_email_iv + end + + def test_should_not_update_iv_if_same_value_when_option_is_false + user = User.new(email: 'thing@thing.com') + User.encrypted_attributes[:email][:update_unchanged] = false + old_encrypted_email_iv = user.encrypted_email_iv + refute_nil old_encrypted_email_iv + user.email = 'thing@thing.com' + assert_equal old_encrypted_email_iv, user.encrypted_email_iv + end + + def test_should_not_update_iv_if_same_value_when_option_is_true + user = User.new(email: 'thing@thing.com') + User.encrypted_attributes[:email][:update_unchanged] = true + old_encrypted_email_iv = user.encrypted_email_iv + refute_nil old_encrypted_email_iv + user.email = 'thing@thing.com' + refute_nil user.encrypted_email_iv + refute_equal old_encrypted_email_iv, user.encrypted_email_iv + end end From 9fc8962899f77d2307939630adefb1548742079f Mon Sep 17 00:00:00 2001 From: Brian Freese Date: Thu, 19 Jul 2018 09:44:51 -0500 Subject: [PATCH 02/17] Fixes defect in which encrypted_attributes state was shared across all instances of a given class due to shallow dup. This caused random OpenSSL::Cipher::CipherError errors, particularly in cases in which concurrent encrypts/decrypts were occurring. --- lib/attr_encrypted.rb | 6 +++++- test/attr_encrypted_test.rb | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/attr_encrypted.rb b/lib/attr_encrypted.rb index ef9b2d51..15d2e076 100644 --- a/lib/attr_encrypted.rb +++ b/lib/attr_encrypted.rb @@ -360,7 +360,11 @@ def encrypt(attribute, value) # and their corresponding options as values to the instance # def encrypted_attributes - @encrypted_attributes ||= self.class.encrypted_attributes.dup + @encrypted_attributes ||= begin + duplicated= {} + self.class.encrypted_attributes.map { |key, value| duplicated[key] = value.dup } + duplicated + end end protected diff --git a/test/attr_encrypted_test.rb b/test/attr_encrypted_test.rb index 6c65a674..f66fed37 100644 --- a/test/attr_encrypted_test.rb +++ b/test/attr_encrypted_test.rb @@ -505,4 +505,14 @@ def test_should_not_update_iv_if_same_value_when_option_is_true refute_nil user.encrypted_email_iv refute_equal old_encrypted_email_iv, user.encrypted_email_iv end + + def test_encrypted_attributes_state_is_not_shared + user = User.new + user.ssn = '123456789' + + another_user = User.new + + assert_equal :encrypting, user.encrypted_attributes[:ssn][:operation] + assert_nil another_user.encrypted_attributes[:ssn][:operation] + end end From d01839e160ca4d66efe37b7f1915966e4ddde1b3 Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Mon, 10 Sep 2018 16:55:42 +0200 Subject: [PATCH 03/17] Fix tests after cherry-pick. --- test/attr_encrypted_test.rb | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/test/attr_encrypted_test.rb b/test/attr_encrypted_test.rb index f66fed37..ac5d959e 100644 --- a/test/attr_encrypted_test.rb +++ b/test/attr_encrypted_test.rb @@ -29,6 +29,7 @@ class User attr_encrypted :with_false_unless, :key => SECRET_KEY, :unless => false, mode: :per_attribute_iv_and_salt attr_encrypted :with_if_changed, :key => SECRET_KEY, :if => :should_encrypt attr_encrypted :with_allow_empty_value, key: SECRET_KEY, allow_empty_value: true, marshal: true + attr_encrypted :with_unchanged_false, key: SECRET_KEY, update_unchanged: false attr_encryptor :aliased, :key => SECRET_KEY @@ -469,17 +470,16 @@ def test_should_not_by_default_generate_iv_when_attribute_is_empty def test_should_not_generate_iv_if_same_value_when_option_is_false user = User.new - User.encrypted_attributes[:email][:update_unchanged] = false - assert_nil user.encrypted_email_iv - user.email = 'thing@thing.com' - refute_nil(old_value = user.encrypted_email_iv) - user.email = 'thing@thing.com' - assert_equal old_value, user.encrypted_email_iv + assert_nil user.encrypted_with_unchanged_false_iv + user.with_unchanged_false = 'thing@thing.com' + old_value = user.encrypted_with_unchanged_false_iv + refute_nil(old_value) + user.with_unchanged_false = 'thing@thing.com' + assert_equal old_value, user.encrypted_with_unchanged_false_iv end def test_should_generate_iv_if_same_value_when_option_is_true user = User.new - User.encrypted_attributes[:email][:update_unchanged] = true assert_nil user.encrypted_email_iv user.email = 'thing@thing.com' refute_nil(old_value = user.encrypted_email_iv) @@ -488,17 +488,16 @@ def test_should_generate_iv_if_same_value_when_option_is_true end def test_should_not_update_iv_if_same_value_when_option_is_false - user = User.new(email: 'thing@thing.com') - User.encrypted_attributes[:email][:update_unchanged] = false - old_encrypted_email_iv = user.encrypted_email_iv - refute_nil old_encrypted_email_iv - user.email = 'thing@thing.com' - assert_equal old_encrypted_email_iv, user.encrypted_email_iv + user = User.new + user.with_unchanged_false = 'thing@thing.com' + old_encrypted_with_unchanged_false_iv = user.encrypted_with_unchanged_false_iv + refute_nil old_encrypted_with_unchanged_false_iv + user.with_unchanged_false = 'thing@thing.com' + assert_equal old_encrypted_with_unchanged_false_iv, user.encrypted_with_unchanged_false_iv end def test_should_not_update_iv_if_same_value_when_option_is_true user = User.new(email: 'thing@thing.com') - User.encrypted_attributes[:email][:update_unchanged] = true old_encrypted_email_iv = user.encrypted_email_iv refute_nil old_encrypted_email_iv user.email = 'thing@thing.com' From 08bc4c39b57c42e85892868518d5d0a105fac485 Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Thu, 3 Jan 2019 15:34:17 +0100 Subject: [PATCH 04/17] Remove post install message. --- attr_encrypted.gemspec | 6 ------ 1 file changed, 6 deletions(-) diff --git a/attr_encrypted.gemspec b/attr_encrypted.gemspec index 41f96ed9..bb40df9f 100644 --- a/attr_encrypted.gemspec +++ b/attr_encrypted.gemspec @@ -59,10 +59,4 @@ Gem::Specification.new do |s| s.cert_chain = ['certs/saghaulor.pem'] s.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/ - - s.post_install_message = "\n\n\nWARNING: Several insecure default options and features were deprecated in attr_encrypted v2.0.0.\n -Additionally, there was a bug in Encryptor v2.0.0 that insecurely encrypted data when using an AES-*-GCM algorithm.\n -This bug was fixed but introduced breaking changes between v2.x and v3.x.\n -Please see the README for more information regarding upgrading to attr_encrypted v3.0.0.\n\n\n" - end From a015e07a85e428500bff91e516cfe2137b1aa5b3 Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Mon, 18 Feb 2019 09:56:08 +0100 Subject: [PATCH 05/17] Fix deprecatio warning: remove #has_rdoc. --- attr_encrypted.gemspec | 1 - 1 file changed, 1 deletion(-) diff --git a/attr_encrypted.gemspec b/attr_encrypted.gemspec index bb40df9f..a7c9b4de 100644 --- a/attr_encrypted.gemspec +++ b/attr_encrypted.gemspec @@ -19,7 +19,6 @@ Gem::Specification.new do |s| s.homepage = 'http://github.com/attr-encrypted/attr_encrypted' s.license = 'MIT' - s.has_rdoc = false s.rdoc_options = ['--line-numbers', '--inline-source', '--main', 'README.rdoc'] s.require_paths = ['lib'] From 4da5851a3c30b4e2fc3af3339e7c5ca47c86f882 Mon Sep 17 00:00:00 2001 From: Sean Abrahams Date: Thu, 7 Feb 2019 13:34:15 -0800 Subject: [PATCH 06/17] Support ActiveRecord 5.2 --- .travis.yml | 7 +++++ attr_encrypted.gemspec | 3 ++- lib/attr_encrypted/adapters/active_record.rb | 16 +++++++++++- test/active_record_test.rb | 27 +++++++++++++++++++- test/test_helper.rb | 3 ++- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f75b2ede..7b56a02b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,16 +18,21 @@ env: - ACTIVERECORD=4.2.0 - ACTIVERECORD=5.0.0 - ACTIVERECORD=5.1.1 + - ACTIVERECORD=5.2.0 matrix: exclude: - rvm: 2.0 env: ACTIVERECORD=5.0.0 - rvm: 2.0 env: ACTIVERECORD=5.1.1 + - rvm: 2.0 + env: ACTIVERECORD=5.2.0 - rvm: 2.1 env: ACTIVERECORD=5.0.0 - rvm: 2.1 env: ACTIVERECORD=5.1.1 + - rvm: 2.1 + env: ACTIVERECORD=5.2.0 - rvm: 2.4.0 env: ACTIVERECORD=3.0.0 - rvm: 2.4.0 @@ -52,6 +57,8 @@ matrix: env: ACTIVERECORD=5.0.0 - rvm: rbx env: ACTIVERECORD=5.1.1 + - rvm: rbx + env: ACTIVERECORD=5.2.0 allow_failures: - rvm: rbx fast_finish: true diff --git a/attr_encrypted.gemspec b/attr_encrypted.gemspec index a7c9b4de..74ac4708 100644 --- a/attr_encrypted.gemspec +++ b/attr_encrypted.gemspec @@ -41,6 +41,7 @@ Gem::Specification.new do |s| s.add_development_dependency('rake') s.add_development_dependency('minitest') s.add_development_dependency('sequel') + s.add_development_dependency('pry-byebug') if RUBY_VERSION < '2.1.0' s.add_development_dependency('nokogiri', '< 1.7.0') s.add_development_dependency('public_suffix', '< 3.0.0') @@ -49,7 +50,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.3.0', '>= 1.3.6') end s.add_development_dependency('dm-sqlite-adapter') s.add_development_dependency('simplecov') diff --git a/lib/attr_encrypted/adapters/active_record.rb b/lib/attr_encrypted/adapters/active_record.rb index a22108e4..5e4e3deb 100644 --- a/lib/attr_encrypted/adapters/active_record.rb +++ b/lib/attr_encrypted/adapters/active_record.rb @@ -64,7 +64,7 @@ def attr_encrypted(*attrs) end else define_method("#{attr}_changed?") do - attribute_changed?(attr) + attribute_changed?(attr) end end @@ -73,6 +73,20 @@ def attr_encrypted(*attrs) end define_method("#{attr}_with_dirtiness=") do |value| + ## + # In ActiveRecord 5.2+, due to changes to the way virtual + # attributes are handled, @attributes[attr].value is nil which + # breaks attribute_was. Setting it here returns us to the expected + # behavior. + if ::ActiveRecord::VERSION::STRING >= "5.2" + # This is needed support attribute_was before a record has + # been saved + set_attribute_was(attr, __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) + end + ## attribute_will_change!(attr) if value != __send__(attr) __send__("#{attr}_without_dirtiness=", value) end diff --git a/test/active_record_test.rb b/test/active_record_test.rb index 89134548..543e04d7 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -124,7 +124,6 @@ class Address < ActiveRecord::Base end class ActiveRecordTest < Minitest::Test - def setup drop_all_tables create_tables @@ -219,6 +218,32 @@ def test_attribute_was_works_when_options_for_old_encrypted_value_are_different_ assert_equal pw.reverse, account.password end + # ActiveRecord 5.2 specific methods + if ::ActiveRecord::VERSION::STRING >= "5.2" + def test_should_create_will_save_change_to_predicate + person = Person.create!(email: 'test@example.com') + refute person.will_save_change_to_email? + person.email = 'test@example.com' + refute person.will_save_change_to_email? + person.email = 'test2@example.com' + assert person.will_save_change_to_email? + end + + def test_should_create_saved_change_to_predicate + person = Person.create!(email: 'test@example.com') + assert person.saved_change_to_email? + person.reload + person.email = 'test@example.com' + refute person.saved_change_to_email? + person.email = nil + refute person.saved_change_to_email? + person.email = 'test2@example.com' + refute person.saved_change_to_email? + person.save + assert person.saved_change_to_email? + end + end + if ::ActiveRecord::VERSION::STRING > "4.0" def test_should_assign_attributes @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) diff --git a/test/test_helper.rb b/test/test_helper.rb index ad6ef3bd..6202e2e4 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,6 +1,7 @@ require 'simplecov' require 'simplecov-rcov' -require "codeclimate-test-reporter" +require 'codeclimate-test-reporter' +require 'pry-byebug' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( [ From 3c2aa0100db889f2c94f1324bbcb50746563a572 Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Tue, 15 Jun 2021 14:58:29 +0200 Subject: [PATCH 07/17] Use GitHub Actions for CI. --- .github/workflows/test.yml | 22 +++++++++++++ .travis.yml | 67 -------------------------------------- 2 files changed, 22 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..b42bced6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby-version: ['2.3', '2.4', '2.5'] + rails-version: ['4.2.0', '5.0.0', '5.1.1', '5.2.0'] + env: + ACTIVERECORD: ${{ matrix.rails-version }} + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + - name: Run tests + run: bundle exec rake diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7b56a02b..00000000 --- a/.travis.yml +++ /dev/null @@ -1,67 +0,0 @@ -sudo: false -language: ruby -cache: bundler -rvm: - - 2.0 - - 2.1 - - 2.2.2 - - 2.3.0 - - 2.4.0 - - 2.5.0 - - rbx -env: - - ACTIVERECORD=3.0.0 - - ACTIVERECORD=3.1.0 - - ACTIVERECORD=3.2.0 - - ACTIVERECORD=4.0.0 - - ACTIVERECORD=4.1.0 - - ACTIVERECORD=4.2.0 - - ACTIVERECORD=5.0.0 - - ACTIVERECORD=5.1.1 - - ACTIVERECORD=5.2.0 -matrix: - exclude: - - rvm: 2.0 - env: ACTIVERECORD=5.0.0 - - rvm: 2.0 - env: ACTIVERECORD=5.1.1 - - rvm: 2.0 - env: ACTIVERECORD=5.2.0 - - rvm: 2.1 - env: ACTIVERECORD=5.0.0 - - rvm: 2.1 - env: ACTIVERECORD=5.1.1 - - rvm: 2.1 - env: ACTIVERECORD=5.2.0 - - rvm: 2.4.0 - env: ACTIVERECORD=3.0.0 - - rvm: 2.4.0 - env: ACTIVERECORD=3.1.0 - - rvm: 2.4.0 - env: ACTIVERECORD=3.2.0 - - rvm: 2.4.0 - env: ACTIVERECORD=4.0.0 - - rvm: 2.4.0 - env: ACTIVERECORD=4.1.0 - - rvm: 2.5.0 - env: ACTIVERECORD=3.0.0 - - rvm: 2.5.0 - env: ACTIVERECORD=3.1.0 - - rvm: 2.5.0 - env: ACTIVERECORD=3.2.0 - - rvm: 2.5.0 - env: ACTIVERECORD=4.0.0 - - rvm: 2.5.0 - env: ACTIVERECORD=4.1.0 - - rvm: rbx - env: ACTIVERECORD=5.0.0 - - rvm: rbx - env: ACTIVERECORD=5.1.1 - - rvm: rbx - env: ACTIVERECORD=5.2.0 - allow_failures: - - rvm: rbx - fast_finish: true -addons: - code_climate: - repo_token: a90435ed4954dd6e9f3697a20c5bc3754f67d94703f870e8fc7b00f69f5b2d06 From 67bbfa86310045a67a4b65032ede8b329cb06ff2 Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Tue, 15 Jun 2021 15:18:35 +0200 Subject: [PATCH 08/17] Update --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 36a79e1a..36cd64c2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # attr_encrypted -[![Build Status](https://secure.travis-ci.org/attr-encrypted/attr_encrypted.svg)](https://travis-ci.org/attr-encrypted/attr_encrypted) [![Test Coverage](https://codeclimate.com/github/attr-encrypted/attr_encrypted/badges/coverage.svg)](https://codeclimate.com/github/attr-encrypted/attr_encrypted/coverage) [![Code Climate](https://codeclimate.com/github/attr-encrypted/attr_encrypted/badges/gpa.svg)](https://codeclimate.com/github/attr-encrypted/attr_encrypted) [![Gem Version](https://badge.fury.io/rb/attr_encrypted.svg)](https://badge.fury.io/rb/attr_encrypted) [![security](https://hakiri.io/github/attr-encrypted/attr_encrypted/master.svg)](https://hakiri.io/github/attr-encrypted/attr_encrypted/master) + +[![Build Status](https://github.com/KentaaNL/attr_encrypted/actions/workflows/test.yml/badge.svg)](https://github.com/KentaaNL/attr_encrypted/actions) Generates attr_accessors that transparently encrypt and decrypt attributes. @@ -11,7 +12,7 @@ It works with ANY class, however, you get a few extra features when you're using Add attr_encrypted to your gemfile: ```ruby - gem "attr_encrypted", "~> 3.0.0" + gem "attr_encrypted", github: "KentaaNL/attr_encrypted" ``` Then install the gem: From 37d29e256fb32f4b77e799916aad88d0d1e61fde Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Tue, 15 Jun 2021 15:26:22 +0200 Subject: [PATCH 09/17] Explain fork fixes --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 36cd64c2..d5b9c599 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ Generates attr_accessors that transparently encrypt and decrypt attributes. It works with ANY class, however, you get a few extra features when you're using it with `ActiveRecord`, `DataMapper`, or `Sequel`. +Forked from [attr-encrypted/attr_encrypted](https://github.com/attr-encrypted/attr_encrypted) with the following fixes: + +* Optional update encrypted attributes only when values changed (#1) +* Fix concurrency problem (#2) +* Support ActiveRecord 5.2 (#3) ## Installation From a945328a6b84b824603de211b49328e1e8207dd8 Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Fri, 8 Apr 2022 15:00:31 +0200 Subject: [PATCH 10/17] Rails 6.0 --- .github/workflows/test.yml | 4 ++-- attr_encrypted.gemspec | 2 +- lib/attr_encrypted/adapters/active_record.rb | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b42bced6..4e250446 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,8 +8,8 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ['2.3', '2.4', '2.5'] - rails-version: ['4.2.0', '5.0.0', '5.1.1', '5.2.0'] + ruby-version: ['2.5', '2.6', '2.7'] + rails-version: ['5.1.1', '5.2.0', '6.0.0'] env: ACTIVERECORD: ${{ matrix.rails-version }} steps: diff --git a/attr_encrypted.gemspec b/attr_encrypted.gemspec index 74ac4708..a7d5266c 100644 --- a/attr_encrypted.gemspec +++ b/attr_encrypted.gemspec @@ -50,7 +50,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', '~> 1.3.0', '>= 1.3.6') + s.add_development_dependency('sqlite3', '~> 1.4.0', '>= 1.4') end s.add_development_dependency('dm-sqlite-adapter') s.add_development_dependency('simplecov') diff --git a/lib/attr_encrypted/adapters/active_record.rb b/lib/attr_encrypted/adapters/active_record.rb index 5e4e3deb..a24ac807 100644 --- a/lib/attr_encrypted/adapters/active_record.rb +++ b/lib/attr_encrypted/adapters/active_record.rb @@ -81,7 +81,9 @@ def attr_encrypted(*attrs) if ::ActiveRecord::VERSION::STRING >= "5.2" # This is needed support attribute_was before a record has # been saved - set_attribute_was(attr, __send__(attr)) if value != __send__(attr) + if ::ActiveRecord::VERSION::STRING < "6.0" + set_attribute_was(attr, __send__(attr)) if value != __send__(attr) + end # 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) From b77e71e5de9ac26e9b5f008e2342b7034114ba05 Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Wed, 12 Oct 2022 15:41:32 +0200 Subject: [PATCH 11/17] Attribute was test < Rails 6.0 --- test/active_record_test.rb | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/test/active_record_test.rb b/test/active_record_test.rb index 543e04d7..3d0f70c8 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -201,21 +201,23 @@ def test_should_create_was_predicate assert_equal old_zipcode, address.zipcode_was end - def test_attribute_was_works_when_options_for_old_encrypted_value_are_different_than_options_for_new_encrypted_value - pw = 'password' - crypto_key = SecureRandom.urlsafe_base64(24) - old_iv = SecureRandom.random_bytes(12) - account = Account.create - encrypted_value = Encryptor.encrypt(value: pw, iv: old_iv, key: crypto_key) - Account.where(id: account.id).update_all(key: crypto_key, encrypted_password_iv: [old_iv].pack('m'), encrypted_password: [encrypted_value].pack('m')) - account = Account.find(account.id) - assert_equal pw, account.password - account.password = pw.reverse - assert_equal pw, account.password_was - account.save - account.reload - assert_equal Account::ACCOUNT_ENCRYPTION_KEY, account.key - assert_equal pw.reverse, account.password + if ::ActiveRecord::VERSION::STRING < "6.0" + def test_attribute_was_works_when_options_for_old_encrypted_value_are_different_than_options_for_new_encrypted_value + pw = 'password' + crypto_key = SecureRandom.urlsafe_base64(24) + old_iv = SecureRandom.random_bytes(12) + account = Account.create + encrypted_value = Encryptor.encrypt(value: pw, iv: old_iv, key: crypto_key) + Account.where(id: account.id).update_all(key: crypto_key, encrypted_password_iv: [old_iv].pack('m'), encrypted_password: [encrypted_value].pack('m')) + account = Account.find(account.id) + assert_equal pw, account.password + account.password = pw.reverse + assert_equal pw, account.password_was + account.save + account.reload + assert_equal Account::ACCOUNT_ENCRYPTION_KEY, account.key + assert_equal pw.reverse, account.password + end end # ActiveRecord 5.2 specific methods From 9e1209c056a5515534d67f27f13e2167ac1b7013 Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Wed, 12 Oct 2022 15:41:48 +0200 Subject: [PATCH 12/17] Fix keyword arguments deprecation in Ruby 2.7 --- lib/attr_encrypted/adapters/active_record.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/attr_encrypted/adapters/active_record.rb b/lib/attr_encrypted/adapters/active_record.rb index a24ac807..8bf241f4 100644 --- a/lib/attr_encrypted/adapters/active_record.rb +++ b/lib/attr_encrypted/adapters/active_record.rb @@ -60,7 +60,7 @@ def attr_encrypted(*attrs) if ::ActiveRecord::VERSION::STRING >= "4.1" define_method("#{attr}_changed?") do |options = {}| - attribute_changed?(attr, options) + attribute_changed?(attr, **options) end else define_method("#{attr}_changed?") do From a35017f3f58393ba19edceee87645cd1eb840b1d Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Wed, 12 Oct 2022 15:45:07 +0200 Subject: [PATCH 13/17] Build only Ruby 2.7 --- .github/workflows/test.yml | 2 +- .ruby-gemset | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .ruby-gemset diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e250446..2c5935fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ['2.5', '2.6', '2.7'] + ruby-version: ['2.7'] rails-version: ['5.1.1', '5.2.0', '6.0.0'] env: ACTIVERECORD: ${{ matrix.rails-version }} diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 00000000..0d47d339 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +attr_encrypted From f53a482380f4b6060da870996339a8684eb1f237 Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Wed, 12 Oct 2022 15:58:48 +0200 Subject: [PATCH 14/17] Add Rails 6.1 + Ruby 3.0/3.1 to CI Remove DataMapper. --- .github/workflows/test.yml | 4 +- README.md | 6 +-- attr_encrypted.gemspec | 2 - lib/attr_encrypted.rb | 2 +- lib/attr_encrypted/adapters/data_mapper.rb | 22 --------- test/data_mapper_test.rb | 57 ---------------------- test/legacy_data_mapper_test.rb | 55 --------------------- test/test_helper.rb | 1 - 8 files changed, 6 insertions(+), 143 deletions(-) delete mode 100644 lib/attr_encrypted/adapters/data_mapper.rb delete mode 100644 test/data_mapper_test.rb delete mode 100644 test/legacy_data_mapper_test.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c5935fb..2835ad4e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,8 +8,8 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ['2.7'] - rails-version: ['5.1.1', '5.2.0', '6.0.0'] + ruby-version: ['2.7', '3.0', '3.1'] + rails-version: ['5.1.1', '5.2.0', '6.0.0', '6.1.0'] env: ACTIVERECORD: ${{ matrix.rails-version }} steps: diff --git a/README.md b/README.md index d5b9c599..ae6836c7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Generates attr_accessors that transparently encrypt and decrypt attributes. -It works with ANY class, however, you get a few extra features when you're using it with `ActiveRecord`, `DataMapper`, or `Sequel`. +It works with ANY class, however, you get a few extra features when you're using it with `ActiveRecord` or `Sequel`. Forked from [attr-encrypted/attr_encrypted](https://github.com/attr-encrypted/attr_encrypted) with the following fixes: @@ -28,7 +28,7 @@ Then install the gem: ## Usage -If you're using an ORM like `ActiveRecord`, `DataMapper`, or `Sequel`, using attr_encrypted is easy: +If you're using an ORM like `ActiveRecord` or `Sequel`, using attr_encrypted is easy: ```ruby class User @@ -374,7 +374,7 @@ NOTE: This only works if all records are encrypted with the same encryption key __NOTE: This feature is deprecated and will be removed in the next major release.__ -### DataMapper and Sequel +### Sequel #### Default options diff --git a/attr_encrypted.gemspec b/attr_encrypted.gemspec index a7d5266c..9631d6d9 100644 --- a/attr_encrypted.gemspec +++ b/attr_encrypted.gemspec @@ -37,7 +37,6 @@ Gem::Specification.new do |s| end s.add_development_dependency('activerecord', activerecord_version) s.add_development_dependency('actionpack', activerecord_version) - s.add_development_dependency('datamapper') s.add_development_dependency('rake') s.add_development_dependency('minitest') s.add_development_dependency('sequel') @@ -52,7 +51,6 @@ Gem::Specification.new do |s| else s.add_development_dependency('sqlite3', '~> 1.4.0', '>= 1.4') end - s.add_development_dependency('dm-sqlite-adapter') s.add_development_dependency('simplecov') s.add_development_dependency('simplecov-rcov') s.add_development_dependency("codeclimate-test-reporter", '<= 0.6.0') diff --git a/lib/attr_encrypted.rb b/lib/attr_encrypted.rb index 15d2e076..ca1b8499 100644 --- a/lib/attr_encrypted.rb +++ b/lib/attr_encrypted.rb @@ -52,7 +52,7 @@ def self.extended(base) # :nodoc: # string instead of just 'true'. See # http://www.ruby-doc.org/core/classes/Array.html#M002245 # for more encoding directives. - # Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel. + # Defaults to false unless you're using it with ActiveRecord or Sequel. # # encode_iv: Defaults to true. diff --git a/lib/attr_encrypted/adapters/data_mapper.rb b/lib/attr_encrypted/adapters/data_mapper.rb deleted file mode 100644 index d918f312..00000000 --- a/lib/attr_encrypted/adapters/data_mapper.rb +++ /dev/null @@ -1,22 +0,0 @@ -if defined?(DataMapper) - module AttrEncrypted - module Adapters - module DataMapper - def self.extended(base) # :nodoc: - class << base - alias_method :included_without_attr_encrypted, :included - alias_method :included, :included_with_attr_encrypted - end - end - - def included_with_attr_encrypted(base) - included_without_attr_encrypted(base) - base.extend AttrEncrypted - base.attr_encrypted_options[:encode] = true - end - end - end - end - - DataMapper::Resource.extend AttrEncrypted::Adapters::DataMapper -end \ No newline at end of file diff --git a/test/data_mapper_test.rb b/test/data_mapper_test.rb deleted file mode 100644 index b01ccbd2..00000000 --- a/test/data_mapper_test.rb +++ /dev/null @@ -1,57 +0,0 @@ -require_relative 'test_helper' - -DataMapper.setup(:default, 'sqlite3::memory:') - -class Client - include DataMapper::Resource - - property :id, Serial - property :encrypted_email, String - property :encrypted_email_iv, String - property :encrypted_email_salt, String - - property :encrypted_credentials, Text - property :encrypted_credentials_iv, Text - property :encrypted_credentials_salt, Text - - self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt - - attr_encrypted :email, :key => SECRET_KEY - attr_encrypted :credentials, :key => SECRET_KEY, :marshal => true - - def initialize(attrs = {}) - super attrs - self.credentials ||= { :username => 'example', :password => 'test' } - end -end - -DataMapper.auto_migrate! - -class DataMapperTest < Minitest::Test - - def setup - Client.all.each(&:destroy) - end - - def test_should_encrypt_email - @client = Client.new :email => 'test@example.com' - assert @client.save - refute_nil @client.encrypted_email - refute_equal @client.email, @client.encrypted_email - assert_equal @client.email, Client.first.email - end - - def test_should_marshal_and_encrypt_credentials - @client = Client.new - assert @client.save - refute_nil @client.encrypted_credentials - refute_equal @client.credentials, @client.encrypted_credentials - assert_equal @client.credentials, Client.first.credentials - assert Client.first.credentials.is_a?(Hash) - end - - def test_should_encode_by_default - assert Client.attr_encrypted_options[:encode] - end - -end diff --git a/test/legacy_data_mapper_test.rb b/test/legacy_data_mapper_test.rb deleted file mode 100644 index eab5a23e..00000000 --- a/test/legacy_data_mapper_test.rb +++ /dev/null @@ -1,55 +0,0 @@ -require_relative 'test_helper' - -DataMapper.setup(:default, 'sqlite3::memory:') - -class LegacyClient - include DataMapper::Resource - self.attr_encrypted_options[:insecure_mode] = true - self.attr_encrypted_options[:algorithm] = 'aes-256-cbc' - self.attr_encrypted_options[:mode] = :single_iv_and_salt - - property :id, Serial - property :encrypted_email, String - property :encrypted_credentials, Text - property :salt, String - - attr_encrypted :email, :key => 'a secret key', mode: :single_iv_and_salt - attr_encrypted :credentials, :key => Proc.new { |client| Encryptor.encrypt(:value => client.salt, :key => 'some private key', insecure_mode: true, algorithm: 'aes-256-cbc') }, :marshal => true, mode: :single_iv_and_salt - - def initialize(attrs = {}) - super attrs - self.salt ||= Digest::SHA1.hexdigest((Time.now.to_i * rand(5)).to_s) - self.credentials ||= { :username => 'example', :password => 'test' } - end -end - -DataMapper.auto_migrate! - -class LegacyDataMapperTest < Minitest::Test - - def setup - LegacyClient.all.each(&:destroy) - end - - def test_should_encrypt_email - @client = LegacyClient.new :email => 'test@example.com' - assert @client.save - refute_nil @client.encrypted_email - refute_equal @client.email, @client.encrypted_email - assert_equal @client.email, LegacyClient.first.email - end - - def test_should_marshal_and_encrypt_credentials - @client = LegacyClient.new - assert @client.save - refute_nil @client.encrypted_credentials - refute_equal @client.credentials, @client.encrypted_credentials - assert_equal @client.credentials, LegacyClient.first.credentials - assert LegacyClient.first.credentials.is_a?(Hash) - end - - def test_should_encode_by_default - assert LegacyClient.attr_encrypted_options[:encode] - end - -end diff --git a/test/test_helper.rb b/test/test_helper.rb index 6202e2e4..ad9dd2f8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -25,7 +25,6 @@ end require 'active_record' -require 'data_mapper' require 'digest/sha2' require 'sequel' ActiveSupport::Deprecation.behavior = :raise From 0e5ad8566b0fe8630863a26a3a6a6bedd167d3af Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Wed, 12 Oct 2022 16:02:16 +0200 Subject: [PATCH 15/17] Exclude Rails 5.x from Ruby 3.x --- .github/workflows/test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2835ad4e..8145f56b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,15 @@ jobs: matrix: ruby-version: ['2.7', '3.0', '3.1'] rails-version: ['5.1.1', '5.2.0', '6.0.0', '6.1.0'] + exclude: + - ruby-version: 3.0 + rails-version: 5.1.1 + - ruby-version: 3.1 + rails-version: 5.1.1 + - ruby-version: 3.0 + rails-version: 5.2.0 + - ruby-version: 3.1 + rails-version: 5.2.0 env: ACTIVERECORD: ${{ matrix.rails-version }} steps: From b5a0f611d8eb8cb75e3cb7fc69620799affaa774 Mon Sep 17 00:00:00 2001 From: Robin van Dijk Date: Wed, 12 Oct 2022 16:37:09 +0200 Subject: [PATCH 16/17] Rename encrypt/decrypt methods --- lib/attr_encrypted.rb | 30 +++++++++++++++++------------- test/attr_encrypted_test.rb | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/attr_encrypted.rb b/lib/attr_encrypted.rb index ca1b8499..675cbf08 100644 --- a/lib/attr_encrypted.rb +++ b/lib/attr_encrypted.rb @@ -161,12 +161,12 @@ 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}", decrypt_attribute(attribute, send(encrypted_attribute_name))) end define_method("#{attribute}=") do |value| if should_update_encrypted_attribute?(attribute, value) - send("#{encrypted_attribute_name}=", encrypt(attribute, value)) + send("#{encrypted_attribute_name}=", encrypt_attribute(attribute, value)) instance_variable_set("@#{attribute}", value) end end @@ -238,8 +238,8 @@ def attr_encrypted?(attribute) # attr_encrypted :email # end # - # email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING') - def decrypt(attribute, encrypted_value, options = {}) + # email = User.decrypt_attribute(:email, 'SOME_ENCRYPTED_EMAIL_STRING') + def decrypt_attribute(attribute, encrypted_value, options = {}) options = 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] @@ -264,8 +264,8 @@ def decrypt(attribute, encrypted_value, options = {}) # attr_encrypted :email # end # - # encrypted_email = User.encrypt(:email, 'test@example.com') - def encrypt(attribute, value, options = {}) + # encrypted_email = User.encrypt_attribute(:email, 'test@example.com') + def encrypt_attribute(attribute, value, options = {}) options = 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 @@ -307,7 +307,11 @@ 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) + if $1 == 'encrypt' + send(:encrypt_attribute, $3, *arguments) + else + send(:decrypt_attribute, $3, *arguments) + end else super end @@ -328,11 +332,11 @@ module InstanceMethods # end # # @user = User.new('some-secret-key') - # @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING') - def decrypt(attribute, encrypted_value) + # @user.decrypt_attribute(:email, 'SOME_ENCRYPTED_EMAIL_STRING') + def decrypt_attribute(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)) + self.class.decrypt_attribute(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 @@ -349,11 +353,11 @@ def decrypt(attribute, encrypted_value) # end # # @user = User.new('some-secret-key') - # @user.encrypt(:email, 'test@example.com') - def encrypt(attribute, value) + # @user.encrypt_attribute(:email, 'test@example.com') + def encrypt_attribute(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)) + self.class.encrypt_attribute(attribute, value, evaluated_attr_encrypted_options_for(attribute)) end # Copies the class level hash of encrypted attributes with virtual attribute names as keys diff --git a/test/attr_encrypted_test.rb b/test/attr_encrypted_test.rb index ac5d959e..780250d4 100644 --- a/test/attr_encrypted_test.rb +++ b/test/attr_encrypted_test.rb @@ -380,7 +380,7 @@ 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.decrypt_attribute(:email, @user1.encrypted_email) end def test_should_specify_the_default_algorithm From e076b9b0b736010a6d8ef33d7b24937a2782fcfd Mon Sep 17 00:00:00 2001 From: Peter Postma Date: Wed, 12 Oct 2022 16:48:09 +0200 Subject: [PATCH 17/17] Update fork fixes --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ae6836c7..b0f097ce 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ Forked from [attr-encrypted/attr_encrypted](https://github.com/attr-encrypted/at * Optional update encrypted attributes only when values changed (#1) * Fix concurrency problem (#2) -* Support ActiveRecord 5.2 (#3) +* Support ActiveRecord 5.2, 6.0 and 6.1 (#3, #6, #7) +* Rename encrypt/decrypt methods (#8) ## Installation