Skip to content

Optional update encrypted attributes only when values changed #301

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

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fe1d391
Optional update encrypted attributes only when values changed
Mar 9, 2018
c3dde48
Merge pull request #1 from KentaaNL/optional-encrypt-unchanged-attrib…
ppostma Mar 12, 2018
9fc8962
Fixes defect in which encrypted_attributes state was shared across al…
bfreese Jul 19, 2018
d01839e
Fix tests after cherry-pick.
ppostma Sep 10, 2018
399c5dd
Merge pull request #2 from KentaaNL/fix-concurrency-problem
twanmaus Sep 10, 2018
08bc4c3
Remove post install message.
ppostma Jan 3, 2019
a015e07
Fix deprecatio warning: remove #has_rdoc.
ppostma Feb 18, 2019
4da5851
Support ActiveRecord 5.2
seanabrahams Feb 7, 2019
2b86ea7
Merge pull request #3 from KentaaNL/rails-5-2
twanmaus Jun 15, 2021
3c2aa01
Use GitHub Actions for CI.
ppostma Jun 15, 2021
e22667f
Merge pull request #4 from KentaaNL/github-workflows
ppostma Jun 15, 2021
67bbfa8
Update
ppostma Jun 15, 2021
37d29e2
Explain fork fixes
ppostma Jun 15, 2021
a945328
Rails 6.0
ppostma Apr 8, 2022
b77e71e
Attribute was test < Rails 6.0
ppostma Oct 12, 2022
9e1209c
Fix keyword arguments deprecation in Ruby 2.7
ppostma Oct 12, 2022
a35017f
Build only Ruby 2.7
ppostma Oct 12, 2022
ee8b89c
Merge pull request #6 from KentaaNL/rails-6.0
rvd-kentaa Oct 12, 2022
f53a482
Add Rails 6.1 + Ruby 3.0/3.1 to CI
ppostma Oct 12, 2022
0e5ad85
Exclude Rails 5.x from Ruby 3.x
ppostma Oct 12, 2022
7c59e57
Merge pull request #7 from KentaaNL/rails-6.1
rvd-kentaa Oct 12, 2022
b5a0f61
Rename encrypt/decrypt methods
rvd-kentaa Oct 12, 2022
d2314d6
Merge pull request #8 from KentaaNL/rename-encrypt-2
ppostma Oct 12, 2022
e076b9b
Update fork fixes
ppostma Oct 12, 2022
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
31 changes: 31 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
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:
- 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
1 change: 1 addition & 0 deletions .ruby-gemset
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
attr_encrypted
60 changes: 0 additions & 60 deletions .travis.yml

This file was deleted.

30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
# 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.

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:

* Optional update encrypted attributes only when values changed (#1)
* Fix concurrency problem (#2)
* Support ActiveRecord 5.2, 6.0 and 6.1 (#3, #6, #7)
* Rename encrypt/decrypt methods (#8)

## Installation

Add attr_encrypted to your gemfile:

```ruby
gem "attr_encrypted", "~> 3.0.0"
gem "attr_encrypted", github: "KentaaNL/attr_encrypted"
```

Then install the gem:
Expand All @@ -22,7 +29,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 @@ -145,7 +152,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.
Expand Down Expand Up @@ -322,6 +330,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

Expand Down Expand Up @@ -357,7 +375,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

Expand Down
12 changes: 2 additions & 10 deletions attr_encrypted.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -38,10 +37,10 @@ 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')
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')
Expand All @@ -50,19 +49,12 @@ 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.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')

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
56 changes: 40 additions & 16 deletions lib/attr_encrypted.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -158,12 +161,14 @@ 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|
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(attribute, value))
instance_variable_set("@#{attribute}", value)
end
end

define_method("#{attribute}?") do
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -232,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]
Expand All @@ -258,8 +264,8 @@ def decrypt(attribute, encrypted_value, options = {})
# attr_encrypted :email
# end
#
# encrypted_email = User.encrypt(:email, '[email protected]')
def encrypt(attribute, value, options = {})
# encrypted_email = User.encrypt_attribute(:email, '[email protected]')
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
Expand Down Expand Up @@ -301,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
Expand All @@ -322,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
Expand All @@ -343,22 +353,36 @@ def decrypt(attribute, encrypted_value)
# end
#
# @user = User.new('some-secret-key')
# @user.encrypt(:email, '[email protected]')
def encrypt(attribute, value)
# @user.encrypt_attribute(:email, '[email protected]')
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
# 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

# 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
Expand Down
20 changes: 18 additions & 2 deletions lib/attr_encrypted/adapters/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ 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
attribute_changed?(attr)
attribute_changed?(attr)
end
end

Expand All @@ -73,6 +73,22 @@ 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
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)
end
##
attribute_will_change!(attr) if value != __send__(attr)
__send__("#{attr}_without_dirtiness=", value)
end
Expand Down
Loading