attr_encrypted 3.1.0 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/CI.yml +43 -0
- data/CHANGELOG.md +47 -18
- data/README.md +16 -10
- data/Rakefile +2 -0
- data/attr_encrypted.gemspec +8 -21
- data/checksum/attr_encrypted-3.1.0.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.1.0.gem.sha512 +1 -0
- data/lib/attr_encrypted/adapters/active_record.rb +31 -18
- data/lib/attr_encrypted/adapters/sequel.rb +3 -1
- data/lib/attr_encrypted/version.rb +3 -1
- data/lib/attr_encrypted.rb +42 -30
- data/test/active_record_test.rb +55 -56
- data/test/attr_encrypted_test.rb +31 -10
- data/test/compatibility_test.rb +2 -0
- data/test/legacy_active_record_test.rb +11 -3
- data/test/legacy_attr_encrypted_test.rb +8 -6
- data/test/legacy_compatibility_test.rb +2 -0
- data/test/legacy_sequel_test.rb +2 -0
- data/test/run.sh +15 -7
- data/test/sequel_test.rb +2 -0
- data/test/test_helper.rb +10 -13
- metadata +35 -80
- checksums.yaml.gz.sig +0 -1
- data/.travis.yml +0 -60
- data/certs/saghaulor.pem +0 -21
- data/lib/attr_encrypted/adapters/data_mapper.rb +0 -22
- data/test/data_mapper_test.rb +0 -57
- data/test/legacy_data_mapper_test.rb +0 -55
- data.tar.gz.sig +0 -0
- metadata.gz.sig +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5a992ab4061d1d05c6de0891accee520006d545d727be49bd80c09ea4d55b44a
|
4
|
+
data.tar.gz: c6dcfb039445ce7128e0c610d5ca6a4efce02ed195fe754fbc947a99f7b8cef7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 62299d9d34f907ef99464ca41c5de51f9b05d440b2210ba377e05ac4fe02978fa58893cdb0ce002cd25216dace37c5f67b8a8c97ef76259066ee6fc57b93fd08
|
7
|
+
data.tar.gz: f081aaebe945a24d2aff5fea11e671b700b8bf5a6513d36252c96f38e5134448efb9660d0fb6843418d926dcb3821ae1e9c23d2f2551ff2a0b261a678a6bb8f5
|
@@ -0,0 +1,43 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- master
|
7
|
+
pull_request:
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
name: Ruby ${{ matrix.ruby }} / ActiveRecord ${{ matrix.active_record }}
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
strategy:
|
14
|
+
fail-fast: false
|
15
|
+
matrix:
|
16
|
+
active_record:
|
17
|
+
- '6.0'
|
18
|
+
- '6.1'
|
19
|
+
- '7.0'
|
20
|
+
- '7.1'
|
21
|
+
ruby:
|
22
|
+
- '2.7'
|
23
|
+
- '3.0'
|
24
|
+
- '3.1'
|
25
|
+
- '3.2'
|
26
|
+
include:
|
27
|
+
- active_record: '5.1'
|
28
|
+
ruby: '2.7'
|
29
|
+
- active_record: '5.2'
|
30
|
+
ruby: '2.7'
|
31
|
+
- active_record: '7.1'
|
32
|
+
ruby: '3.3'
|
33
|
+
env:
|
34
|
+
ACTIVERECORD: ${{ matrix.active_record }}
|
35
|
+
steps:
|
36
|
+
- uses: actions/checkout@v3
|
37
|
+
- name: Set up Ruby
|
38
|
+
uses: ruby/setup-ruby@v1
|
39
|
+
with:
|
40
|
+
ruby-version: ${{ matrix.ruby }}
|
41
|
+
bundler-cache: true
|
42
|
+
- run: |
|
43
|
+
bundle exec rake test
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,19 @@
|
|
1
|
-
# attr_encrypted
|
1
|
+
# attr_encrypted
|
2
|
+
|
3
|
+
## 4.1.0
|
4
|
+
|
5
|
+
* Changed: Dropped support for `datamapper` which has not had a release since October 2011. This is in an attempt to make
|
6
|
+
maintenance and testing easier moving forward.
|
7
|
+
|
8
|
+
## 4.0.0
|
9
|
+
|
10
|
+
* Added: Support for Ruby >= 3.0.
|
11
|
+
* Added: Rails 7 support.
|
12
|
+
* Changed: Using `#encrypted_attributes` is no longer supported. Instead, use `#attr_encrypted_encrypted_attributes` to avoid
|
13
|
+
collision with Active Record 7 native encryption.
|
14
|
+
|
15
|
+
## 3.1.0
|
2
16
|
|
3
|
-
## 3.1.0 ##
|
4
17
|
* Added: Abitilty to encrypt empty values. (@tamird)
|
5
18
|
* Added: MIT license
|
6
19
|
* Added: MRI 2.5.x support (@saghaulor)
|
@@ -11,23 +24,28 @@
|
|
11
24
|
* Fixed: Only check empty on strings, allows for encrypting non-string type objects
|
12
25
|
* Fixed: Fixed how accessors for db columns are defined in the ActiveRecord adapter, preventing premature definition. (@nagachika)
|
13
26
|
|
14
|
-
## 3.0.3
|
27
|
+
## 3.0.3
|
28
|
+
|
15
29
|
* Fixed: attr_was would decrypt the attribute upon every call. This is inefficient and introduces problems when the options change between decrypting an old value and encrypting a new value; for example, when rotating the encryption key. As such, the new approach caches the decrypted value of the old encrypted value such that the old options are no longer needed. (@johnny-lai) (@saghaulor)
|
16
30
|
|
17
|
-
## 3.0.2
|
31
|
+
## 3.0.2
|
32
|
+
|
18
33
|
* Changed: Removed alias_method_chain for compatibility with Rails v5.x (@grosser)
|
19
34
|
* Changed: Updated Travis build matrix to include Rails 5. (@saghaulor) (@connorshea)
|
20
35
|
* Changed: Removed `.silence_stream` from tests as it has been removed from Rails 5. (@sblackstone)
|
21
36
|
|
22
|
-
## 3.0.1
|
37
|
+
## 3.0.1
|
38
|
+
|
23
39
|
* Fixed: attr_was method no longer calls undefined methods. (@saghaulor)
|
24
40
|
|
25
|
-
## 3.0.0
|
41
|
+
## 3.0.0
|
42
|
+
|
26
43
|
* Changed: Updated gemspec to use Encryptor v3.0.0. (@saghaulor)
|
27
44
|
* Changed: Updated README with instructions related to moving from v2.0.0 to v3.0.0. (@saghaulor)
|
28
45
|
* Fixed: ActiveModel::Dirty methods in the ActiveRecord adapter. (@saghaulor)
|
29
46
|
|
30
|
-
## 2.0.0
|
47
|
+
## 2.0.0
|
48
|
+
|
31
49
|
* Added: Now using Encryptor v2.0.0 (@saghaulor)
|
32
50
|
* Added: Options are copied to the instance. (@saghaulor)
|
33
51
|
* Added: Operation option is set during encryption/decryption to allow options to be evaluated in the context of the current operation. (@saghaulor)
|
@@ -48,51 +66,62 @@
|
|
48
66
|
* Removed: Support for Rails < 3.x (@saghaulor)
|
49
67
|
* Removed: Unnecessary use of `alias_method` from ActiveRecord adapter. (@saghaulor)
|
50
68
|
|
51
|
-
## 1.4.0
|
69
|
+
## 1.4.0
|
70
|
+
|
52
71
|
* Added: ActiveModel::Dirty#attribute_was (@saghaulor)
|
53
72
|
* Added: ActiveModel::Dirty#attribute_changed? (@mwean)
|
54
73
|
|
55
|
-
## 1.3.5
|
74
|
+
## 1.3.5
|
75
|
+
|
56
76
|
* Changed: Fixed gemspec to explicitly depend on Encryptor v1.3.0 (@saghaulor)
|
57
77
|
* Fixed: Evaluate `:mode` option as a symbol or proc. (@cheynewallace)
|
58
78
|
|
59
|
-
## 1.3.4
|
79
|
+
## 1.3.4
|
80
|
+
|
60
81
|
* Added: ActiveRecord::Base.reload support. (@rcook)
|
61
82
|
* Fixed: ActiveRecord adapter no longer forces attribute hashes to be string-keyed. (@tamird)
|
62
83
|
* Fixed: Mass assignment protection in ActiveRecord 4. (@tamird)
|
63
84
|
* Changed: Now using rubygems over https. (@tamird)
|
64
85
|
* Changed: Let ActiveRecord define attribute methods. (@saghaulor)
|
65
86
|
|
66
|
-
## 1.3.3
|
87
|
+
## 1.3.3
|
88
|
+
|
67
89
|
* Added: Alias attr_encryptor and attr_encrpted. (@Billy Monk)
|
68
90
|
|
69
|
-
## 1.3.2
|
91
|
+
## 1.3.2
|
92
|
+
|
70
93
|
* Fixed: Bug regarding strong parameters. (@S. Brent Faulkner)
|
71
94
|
* Fixed: Bug regarding loading per instance IV and salt. (@S. Brent Faulkner)
|
72
95
|
* Fixed: Bug regarding assigning nil. (@S. Brent Faulkner)
|
73
96
|
* Added: Support for protected attributes. (@S. Brent Faulkner)
|
74
97
|
* Added: Support for ActiveRecord 4. (@S. Brent Faulkner)
|
75
98
|
|
76
|
-
## 1.3.1
|
99
|
+
## 1.3.1
|
100
|
+
|
77
101
|
* Added: Support for Rails 2.3.x and 3.1.x. (@S. Brent Faulkner)
|
78
102
|
|
79
|
-
## 1.3.0
|
103
|
+
## 1.3.0
|
104
|
+
|
80
105
|
* Fixed: Serialization bug. (@Billy Monk)
|
81
106
|
* Added: Support for :per_attribute_iv_and_salt mode. (@rcook)
|
82
107
|
* Fixed: Added dependencies to gemspec. (@jmazzi)
|
83
108
|
|
84
|
-
## 1.2.1
|
109
|
+
## 1.2.1
|
110
|
+
|
85
111
|
* Added: Force encoding when not marshaling. (@mosaicxm)
|
86
112
|
* Fixed: Issue specifying multiple attributes on the same line. (@austintaylor)
|
87
113
|
* Added: Typecasting to String before encryption (@shuber)
|
88
114
|
* Added: `"#{attribute}?"` method. (@shuber)
|
89
115
|
|
90
|
-
## 1.2.0
|
116
|
+
## 1.2.0
|
117
|
+
|
91
118
|
* Changed: General code refactoring (@shuber)
|
92
119
|
|
93
|
-
## 1.1.2
|
120
|
+
## 1.1.2
|
121
|
+
|
94
122
|
* No significant changes
|
95
123
|
|
96
|
-
## 1.1.1
|
124
|
+
## 1.1.1
|
125
|
+
|
97
126
|
* Changled: Updated README. (@shuber)
|
98
127
|
* Added: `before_type_cast` alias to ActiveRecord adapter. (@shuber)
|
data/README.md
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
# attr_encrypted
|
2
|
-
[![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)
|
3
2
|
|
4
|
-
|
3
|
+
![workflow](https://github.com/attr-encrypted/attr_encrypted/actions/workflows/CI.yml/badge.svg) [![Gem Version](https://badge.fury.io/rb/attr_encrypted.svg)](https://badge.fury.io/rb/attr_encrypted)
|
5
4
|
|
6
|
-
|
5
|
+
Generates attr_accessors that transparently encrypt and decrypt attributes.
|
7
6
|
|
7
|
+
It works with ANY class, however, you get a few extra features when you're using it with `ActiveRecord` or `Sequel`.
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
11
11
|
Add attr_encrypted to your gemfile:
|
12
12
|
|
13
13
|
```ruby
|
14
|
-
gem "attr_encrypted"
|
14
|
+
gem "attr_encrypted"
|
15
15
|
```
|
16
16
|
|
17
17
|
Then install the gem:
|
@@ -22,7 +22,7 @@ Then install the gem:
|
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
25
|
-
If you're using an ORM like `ActiveRecord
|
25
|
+
If you're using an ORM like `ActiveRecord` or `Sequel`, using attr_encrypted is easy:
|
26
26
|
|
27
27
|
```ruby
|
28
28
|
class User
|
@@ -87,6 +87,12 @@ Create or modify the table that your model uses to add a column with the `encryp
|
|
87
87
|
|
88
88
|
You can use a string or binary column type. (See the encode option section below for more info)
|
89
89
|
|
90
|
+
If you use the same key for each record, add a unique index on the IV. Repeated IVs with AES-GCM (the default algorithm) allow an attacker to recover the key.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
add_index :users, :encrypted_ssn_iv, unique: true
|
94
|
+
```
|
95
|
+
|
90
96
|
### Specifying the encrypted attribute name
|
91
97
|
|
92
98
|
By default, the encrypted attribute name is `encrypted_#{attribute}` (e.g. `attr_encrypted :email` would create an attribute named `encrypted_email`). So, if you're storing the encrypted attribute in the database, you need to make sure the `encrypted_#{attribute}` field exists in your table. You have a couple of options if you want to name your attribute or db column something else, see below for more details.
|
@@ -357,7 +363,7 @@ NOTE: This only works if all records are encrypted with the same encryption key
|
|
357
363
|
__NOTE: This feature is deprecated and will be removed in the next major release.__
|
358
364
|
|
359
365
|
|
360
|
-
###
|
366
|
+
### Sequel
|
361
367
|
|
362
368
|
#### Default options
|
363
369
|
|
@@ -403,7 +409,7 @@ Then modify your models using attr\_encrypted to account for the changes in defa
|
|
403
409
|
|
404
410
|
## Upgrading from attr_encrypted v2.x to v3.x
|
405
411
|
|
406
|
-
A bug was discovered in Encryptor v2.0.0 that
|
412
|
+
A bug was discovered in Encryptor v2.0.0 that incorrectly set the IV when using an AES-\*-GCM algorithm. Unfornately fixing this major security issue results in the inability to decrypt records encrypted using an AES-*-GCM algorithm from Encryptor v2.0.0. Please see [Upgrading to Encryptor v3.0.0](https://github.com/attr-encrypted/encryptor#upgrading-from-v200-to-v300) for more info.
|
407
413
|
|
408
414
|
It is strongly advised that you re-encrypt your data encrypted with Encryptor v2.0.0. However, you'll have to take special care to re-encrypt. To decrypt data encrypted with Encryptor v2.0.0 using an AES-\*-GCM algorithm you can use the `:v2_gcm_iv` option.
|
409
415
|
|
@@ -414,7 +420,7 @@ It is recommended that you implement a strategy to insure that you do not mix th
|
|
414
420
|
attr_encrypted :ssn, key: :encryption_key, v2_gcm_iv: is_decrypting?(:ssn)
|
415
421
|
|
416
422
|
def is_decrypting?(attribute)
|
417
|
-
|
423
|
+
attr_encrypted_encrypted_attributes[attribute][:operation] == :decrypting
|
418
424
|
end
|
419
425
|
end
|
420
426
|
|
@@ -431,7 +437,7 @@ It is recommended that you implement a strategy to insure that you do not mix th
|
|
431
437
|
While choosing to encrypt at the attribute level is the most secure solution, it is not without drawbacks. Namely, you cannot search the encrypted data, and because you can't search it, you can't index it either. You also can't use joins on the encrypted data. Data that is securely encrypted is effectively noise. So any operations that rely on the data not being noise will not work. If you need to do any of the aforementioned operations, please consider using database and file system encryption along with transport encryption as it moves through your stack.
|
432
438
|
|
433
439
|
#### Data leaks
|
434
|
-
Please also consider where your data leaks. If you're using attr_encrypted with Rails, it's highly likely that this data will enter your app as a request parameter. You'll want to be sure that you're filtering your request params from
|
440
|
+
Please also consider where your data leaks. If you're using attr_encrypted with Rails, it's highly likely that this data will enter your app as a request parameter. You'll want to be sure that you're filtering your request params from your logs or else your data is sitting in the clear in your logs. [Parameter Filtering in Rails](http://apidock.com/rails/ActionDispatch/Http/FilterParameters) Please also consider other possible leak points.
|
435
441
|
|
436
442
|
#### Storage requirements
|
437
443
|
When storing your encrypted data, please consider the length requirements of the db column that you're storing the cipher text in. Older versions of Mysql attempt to 'help' you by truncating strings that are too large for the column. When this happens, you will not be able to decrypt your data. [MySQL Strict Trans](http://www.davidpashley.com/2009/02/15/silently-truncated/)
|
@@ -440,7 +446,7 @@ When storing your encrypted data, please consider the length requirements of the
|
|
440
446
|
It is advisable to also store metadata regarding the circumstances of your encrypted data. Namely, you should store information about the key used to encrypt your data, as well as the algorithm. Having this metadata with every record will make key rotation and migrating to a new algorithm signficantly easier. It will allow you to continue to decrypt old data using the information provided in the metadata and new data can be encrypted using your new key and algorithm of choice.
|
441
447
|
|
442
448
|
#### Enforcing the IV as a nonce
|
443
|
-
On a related note, most
|
449
|
+
On a related note, most algorithms require that your IV be unique for every record and key combination. You can enforce this using composite unique indexes on your IV and encryption key name/id column. [RFC 5084](https://tools.ietf.org/html/rfc5084#section-1.5)
|
444
450
|
|
445
451
|
#### Unique key per record
|
446
452
|
Lastly, while the `:per_attribute_iv_and_salt` mode is more secure than `:per_attribute_iv` mode because it uses a unique key per record, it uses a PBKDF function which introduces a huge performance hit (175x slower by my benchmarks). There are other ways of deriving a unique key per record that would be much faster.
|
data/Rakefile
CHANGED
data/attr_encrypted.gemspec
CHANGED
@@ -14,55 +14,42 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.summary = 'Encrypt and decrypt attributes'
|
15
15
|
s.description = 'Generates attr_accessors that encrypt and decrypt attributes transparently'
|
16
16
|
|
17
|
-
s.authors = ['Sean Huber', 'S. Brent Faulkner', 'William Monk', 'Stephen Aghaulor']
|
18
|
-
s.email = ['seah@shuber.io', 'sbfaulkner@gmail.com', 'billy.monk@gmail.com', 'saghaulor@gmail.com']
|
17
|
+
s.authors = ['Sean Huber', 'S. Brent Faulkner', 'William Monk', 'Stephen Aghaulor', 'Josh Branham', 'Mike Vastola']
|
18
|
+
s.email = ['seah@shuber.io', 'sbfaulkner@gmail.com', 'billy.monk@gmail.com', 'saghaulor@gmail.com', 'josh.php@gmail.com', 'Mike@Vasto.la']
|
19
19
|
s.homepage = 'http://github.com/attr-encrypted/attr_encrypted'
|
20
20
|
s.license = 'MIT'
|
21
21
|
|
22
|
-
s.has_rdoc = false
|
23
|
-
s.rdoc_options = ['--line-numbers', '--inline-source', '--main', 'README.rdoc']
|
24
|
-
|
25
22
|
s.require_paths = ['lib']
|
26
23
|
|
27
24
|
s.files = `git ls-files`.split("\n")
|
28
25
|
s.test_files = `git ls-files -- test/*`.split("\n")
|
29
26
|
|
30
|
-
s.required_ruby_version = '>= 2.
|
27
|
+
s.required_ruby_version = '>= 2.6.0'
|
31
28
|
|
32
29
|
s.add_dependency('encryptor', ['~> 3.0.0'])
|
33
30
|
# support for testing with specific active record version
|
34
31
|
activerecord_version = if ENV.key?('ACTIVERECORD')
|
35
|
-
"~> #{ENV['ACTIVERECORD']}"
|
32
|
+
"~> #{ENV['ACTIVERECORD']}.0"
|
36
33
|
else
|
37
34
|
'>= 2.0.0'
|
38
35
|
end
|
39
36
|
s.add_development_dependency('activerecord', activerecord_version)
|
40
37
|
s.add_development_dependency('actionpack', activerecord_version)
|
41
|
-
s.add_development_dependency('datamapper')
|
42
38
|
s.add_development_dependency('rake')
|
43
39
|
s.add_development_dependency('minitest')
|
44
40
|
s.add_development_dependency('sequel')
|
45
|
-
if RUBY_VERSION < '2.1.0'
|
46
|
-
s.add_development_dependency('nokogiri', '< 1.7.0')
|
47
|
-
s.add_development_dependency('public_suffix', '< 3.0.0')
|
48
|
-
end
|
49
41
|
if defined?(RUBY_ENGINE) && RUBY_ENGINE.to_sym == :jruby
|
50
42
|
s.add_development_dependency('activerecord-jdbcsqlite3-adapter')
|
51
43
|
s.add_development_dependency('jdbc-sqlite3', '< 3.8.7') # 3.8.7 is nice and broke
|
52
44
|
else
|
53
|
-
s.add_development_dependency('sqlite3')
|
45
|
+
s.add_development_dependency('sqlite3', '= 1.5.4')
|
54
46
|
end
|
55
47
|
s.add_development_dependency('dm-sqlite-adapter')
|
48
|
+
s.add_development_dependency('pry')
|
56
49
|
s.add_development_dependency('simplecov')
|
57
50
|
s.add_development_dependency('simplecov-rcov')
|
58
|
-
s.add_development_dependency("codeclimate-test-reporter", '<= 0.6.0')
|
59
|
-
|
60
|
-
s.cert_chain = ['certs/saghaulor.pem']
|
61
|
-
s.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/
|
62
51
|
|
63
|
-
s.post_install_message = "\n\n\nWARNING:
|
64
|
-
|
65
|
-
This bug was fixed but introduced breaking changes between v2.x and v3.x.\n
|
66
|
-
Please see the README for more information regarding upgrading to attr_encrypted v3.0.0.\n\n\n"
|
52
|
+
s.post_install_message = "\n\n\nWARNING: Using `#encrypted_attributes` is no longer supported. Instead, use `#attr_encrypted_encrypted_attributes` to avoid
|
53
|
+
collision with Active Record 7 native encryption.\n\n\n"
|
67
54
|
|
68
55
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
4f0682604714ed4599cf00771ad27e82f0b51b0ed8644af51a43d21fbe129b59
|
@@ -0,0 +1 @@
|
|
1
|
+
dc757a50c53175dc05adbfccb25b536f7f1a060c7bd626bfc72ff577479c6fe28d3b65747033276bd4bce3dad741b67235f3e01ea61409f6c67959da65df445b
|
@@ -1,7 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
if defined?(ActiveRecord::Base)
|
2
4
|
module AttrEncrypted
|
3
5
|
module Adapters
|
4
6
|
module ActiveRecord
|
7
|
+
RAILS_VERSION = Gem::Version.new(::ActiveRecord::VERSION::STRING).freeze
|
8
|
+
|
5
9
|
def self.extended(base) # :nodoc:
|
6
10
|
base.class_eval do
|
7
11
|
|
@@ -9,7 +13,7 @@ if defined?(ActiveRecord::Base)
|
|
9
13
|
alias_method :reload_without_attr_encrypted, :reload
|
10
14
|
def reload(*args, &block)
|
11
15
|
result = reload_without_attr_encrypted(*args, &block)
|
12
|
-
self.class.
|
16
|
+
self.class.attr_encrypted_encrypted_attributes.keys.each do |attribute_name|
|
13
17
|
instance_variable_set("@#{attribute_name}", nil)
|
14
18
|
end
|
15
19
|
result
|
@@ -25,12 +29,12 @@ if defined?(ActiveRecord::Base)
|
|
25
29
|
def perform_attribute_assignment(method, new_attributes, *args)
|
26
30
|
return if new_attributes.blank?
|
27
31
|
|
28
|
-
send method, new_attributes.reject { |k, _| self.class.
|
29
|
-
send method, new_attributes.reject { |k, _| !self.class.
|
32
|
+
send method, new_attributes.reject { |k, _| self.class.attr_encrypted_encrypted_attributes.key?(k.to_sym) }, *args
|
33
|
+
send method, new_attributes.reject { |k, _| !self.class.attr_encrypted_encrypted_attributes.key?(k.to_sym) }, *args
|
30
34
|
end
|
31
35
|
private :perform_attribute_assignment
|
32
36
|
|
33
|
-
if ::
|
37
|
+
if Gem::Requirement.new('> 3.1').satisfied_by?(RAILS_VERSION)
|
34
38
|
alias_method :assign_attributes_without_attr_encrypted, :assign_attributes
|
35
39
|
def assign_attributes(*args)
|
36
40
|
perform_attribute_assignment :assign_attributes_without_attr_encrypted, *args
|
@@ -51,21 +55,15 @@ if defined?(ActiveRecord::Base)
|
|
51
55
|
super
|
52
56
|
options = attrs.extract_options!
|
53
57
|
attr = attrs.pop
|
54
|
-
attribute attr
|
55
|
-
options.merge!
|
58
|
+
attribute attr
|
59
|
+
options.merge! attr_encrypted_encrypted_attributes[attr]
|
56
60
|
|
57
61
|
define_method("#{attr}_was") do
|
58
62
|
attribute_was(attr)
|
59
63
|
end
|
60
64
|
|
61
|
-
|
62
|
-
|
63
|
-
attribute_changed?(attr, options)
|
64
|
-
end
|
65
|
-
else
|
66
|
-
define_method("#{attr}_changed?") do
|
67
|
-
attribute_changed?(attr)
|
68
|
-
end
|
65
|
+
define_method("#{attr}_changed?") do |options = {}|
|
66
|
+
attribute_changed?(attr, **options)
|
69
67
|
end
|
70
68
|
|
71
69
|
define_method("#{attr}_change") do
|
@@ -73,6 +71,19 @@ if defined?(ActiveRecord::Base)
|
|
73
71
|
end
|
74
72
|
|
75
73
|
define_method("#{attr}_with_dirtiness=") do |value|
|
74
|
+
## Source: https://github.com/priyankatapar/attr_encrypted/commit/7e8702bd5418c927a39d8dd72c0adbea522d5663
|
75
|
+
# In ActiveRecord 5.2+, due to changes to the way virtual
|
76
|
+
# attributes are handled, @attributes[attr].value is nil which
|
77
|
+
# breaks attribute_was. Setting it here returns us to the expected
|
78
|
+
# behavior.
|
79
|
+
if RAILS_VERSION >= Gem::Version.new(5.2)
|
80
|
+
# This is needed support attribute_was before a record has
|
81
|
+
# been saved
|
82
|
+
@attributes.write_cast_value(attr.to_s, __send__(attr)) if value != __send__(attr)
|
83
|
+
# This is needed to support attribute_was after a record has
|
84
|
+
# been saved
|
85
|
+
@attributes.write_from_user(attr.to_s, value) if value != __send__(attr)
|
86
|
+
end
|
76
87
|
attribute_will_change!(attr) if value != __send__(attr)
|
77
88
|
__send__("#{attr}_without_dirtiness=", value)
|
78
89
|
end
|
@@ -120,10 +131,10 @@ if defined?(ActiveRecord::Base)
|
|
120
131
|
if match = /^(find|scoped)_(all_by|by)_([_a-zA-Z]\w*)$/.match(method.to_s)
|
121
132
|
attribute_names = match.captures.last.split('_and_')
|
122
133
|
attribute_names.each_with_index do |attribute, index|
|
123
|
-
if attr_encrypted?(attribute) &&
|
134
|
+
if attr_encrypted?(attribute) && attr_encrypted_encrypted_attributes[attribute.to_sym][:mode] == :single_iv_and_salt
|
124
135
|
args[index] = send("encrypt_#{attribute}", args[index])
|
125
136
|
warn "DEPRECATION WARNING: This feature will be removed in the next major release."
|
126
|
-
attribute_names[index] =
|
137
|
+
attribute_names[index] = attr_encrypted_encrypted_attributes[attribute.to_sym][:attribute]
|
127
138
|
end
|
128
139
|
end
|
129
140
|
method = "#{match.captures[0]}_#{match.captures[1]}_#{attribute_names.join('_and_')}".to_sym
|
@@ -134,6 +145,8 @@ if defined?(ActiveRecord::Base)
|
|
134
145
|
end
|
135
146
|
end
|
136
147
|
|
137
|
-
|
138
|
-
|
148
|
+
ActiveSupport.on_load(:active_record) do
|
149
|
+
extend AttrEncrypted
|
150
|
+
extend AttrEncrypted::Adapters::ActiveRecord
|
151
|
+
end
|
139
152
|
end
|
data/lib/attr_encrypted.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'encryptor'
|
2
4
|
|
3
5
|
# Adds attr_accessors that encrypt and decrypt an object's attributes
|
@@ -8,7 +10,7 @@ module AttrEncrypted
|
|
8
10
|
base.class_eval do
|
9
11
|
include InstanceMethods
|
10
12
|
attr_writer :attr_encrypted_options
|
11
|
-
@attr_encrypted_options, @
|
13
|
+
@attr_encrypted_options, @attr_encrypted_encrypted_attributes = {}, {}
|
12
14
|
end
|
13
15
|
end
|
14
16
|
|
@@ -52,7 +54,7 @@ module AttrEncrypted
|
|
52
54
|
# string instead of just 'true'. See
|
53
55
|
# http://www.ruby-doc.org/core/classes/Array.html#M002245
|
54
56
|
# for more encoding directives.
|
55
|
-
# Defaults to false unless you're using it with ActiveRecord
|
57
|
+
# Defaults to false unless you're using it with ActiveRecord or Sequel.
|
56
58
|
#
|
57
59
|
# encode_iv: Defaults to true.
|
58
60
|
|
@@ -158,11 +160,11 @@ module AttrEncrypted
|
|
158
160
|
end
|
159
161
|
|
160
162
|
define_method(attribute) do
|
161
|
-
instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}",
|
163
|
+
instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", attr_encrypted_decrypt(attribute, send(encrypted_attribute_name)))
|
162
164
|
end
|
163
165
|
|
164
166
|
define_method("#{attribute}=") do |value|
|
165
|
-
send("#{encrypted_attribute_name}=",
|
167
|
+
send("#{encrypted_attribute_name}=", attr_encrypted_encrypt(attribute, value))
|
166
168
|
instance_variable_set("@#{attribute}", value)
|
167
169
|
end
|
168
170
|
|
@@ -171,7 +173,7 @@ module AttrEncrypted
|
|
171
173
|
value.respond_to?(:empty?) ? !value.empty? : !!value
|
172
174
|
end
|
173
175
|
|
174
|
-
|
176
|
+
self.attr_encrypted_encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name)
|
175
177
|
end
|
176
178
|
end
|
177
179
|
|
@@ -221,7 +223,7 @@ module AttrEncrypted
|
|
221
223
|
# User.attr_encrypted?(:name) # false
|
222
224
|
# User.attr_encrypted?(:email) # true
|
223
225
|
def attr_encrypted?(attribute)
|
224
|
-
|
226
|
+
attr_encrypted_encrypted_attributes.has_key?(attribute.to_sym)
|
225
227
|
end
|
226
228
|
|
227
229
|
# Decrypts a value for the attribute specified
|
@@ -232,9 +234,9 @@ module AttrEncrypted
|
|
232
234
|
# attr_encrypted :email
|
233
235
|
# end
|
234
236
|
#
|
235
|
-
# email = User.
|
236
|
-
def
|
237
|
-
options =
|
237
|
+
# email = User.attr_encrypted_decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
238
|
+
def attr_encrypted_decrypt(attribute, encrypted_value, options = {})
|
239
|
+
options = attr_encrypted_encrypted_attributes[attribute.to_sym].merge(options)
|
238
240
|
if options[:if] && !options[:unless] && not_empty?(encrypted_value)
|
239
241
|
encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
|
240
242
|
value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value))
|
@@ -258,9 +260,9 @@ module AttrEncrypted
|
|
258
260
|
# attr_encrypted :email
|
259
261
|
# end
|
260
262
|
#
|
261
|
-
# encrypted_email = User.
|
262
|
-
def
|
263
|
-
options =
|
263
|
+
# encrypted_email = User.attr_encrypted_encrypt(:email, 'test@example.com')
|
264
|
+
def attr_encrypted_encrypt(attribute, value, options = {})
|
265
|
+
options = attr_encrypted_encrypted_attributes[attribute.to_sym].merge(options)
|
264
266
|
if options[:if] && !options[:unless] && (options[:allow_empty_value] || not_empty?(value))
|
265
267
|
value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
|
266
268
|
encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(value: value))
|
@@ -284,9 +286,9 @@ module AttrEncrypted
|
|
284
286
|
# attr_encrypted :email, key: 'my secret key'
|
285
287
|
# end
|
286
288
|
#
|
287
|
-
# User.
|
288
|
-
def
|
289
|
-
@
|
289
|
+
# User.attr_encrypted_encrypted_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } }
|
290
|
+
def attr_encrypted_encrypted_attributes
|
291
|
+
@attr_encrypted_encrypted_attributes ||= superclass.attr_encrypted_encrypted_attributes.dup
|
290
292
|
end
|
291
293
|
|
292
294
|
# Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
|
@@ -301,7 +303,7 @@ module AttrEncrypted
|
|
301
303
|
# User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
|
302
304
|
def method_missing(method, *arguments, &block)
|
303
305
|
if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3)
|
304
|
-
send($1, $3, *arguments)
|
306
|
+
send("attr_encrypted_#{$1}", $3, *arguments)
|
305
307
|
else
|
306
308
|
super
|
307
309
|
end
|
@@ -323,10 +325,10 @@ module AttrEncrypted
|
|
323
325
|
#
|
324
326
|
# @user = User.new('some-secret-key')
|
325
327
|
# @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
326
|
-
def
|
327
|
-
|
328
|
-
|
329
|
-
self.class.
|
328
|
+
def attr_encrypted_decrypt(attribute, encrypted_value)
|
329
|
+
attr_encrypted_encrypted_attributes[attribute.to_sym][:operation] = :decrypting
|
330
|
+
attr_encrypted_encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value)
|
331
|
+
self.class.attr_encrypted_decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
|
330
332
|
end
|
331
333
|
|
332
334
|
# Encrypts a value for the attribute specified using options evaluated in the current object's scope
|
@@ -343,18 +345,22 @@ module AttrEncrypted
|
|
343
345
|
# end
|
344
346
|
#
|
345
347
|
# @user = User.new('some-secret-key')
|
346
|
-
# @user.
|
347
|
-
def
|
348
|
-
|
349
|
-
|
350
|
-
self.class.
|
348
|
+
# @user.attr_encrypted_encrypt(:email, 'test@example.com')
|
349
|
+
def attr_encrypted_encrypt(attribute, value)
|
350
|
+
attr_encrypted_encrypted_attributes[attribute.to_sym][:operation] = :encrypting
|
351
|
+
attr_encrypted_encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(value)
|
352
|
+
self.class.attr_encrypted_encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
|
351
353
|
end
|
352
354
|
|
353
355
|
# Copies the class level hash of encrypted attributes with virtual attribute names as keys
|
354
356
|
# and their corresponding options as values to the instance
|
355
357
|
#
|
356
|
-
def
|
357
|
-
@
|
358
|
+
def attr_encrypted_encrypted_attributes
|
359
|
+
@attr_encrypted_encrypted_attributes ||= begin
|
360
|
+
duplicated= {}
|
361
|
+
self.class.attr_encrypted_encrypted_attributes.map { |key, value| duplicated[key] = value.dup }
|
362
|
+
duplicated
|
363
|
+
end
|
358
364
|
end
|
359
365
|
|
360
366
|
protected
|
@@ -362,15 +368,21 @@ module AttrEncrypted
|
|
362
368
|
# Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
|
363
369
|
def evaluated_attr_encrypted_options_for(attribute)
|
364
370
|
evaluated_options = Hash.new
|
365
|
-
|
366
|
-
|
367
|
-
|
371
|
+
attributes = attr_encrypted_encrypted_attributes[attribute.to_sym]
|
372
|
+
attribute_option_value = attributes[:attribute]
|
373
|
+
|
374
|
+
[:if, :unless, :value_present, :allow_empty_value].each do |option|
|
375
|
+
evaluated_options[option] = evaluate_attr_encrypted_option(attributes[option])
|
368
376
|
end
|
369
377
|
|
370
378
|
evaluated_options[:attribute] = attribute_option_value
|
371
379
|
|
372
380
|
evaluated_options.tap do |options|
|
373
381
|
if options[:if] && !options[:unless] && options[:value_present] || options[:allow_empty_value]
|
382
|
+
(attributes.keys - evaluated_options.keys).each do |option|
|
383
|
+
options[option] = evaluate_attr_encrypted_option(attributes[option])
|
384
|
+
end
|
385
|
+
|
374
386
|
unless options[:mode] == :single_iv_and_salt
|
375
387
|
load_iv_for_attribute(attribute, options)
|
376
388
|
end
|