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

Sample of deprecating EOL support, updating test wrapper #413

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 5 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
20 changes: 3 additions & 17 deletions attr_encrypted.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/
Expand Down
32 changes: 16 additions & 16 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_attributes = {}, {}
end
end

Expand Down Expand Up @@ -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

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_attributes.has_key?(attribute.to_sym)
end

# Decrypts a value for the attribute specified
Expand All @@ -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))
Expand All @@ -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))
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_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
Expand Down Expand Up @@ -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

Expand All @@ -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
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_attributes[attribute.to_sym]
attribute_option_value = attributes[:attribute]

[:if, :unless, :value_present, :allow_empty_value].each do |option|
Expand Down
69 changes: 10 additions & 59 deletions lib/attr_encrypted/adapters/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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=
Expand All @@ -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|
Expand Down Expand Up @@ -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 <tt>find_by_email</tt> or <tt>scoped_by_email</tt> for
# encrypted attributes
#
# NOTE: This only works when the <tt>:key</tt> 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
Expand Down
Loading