diff --git a/README.md b/README.md index 87ac7219..67e55248 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,10 @@ -## Maintainer(s) wanted!!! - -**If you have an interest in maintaining this project... please see https://github.com/attr-encrypted/attr_encrypted/issues/379** - # 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) 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`. ## Installation @@ -27,7 +23,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 @@ -106,6 +102,7 @@ By default, the encrypted attribute name is `encrypted_#{attribute}` (e.g. `attr ## attr_encrypted options #### Options are evaluated + All options will be evaluated at the instance level. If you pass in a symbol it will be passed as a message to the instance of your class. If you pass a proc or any object that responds to `:call` it will be called. You can pass in the instance of your class as an argument to the proc. Anything else will be returned. For example: ##### Symbols representing instance methods @@ -344,37 +341,12 @@ If you're using this gem with `ActiveRecord`, you get a few extra features: The `:encode` option is set to true by default. -#### Dynamic `find_by_` and `scoped_by_` methods - -Let's say you'd like to encrypt your user's email addresses, but you also need a way for them to login. Simply set up your class like so: - -```ruby - class User < ActiveRecord::Base - attr_encrypted :email, key: 'This is a key that is 256 bits!!' - attr_encrypted :password, key: 'some other secret key' - end -``` - -You can now lookup and login users like so: - -```ruby - User.find_by_email_and_password('test@example.com', 'testing') -``` - -The call to `find_by_email_and_password` is intercepted and modified to `find_by_encrypted_email_and_encrypted_password('ENCRYPTED EMAIL', 'ENCRYPTED PASSWORD')`. The dynamic scope methods like `scoped_by_email_and_password` work the same way. - -NOTE: This only works if all records are encrypted with the same encryption key (per attribute). - -__NOTE: This feature is deprecated and will be removed in the next major release.__ - - -### DataMapper and Sequel +### Sequel #### Default options The `:encode` option is set to true by default. - ## Deprecations attr_encrypted v2.0.0 now depends on encryptor v2.0.0. As part of both major releases many insecure defaults and behaviors have been deprecated. The new default behavior is as follows: @@ -425,7 +397,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_attr_encrypted_attributes[attribute][:operation] == :decrypting end end diff --git a/attr_encrypted.gemspec b/attr_encrypted.gemspec index 41f96ed9..0d43a794 100644 --- a/attr_encrypted.gemspec +++ b/attr_encrypted.gemspec @@ -19,15 +19,12 @@ 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'] s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- test/*`.split("\n") - s.required_ruby_version = '>= 2.0.0' + s.required_ruby_version = '>= 2.7.0' s.add_dependency('encryptor', ['~> 3.0.0']) # support for testing with specific active record version @@ -38,24 +35,13 @@ 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('pry') s.add_development_dependency('sequel') - if RUBY_VERSION < '2.1.0' - s.add_development_dependency('nokogiri', '< 1.7.0') - s.add_development_dependency('public_suffix', '< 3.0.0') - end - if defined?(RUBY_ENGINE) && RUBY_ENGINE.to_sym == :jruby - 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') - end - s.add_development_dependency('dm-sqlite-adapter') + s.add_development_dependency('sqlite3') s.add_development_dependency('simplecov') s.add_development_dependency('simplecov-rcov') - s.add_development_dependency("codeclimate-test-reporter", '<= 0.6.0') s.cert_chain = ['certs/saghaulor.pem'] s.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/ diff --git a/lib/attr_encrypted.rb b/lib/attr_encrypted.rb index 88e5f65e..f8e98d7c 100644 --- a/lib/attr_encrypted.rb +++ b/lib/attr_encrypted.rb @@ -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_attributes = {}, {} end end @@ -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) + attr_encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name) end end @@ -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_attributes.has_key?(attribute.to_sym) end # Decrypts a value for the attribute specified @@ -236,7 +236,7 @@ def attr_encrypted?(attribute) # # email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING') def decrypt(attribute, encrypted_value, options = {}) - options = encrypted_attributes[attribute.to_sym].merge(options) + options = attr_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)) @@ -262,7 +262,7 @@ def decrypt(attribute, encrypted_value, options = {}) # # encrypted_email = User.encrypt(:email, 'test@example.com') def encrypt(attribute, value, options = {}) - options = encrypted_attributes[attribute.to_sym].merge(options) + options = attr_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)) @@ -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_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } } + def attr_encrypted_attributes + @attr_encrypted_attributes ||= superclass.attr_encrypted_attributes.dup end # Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method @@ -326,8 +326,8 @@ 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) + attr_encrypted_attributes[attribute.to_sym][:operation] = :decrypting + attr_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)) end @@ -347,18 +347,18 @@ def decrypt(attribute, encrypted_value) # @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) + attr_encrypted_attributes[attribute.to_sym][:operation] = :encrypting + attr_encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(value) self.class.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_attributes + @attr_encrypted_attributes ||= begin duplicated= {} - self.class.encrypted_attributes.map { |key, value| duplicated[key] = value.dup } + self.class.attr_encrypted_attributes.map { |key, value| duplicated[key] = value.dup } duplicated end end @@ -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_attributes[attribute.to_sym] attribute_option_value = attributes[:attribute] [:if, :unless, :value_present, :allow_empty_value].each do |option| diff --git a/lib/attr_encrypted/adapters/active_record.rb b/lib/attr_encrypted/adapters/active_record.rb index fca9343e..f90540b6 100644 --- a/lib/attr_encrypted/adapters/active_record.rb +++ b/lib/attr_encrypted/adapters/active_record.rb @@ -11,7 +11,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_attributes.keys.each do |attribute_name| instance_variable_set("@#{attribute_name}", nil) end result @@ -21,22 +21,19 @@ def reload(*args, &block) class << self alias_method :method_missing_without_attr_encrypted, :method_missing - alias_method :method_missing, :method_missing_with_attr_encrypted end 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_attributes.key?(k.to_sym) }, *args + send method, new_attributes.reject { |k, _| !self.class.attr_encrypted_attributes.key?(k.to_sym) }, *args end private :perform_attribute_assignment - if ::ActiveRecord::VERSION::STRING > "3.1" - alias_method :assign_attributes_without_attr_encrypted, :assign_attributes - def assign_attributes(*args) - perform_attribute_assignment :assign_attributes_without_attr_encrypted, *args - end + alias_method :assign_attributes_without_attr_encrypted, :assign_attributes + def assign_attributes(*args) + perform_attribute_assignment :assign_attributes_without_attr_encrypted, *args end alias_method :attributes_without_attr_encrypted=, :attributes= @@ -53,25 +50,11 @@ def attr_encrypted(*attrs) super options = attrs.extract_options! attr = attrs.pop - attribute attr if ::ActiveRecord::VERSION::STRING >= "5.1.0" - options.merge! encrypted_attributes[attr] - - define_method("#{attr}_was") do - attribute_was(attr) - end - - if ::ActiveRecord::VERSION::STRING >= "4.1" - define_method("#{attr}_changed?") do |options = {}| - attribute_changed?(attr, options) - end - else - define_method("#{attr}_changed?") do - attribute_changed?(attr) - end - end + attribute attr + options.merge! attr_encrypted_attributes[attr] - define_method("#{attr}_change") do - attribute_change(attr) + define_method("#{attr}_changed?") do |options = {}| + attribute_changed?(attr, **options) end define_method("#{attr}_with_dirtiness=") do |value| @@ -100,38 +83,6 @@ def attribute_instance_methods_as_symbols def attribute_instance_methods_as_symbols_available? connected? && table_exists? end - - # Allows you to use dynamic methods like find_by_email or scoped_by_email for - # encrypted attributes - # - # NOTE: This only works when the :key option is specified as a string (see the README) - # - # This is useful for encrypting fields like email addresses. Your user's email addresses - # are encrypted in the database, but you can still look up a user by email for logging in - # - # Example - # - # class User < ActiveRecord::Base - # attr_encrypted :email, key: 'secret key' - # end - # - # User.find_by_email_and_password('test@example.com', 'testing') - # # results in a call to - # User.find_by_encrypted_email_and_password('the_encrypted_version_of_test@example.com', 'testing') - 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 - 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] - end - end - method = "#{match.captures[0]}_#{match.captures[1]}_#{attribute_names.join('_and_')}".to_sym - end - method_missing_without_attr_encrypted(method, *args, &block) - end end end end diff --git a/test/active_record_test.rb b/test/active_record_test.rb index 8ec31aea..181187be 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -45,16 +45,14 @@ def create_tables ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError) -if ::ActiveRecord::VERSION::STRING > "4.0" - module Rack - module Test - class UploadedFile; end - end +module Rack + module Test + class UploadedFile; end end - - require 'action_controller/metal/strong_parameters' end +require 'action_controller/metal/strong_parameters' + class Person < ActiveRecord::Base self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt attr_encrypted :email, key: SECRET_KEY @@ -85,7 +83,7 @@ class Account < ActiveRecord::Base attr_encrypted :password, key: :password_encryption_key def encrypting?(attr) - encrypted_attributes[attr][:operation] == :encrypting + attr_encrypted_attributes[attr][:operation] == :encrypting end def password_encryption_key @@ -103,10 +101,9 @@ class PersonWithSerialization < ActiveRecord::Base serialize :password end -class UserWithProtectedAttribute < ActiveRecord::Base +class SomeUser < ActiveRecord::Base self.table_name = 'users' attr_encrypted :password, key: SECRET_KEY - attr_protected :is_admin if ::ActiveRecord::VERSION::STRING < "4.0" end class PersonUsingAlias < ActiveRecord::Base @@ -190,83 +187,62 @@ def test_should_create_changed_predicate assert person.email_changed? end - def test_should_create_was_predicate - original_email = 'test@example.com' - person = Person.create!(email: original_email) - assert_equal original_email, person.email_was - person.email = 'test2@example.com' - assert_equal original_email, person.email_was - old_pm_name = "Winston Churchill" - pm = PrimeMinister.create!(name: old_pm_name) - assert_equal old_pm_name, pm.name_was - old_zipcode = "90210" - address = Address.create!(zipcode: old_zipcode, mode: "single_iv_and_salt") - assert_equal old_zipcode, address.zipcode_was + # TODO: https://github.com/rails/rails/issues/36874 + # It would appear `attribute_was` is now `attribute_in_database` however I am unsure of the intended behavior currently. + # def test_should_create_was_predicate + # original_email = 'test@example.com' + # person = Person.create!(email: original_email) + # binding.pry + # assert_equal original_email, person.email_was + # person.email = 'test2@example.com' + # assert_equal original_email, person.email_was + # old_pm_name = "Winston Churchill" + # pm = PrimeMinister.create!(name: old_pm_name) + # assert_equal old_pm_name, pm.name_was + # old_zipcode = "90210" + # address = Address.create!(zipcode: old_zipcode, mode: "single_iv_and_salt") + # 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 + # end + + def test_should_assign_attributes + @user = SomeUser.new(login: 'login', is_admin: false) + @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login) + assert_equal 'modified', @user.login 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 + def test_should_not_assign_protected_attributes + @user = SomeUser.new(login: 'login', is_admin: false) + @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login) + assert !@user.is_admin? end - if ::ActiveRecord::VERSION::STRING > "4.0" - def test_should_assign_attributes - @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) - @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login) - assert_equal 'modified', @user.login - end - - def test_should_not_assign_protected_attributes - @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) - @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login) - assert !@user.is_admin? - end - - def test_should_raise_exception_if_not_permitted - @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) - assert_raises ActiveModel::ForbiddenAttributesError do - @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true) - end - end - - def test_should_raise_exception_on_init_if_not_permitted - assert_raises ActiveModel::ForbiddenAttributesError do - @user = UserWithProtectedAttribute.new ActionController::Parameters.new(login: 'modified', is_admin: true) - end - end - else - def test_should_assign_attributes - @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) - @user.attributes = { login: 'modified', is_admin: true } - assert_equal 'modified', @user.login - end - - def test_should_not_assign_protected_attributes - @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) - @user.attributes = { login: 'modified', is_admin: true } - assert !@user.is_admin? + def test_should_raise_exception_if_not_permitted + @user = SomeUser.new(login: 'login', is_admin: false) + assert_raises ActiveModel::ForbiddenAttributesError do + @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true) end + end - def test_should_assign_protected_attributes - @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) - if ::ActiveRecord::VERSION::STRING > "3.1" - @user.send(:assign_attributes, { login: 'modified', is_admin: true }, without_protection: true) - else - @user.send(:attributes=, { login: 'modified', is_admin: true }, false) - end - assert @user.is_admin? + def test_should_raise_exception_on_init_if_not_permitted + assert_raises ActiveModel::ForbiddenAttributesError do + @user = SomeUser.new ActionController::Parameters.new(login: 'modified', is_admin: true) end end @@ -279,23 +255,21 @@ 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_attributes[:email][:mode].class, Proc + assert_equal @person.class.attr_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_attributes[:credentials][:mode].class, Proc + assert_equal @person.class.attr_encrypted_attributes[:credentials][:mode].call, :single_iv_and_salt assert_nil @person.encrypted_credentials_salt assert_nil @person.encrypted_credentials_iv end - if ::ActiveRecord::VERSION::STRING > "3.1" - def test_should_allow_assign_attributes_with_nil - @person = Person.new - assert_nil(@person.assign_attributes nil) - end + def test_should_allow_assign_attributes_with_nil + @person = Person.new + assert_nil(@person.assign_attributes nil) end def test_that_alias_encrypts_column diff --git a/test/attr_encrypted_test.rb b/test/attr_encrypted_test.rb index 84cb130a..793fe756 100644 --- a/test/attr_encrypted_test.rb +++ b/test/attr_encrypted_test.rb @@ -82,12 +82,12 @@ def setup @iv = SecureRandom.random_bytes(12) end - def test_should_store_email_in_encrypted_attributes - assert User.encrypted_attributes.include?(:email) + def test_should_store_email_in_attr_encrypted_attributes + assert User.attr_encrypted_attributes.include?(:email) end - def test_should_not_store_salt_in_encrypted_attributes - refute User.encrypted_attributes.include?(:salt) + def test_should_not_store_salt_in_attr_encrypted_attributes + refute User.attr_encrypted_attributes.include?(:salt) end def test_attr_encrypted_should_return_true_for_email @@ -95,7 +95,7 @@ def test_attr_encrypted_should_return_true_for_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_attributes[:email][:attribute], User.attr_encrypted_attributes[:without_encoding][:attribute] end def test_attr_encrypted_should_return_false_for_salt @@ -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_attributes[:email] iv = @user.send(:generate_iv, options[:algorithm]) encoded_iv = [iv].pack(options[:encode_iv]) salt = SecureRandom.random_bytes @@ -222,8 +222,8 @@ def test_should_use_options_found_in_the_attr_encrypted_options_attribute assert_equal encrypted, @user.crypted_password_test 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 + def test_should_inherit_attr_encrypted_attributes + assert_equal [User.attr_encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, Admin.attr_encrypted_attributes.keys.collect { |key| key.to_s }.sort end def test_should_inherit_attr_encrypted_options @@ -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_attributes.empty? end def test_should_evaluate_a_symbol_option @@ -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_attributes.include?(:aliased) end def test_should_always_reset_options @@ -385,8 +385,8 @@ def test_should_decrypt_second_record 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_attributes[:email][:algorithm] + assert_equal YetAnotherClass.attr_encrypted_attributes[:email][:algorithm], 'aes-256-gcm' end def test_should_not_encode_iv_when_encode_iv_is_false @@ -469,14 +469,14 @@ def test_should_not_by_default_generate_iv_when_attribute_is_empty assert_nil user.encrypted_with_true_if_iv end - def test_encrypted_attributes_state_is_not_shared + def test_attr_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] + assert_equal :encrypting, user.attr_encrypted_attributes[:ssn][:operation] + assert_nil another_user.attr_encrypted_attributes[:ssn][:operation] end def test_should_not_by_default_generate_key_when_attribute_is_empty diff --git a/test/data_mapper_test.rb b/test/data_mapper_test.rb deleted file mode 100644 index 3fb284a6..00000000 --- a/test/data_mapper_test.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -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_active_record_test.rb b/test/legacy_active_record_test.rb deleted file mode 100644 index 80231857..00000000 --- a/test/legacy_active_record_test.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -# -*- encoding: utf-8 -*- -require_relative 'test_helper' - -ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:' - -def create_people_table - ActiveRecord::Schema.define(:version => 1) do - create_table :legacy_people do |t| - t.string :encrypted_email - t.string :password - t.string :encrypted_credentials - t.string :salt - end - end -end - -# The table needs to exist before defining the class -create_people_table - -ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError) - -class LegacyPerson < ActiveRecord::Base - self.attr_encrypted_options[:insecure_mode] = true - self.attr_encrypted_options[:algorithm] = 'aes-256-cbc' - self.attr_encrypted_options[:mode] = :single_iv_and_salt - - attr_encrypted :email, :key => 'a secret key' - attr_encrypted :credentials, :key => Proc.new { |user| Encryptor.encrypt(:value => user.salt, :key => 'some private key', insecure_mode: true, algorithm: 'aes-256-cbc') }, :marshal => true - - ActiveSupport::Deprecation.silenced = true - def after_initialize; end - ActiveSupport::Deprecation.silenced = false - - after_initialize :initialize_salt_and_credentials - - protected - - def initialize_salt_and_credentials - self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(5)).to_s) - self.credentials ||= { :username => 'example', :password => 'test' } - rescue ActiveRecord::MissingAttributeError - end -end - -class LegacyPersonWithValidation < LegacyPerson - validates_presence_of :email - validates_uniqueness_of :encrypted_email -end - -class LegacyActiveRecordTest < Minitest::Test - - def setup - drop_all_tables - create_people_table - end - - def test_should_decrypt_with_correct_encoding - if defined?(Encoding) - @person = LegacyPerson.create :email => 'test@example.com' - assert_equal 'UTF-8', LegacyPerson.first.email.encoding.name - end - end - - def test_should_encrypt_email - @person = LegacyPerson.create :email => 'test@example.com' - refute_nil @person.encrypted_email - refute_equal @person.email, @person.encrypted_email - assert_equal @person.email, LegacyPerson.first.email - end - - def test_should_marshal_and_encrypt_credentials - @person = LegacyPerson.create - refute_nil @person.encrypted_credentials - refute_equal @person.credentials, @person.encrypted_credentials - assert_equal @person.credentials, LegacyPerson.first.credentials - end - - def test_should_find_by_email - @person = LegacyPerson.create(:email => 'test@example.com') - assert_equal @person, LegacyPerson.find_by_email('test@example.com') - end - - def test_should_find_by_email_and_password - LegacyPerson.create(:email => 'test@example.com', :password => 'invalid') - @person = LegacyPerson.create(:email => 'test@example.com', :password => 'test') - assert_equal @person, LegacyPerson.find_by_email_and_password('test@example.com', 'test') - end - - def test_should_scope_by_email - @person = LegacyPerson.create(:email => 'test@example.com') - assert_equal @person, LegacyPerson.scoped_by_email('test@example.com').first rescue NoMethodError - end - - def test_should_scope_by_email_and_password - LegacyPerson.create(:email => 'test@example.com', :password => 'invalid') - @person = LegacyPerson.create(:email => 'test@example.com', :password => 'test') - assert_equal @person, LegacyPerson.scoped_by_email_and_password('test@example.com', 'test').first rescue NoMethodError - end - - def test_should_encode_by_default - assert LegacyPerson.attr_encrypted_options[:encode] - end - - def test_should_validate_presence_of_email - @person = LegacyPersonWithValidation.new - assert !@person.valid? - assert !@person.errors[:email].empty? || @person.errors.on(:email) - end - - def test_should_validate_uniqueness_of_email - @person = LegacyPersonWithValidation.new :email => 'test@example.com' - assert @person.save - @person2 = LegacyPersonWithValidation.new :email => @person.email - assert !@person2.valid? - assert !@person2.errors[:encrypted_email].empty? || @person2.errors.on(:encrypted_email) - end - -end diff --git a/test/legacy_attr_encrypted_test.rb b/test/legacy_attr_encrypted_test.rb index 875086d2..608a8197 100644 --- a/test/legacy_attr_encrypted_test.rb +++ b/test/legacy_attr_encrypted_test.rb @@ -57,12 +57,12 @@ def self.call(object) class LegacyAttrEncryptedTest < Minitest::Test - def test_should_store_email_in_encrypted_attributes - assert LegacyUser.encrypted_attributes.include?(:email) + def test_should_store_email_in_attr_encrypted_attributes + assert LegacyUser.attr_encrypted_attributes.include?(:email) end - def test_should_not_store_salt_in_encrypted_attributes - assert !LegacyUser.encrypted_attributes.include?(:salt) + def test_should_not_store_salt_in_attr_encrypted_attributes + assert !LegacyUser.attr_encrypted_attributes.include?(:salt) end def test_attr_encrypted_should_return_true_for_email @@ -70,7 +70,7 @@ def test_attr_encrypted_should_return_true_for_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_attributes[:email][:attribute], LegacyUser.attr_encrypted_attributes[:without_encoding][:attribute] end def test_attr_encrypted_should_return_false_for_salt @@ -200,8 +200,8 @@ def test_should_use_options_found_in_the_attr_encrypted_options_attribute assert_equal Encryptor.encrypt(:value => 'testing', :key => 'LegacyUser', insecure_mode: true, algorithm: 'aes-256-cbc'), @user.crypted_password_test 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 + def test_should_inherit_attr_encrypted_attributes + assert_equal [LegacyUser.attr_encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, LegacyAdmin.attr_encrypted_attributes.keys.collect { |key| key.to_s }.sort end def test_should_inherit_attr_encrypted_options @@ -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_attributes.empty? end def test_should_evaluate_a_symbol_option @@ -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_attributes.include?(:aliased) end def test_should_always_reset_options diff --git a/test/legacy_data_mapper_test.rb b/test/legacy_data_mapper_test.rb deleted file mode 100644 index 03916dd9..00000000 --- a/test/legacy_data_mapper_test.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -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/run.sh b/test/run.sh index 7e9b777e..3543bad4 100755 --- a/test/run.sh +++ b/test/run.sh @@ -1,12 +1,21 @@ -#!/usr/bin/env sh -e +#!/usr/bin/env bash -for RUBY in 1.9.3 2.0.0 2.1 2.2 +set -e + +for RUBY in 2.7.6 3.0.4 do - for RAILS in 2.3.8 3.0.0 3.1.0 3.2.0 4.0.0 4.1.0 4.2.0 + for ACTIVERECORD in 6.1 7.0 do - if [[ $RUBY -gt 1.9.3 && $RAILS -lt 4.0.0 ]]; then - continue - fi - RBENV_VERSION=$RUBY ACTIVERECORD=$RAILS bundle && bundle exec rake + echo ">>> Testing with Ruby ${RUBY} and ActiveRecord ${ACTIVERECORD}." + export RBENV_VERSION=$RUBY + export ACTIVERECORD=$ACTIVERECORD + + rbenv install $RUBY --skip-existing + bundle install + bundle check + bundle exec rake test + rm Gemfile.lock + echo ">>> Finished testing with Ruby ${RUBY} and ActiveRecord ${ACTIVERECORD}." done done + \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index c352a138..070651cc 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,13 +2,12 @@ require 'simplecov' require 'simplecov-rcov' -require "codeclimate-test-reporter" +require 'pry' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( [ SimpleCov::Formatter::HTMLFormatter, - SimpleCov::Formatter::RcovFormatter, - CodeClimate::TestReporter::Formatter + SimpleCov::Formatter::RcovFormatter ] ) @@ -16,8 +15,6 @@ add_filter 'test' end -CodeClimate::TestReporter.start - require 'minitest/autorun' # Rails 4.0.x pins to an old minitest @@ -26,7 +23,6 @@ end require 'active_record' -require 'data_mapper' require 'digest/sha2' require 'sequel' ActiveSupport::Deprecation.behavior = :raise @@ -35,11 +31,7 @@ $:.unshift(File.dirname(__FILE__)) require 'attr_encrypted' -DB = if defined?(RUBY_ENGINE) && RUBY_ENGINE.to_sym == :jruby - Sequel.jdbc('jdbc:sqlite::memory:') -else - Sequel.sqlite -end +DB = Sequel.sqlite # The :after_initialize hook was removed in Sequel 4.0 # and had been deprecated for a while before that: @@ -55,6 +47,6 @@ def base64_encoding_regex def drop_all_tables connection = ActiveRecord::Base.connection - tables = (ActiveRecord::VERSION::MAJOR >= 5 ? connection.data_sources : connection.tables) + tables = connection.data_sources tables.each { |table| ActiveRecord::Base.connection.drop_table(table) } end