Skip to content

Commit

Permalink
Optional update encrypted attributes only when values changed
Browse files Browse the repository at this point in the history
  • Loading branch information
Twan Maus committed May 16, 2023
1 parent dee8d41 commit b33467a
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 44 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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', '3.2']
rails-version: ['5.1.1', '5.2.8', '6.0.6', '6.1.7', '7.0.4']
exclude:
- ruby-version: 2.7
rails-version: 7.0.4
- ruby-version: 3.0
rails-version: 5.1.1
- ruby-version: 3.0
rails-version: 5.2.8
- ruby-version: 3.1
rails-version: 5.1.1
- ruby-version: 3.1
rails-version: 5.2.8
- ruby-version: 3.2
rails-version: 5.1.1
- ruby-version: 3.2
rails-version: 5.2.8
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
pkg
Gemfile.lock
coverage
.idea/
1 change: 1 addition & 0 deletions .ruby-gemset
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
attr_encrypted
37 changes: 0 additions & 37 deletions .travis.yml

This file was deleted.

20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
# attr_encrypted

[![Build Status](https://app.travis-ci.com/attr-encrypted/attr_encrypted.svg?branch=master)](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)
[![Build Status](https://github.com/KentaaNL/attr_encrypted_v4/actions/workflows/test.yml/badge.svg)](https://github.com/KentaaNL/attr_encrypted_v4/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` 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)

## Installation

Add attr_encrypted to your gemfile:

```ruby
gem "attr_encrypted"
gem "attr_encrypted", github: "KentaaNL/attr_encrypted_v4"
```

Then install the gem:
Expand Down Expand Up @@ -152,7 +155,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 @@ -329,6 +333,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
24 changes: 20 additions & 4 deletions lib/attr_encrypted.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,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 @@ -164,16 +167,18 @@ def attr_encrypted(*attributes)
end

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

define_method("#{attribute}?") do
value = send(attribute)
value.respond_to?(:empty?) ? !value.empty? : !!value
end

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

Expand Down Expand Up @@ -206,6 +211,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 @@ -324,7 +330,7 @@ module InstanceMethods
# end
#
# @user = User.new('some-secret-key')
# @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
# @user.attr_encrypted_decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
def attr_encrypted_decrypt(attribute, encrypted_value)
attr_encrypted_encrypted_attributes[attribute.to_sym][:operation] = :decrypting
attr_encrypted_encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value)
Expand Down Expand Up @@ -365,6 +371,16 @@ def attr_encrypted_encrypted_attributes

protected

# Determine if unchanged attribute needs to be updated again
def should_update_encrypted_attribute?(attribute, value)
if attr_encrypted_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
38 changes: 38 additions & 0 deletions test/attr_encrypted_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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

Expand Down Expand Up @@ -469,6 +470,43 @@ def test_should_not_by_default_generate_iv_when_attribute_is_empty
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
assert_nil user.encrypted_with_unchanged_false_iv
user.with_unchanged_false = '[email protected]'
old_value = user.encrypted_with_unchanged_false_iv
refute_nil(old_value)
user.with_unchanged_false = '[email protected]'
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
assert_nil user.encrypted_email_iv
user.email = '[email protected]'
refute_nil(old_value = user.encrypted_email_iv)
user.email = '[email protected]'
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
user.with_unchanged_false = '[email protected]'
old_encrypted_with_unchanged_false_iv = user.encrypted_with_unchanged_false_iv
refute_nil old_encrypted_with_unchanged_false_iv
user.with_unchanged_false = '[email protected]'
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: '[email protected]')
old_encrypted_email_iv = user.encrypted_email_iv
refute_nil old_encrypted_email_iv
user.email = '[email protected]'
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'
Expand Down

0 comments on commit b33467a

Please sign in to comment.