crypt_keeper 1.1.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +48 -10
- data/Appraisals +27 -0
- data/README.md +73 -12
- data/crypt_keeper.gemspec +6 -6
- data/gemfiles/activerecord_4_2.gemfile +2 -0
- data/gemfiles/activerecord_5_0.gemfile +9 -0
- data/gemfiles/activerecord_5_1.gemfile +2 -2
- data/gemfiles/activerecord_5_2.gemfile +8 -0
- data/gemfiles/activerecord_6_0.gemfile +8 -0
- data/gemfiles/activerecord_6_1.gemfile +9 -0
- data/lib/crypt_keeper.rb +1 -1
- data/lib/crypt_keeper/helper.rb +80 -2
- data/lib/crypt_keeper/model.rb +1 -1
- data/lib/crypt_keeper/provider/active_support.rb +51 -0
- data/lib/crypt_keeper/provider/mysql_aes_new.rb +1 -1
- data/lib/crypt_keeper/provider/postgres_pgp.rb +9 -3
- data/lib/crypt_keeper/provider/postgres_pgp_public_key.rb +1 -1
- data/lib/crypt_keeper/version.rb +1 -1
- data/spec/crypt_keeper/helper_spec.rb +94 -0
- data/spec/crypt_keeper/model_spec.rb +22 -7
- data/spec/crypt_keeper/provider/active_support_spec.rb +37 -0
- data/spec/crypt_keeper/provider/mysql_aes_new_spec.rb +5 -3
- data/spec/crypt_keeper/provider/postgres_pgp_spec.rb +36 -0
- data/spec/spec_helper.rb +5 -2
- data/spec/support/active_record.rb +1 -0
- metadata +39 -48
- data/lib/crypt_keeper/provider/aes_new.rb +0 -53
- data/spec/crypt_keeper/provider/aes_new_spec.rb +0 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9607ecd6a9297094077cafed8d0f5a29624d4d68f1309630423b53442bdfba5e
|
4
|
+
data.tar.gz: 7f7f07056ba8990311944d6b9bc4c9977354113e7e489b6fd5a2c27b3bb39b56
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 211a89c08366409e4800fb000e7779fbc4162ce6abd43bc8fb3023984b381db2c5e5061d60668d863d42be75ea7cf3e83f751a1aa5dedf0e96891f019fe4dd04
|
7
|
+
data.tar.gz: 2fab3a0eed0e1fbffc65831a469d83a2e5d1ffa748c26c4e7882696de670d5c18bed078323b5e08066e1fda345094b55bde4f85ec782c3017635709eb9984464
|
data/.travis.yml
CHANGED
@@ -1,23 +1,60 @@
|
|
1
1
|
language: ruby
|
2
2
|
|
3
3
|
rvm:
|
4
|
-
- 2.
|
5
|
-
- 2.
|
6
|
-
- 2.
|
4
|
+
- 2.2.10
|
5
|
+
- 2.3.8
|
6
|
+
- 2.4.5
|
7
|
+
- 2.5.3
|
8
|
+
- 2.6.4
|
9
|
+
- 2.7.2
|
7
10
|
|
8
11
|
gemfile:
|
9
12
|
- gemfiles/activerecord_4_2.gemfile
|
13
|
+
- gemfiles/activerecord_5_0.gemfile
|
10
14
|
- gemfiles/activerecord_5_1.gemfile
|
15
|
+
- gemfiles/activerecord_5_2.gemfile
|
16
|
+
- gemfiles/activerecord_6_0.gemfile
|
17
|
+
- gemfiles/activerecord_6_1.gemfile
|
18
|
+
|
11
19
|
|
12
20
|
matrix:
|
13
21
|
exclude:
|
14
|
-
|
15
|
-
|
22
|
+
- rvm: 2.2.10
|
23
|
+
gemfile: gemfiles/activerecord_5_0.gemfile
|
24
|
+
- rvm: 2.2.10
|
25
|
+
gemfile: gemfiles/activerecord_5_1.gemfile
|
26
|
+
- rvm: 2.2.10
|
27
|
+
gemfile: gemfiles/activerecord_6_0.gemfile
|
28
|
+
- rvm: 2.2.10
|
29
|
+
gemfile: gemfiles/activerecord_6_1.gemfile
|
30
|
+
- rvm: 2.3.8
|
31
|
+
gemfile: gemfiles/activerecord_5_0.gemfile
|
32
|
+
- rvm: 2.3.8
|
33
|
+
gemfile: gemfiles/activerecord_5_1.gemfile
|
34
|
+
- rvm: 2.3.8
|
35
|
+
gemfile: gemfiles/activerecord_6_0.gemfile
|
36
|
+
- rvm: 2.3.8
|
37
|
+
gemfile: gemfiles/activerecord_6_1.gemfile
|
38
|
+
- rvm: 2.4.5
|
39
|
+
gemfile: gemfiles/activerecord_5_0.gemfile
|
40
|
+
- rvm: 2.4.5
|
41
|
+
gemfile: gemfiles/activerecord_5_1.gemfile
|
42
|
+
- rvm: 2.4.5
|
43
|
+
gemfile: gemfiles/activerecord_6_0.gemfile
|
44
|
+
- rvm: 2.4.5
|
45
|
+
gemfile: gemfiles/activerecord_6_1.gemfile
|
46
|
+
- rvm: 2.5.3
|
47
|
+
gemfile: gemfiles/activerecord_5_0.gemfile
|
48
|
+
- rvm: 2.5.3
|
49
|
+
gemfile: gemfiles/activerecord_5_1.gemfile
|
50
|
+
- rvm: 2.6.4
|
51
|
+
gemfile: gemfiles/activerecord_5_0.gemfile
|
52
|
+
- rvm: 2.6.4
|
53
|
+
gemfile: gemfiles/activerecord_5_1.gemfile
|
16
54
|
|
17
|
-
|
18
|
-
postgresql
|
19
|
-
|
20
|
-
sudo: false
|
55
|
+
services:
|
56
|
+
- postgresql
|
57
|
+
- mysql
|
21
58
|
|
22
59
|
before_script:
|
23
60
|
- cp spec/default.database.yml spec/database.yml
|
@@ -25,7 +62,8 @@ before_script:
|
|
25
62
|
- psql crypt_keeper_providers -c 'CREATE EXTENSION IF NOT EXISTS pgcrypto;' -U postgres
|
26
63
|
- mysql -e 'CREATE DATABASE crypt_keeper_providers'
|
27
64
|
|
28
|
-
branches:
|
65
|
+
branches:
|
66
|
+
- master
|
29
67
|
|
30
68
|
notifications:
|
31
69
|
email:
|
data/Appraisals
CHANGED
@@ -1,9 +1,36 @@
|
|
1
1
|
appraise "activerecord_4_2" do
|
2
2
|
gem "activerecord", "~> 4.2.0"
|
3
3
|
gem "activesupport", "~> 4.2.0"
|
4
|
+
gem "sqlite3", "~> 1.3.0"
|
5
|
+
|
6
|
+
# otherwise you get "undefined method `new' for BigDecimal:Class" in Ruby 2.7
|
7
|
+
gem "bigdecimal", "1.3.5"
|
4
8
|
end
|
5
9
|
|
6
10
|
appraise "activerecord_5_0" do
|
7
11
|
gem "activerecord", "~> 5.0.0"
|
8
12
|
gem "activesupport", "~> 5.0.0"
|
13
|
+
|
14
|
+
gem "sqlite3", "~> 1.3.6"
|
15
|
+
end
|
16
|
+
|
17
|
+
appraise "activerecord_5_1" do
|
18
|
+
gem "activerecord", "~> 5.1.0"
|
19
|
+
gem "activesupport", "~> 5.1.0"
|
20
|
+
end
|
21
|
+
|
22
|
+
appraise "activerecord_5_2" do
|
23
|
+
gem "activerecord", "~> 5.2.0"
|
24
|
+
gem "activesupport", "~> 5.2.0"
|
25
|
+
end
|
26
|
+
|
27
|
+
appraise "activerecord_6_0" do
|
28
|
+
gem "activerecord", "~> 6.0.0"
|
29
|
+
gem "activesupport", "~> 6.0.0"
|
30
|
+
end
|
31
|
+
|
32
|
+
appraise "activerecord_6_1" do
|
33
|
+
gem "activerecord", "~> 6.1.0"
|
34
|
+
gem "activesupport", "~> 6.1.0"
|
35
|
+
gem "pg", "~> 1.1"
|
9
36
|
end
|
data/README.md
CHANGED
@@ -15,7 +15,7 @@ is a simple class that does 3 things.
|
|
15
15
|
Note: Any options defined using `crypt_keeper` will be passed to `new` as a
|
16
16
|
hash.
|
17
17
|
|
18
|
-
You can see an
|
18
|
+
You can see an example [here](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/active_support.rb).
|
19
19
|
|
20
20
|
## Why?
|
21
21
|
|
@@ -27,7 +27,7 @@ simple that *just works*.
|
|
27
27
|
|
28
28
|
```ruby
|
29
29
|
class MyModel < ActiveRecord::Base
|
30
|
-
crypt_keeper :field, :other_field, :
|
30
|
+
crypt_keeper :field, :other_field, encryptor: :active_support, key: 'super_good_password', salt: 'salt'
|
31
31
|
end
|
32
32
|
|
33
33
|
model = MyModel.new(field: 'sometext')
|
@@ -47,13 +47,49 @@ expected behavior, and has its use cases. An example would be migrating from
|
|
47
47
|
one type of encryption to another. Using `update_column` would allow you to
|
48
48
|
update the content without going through the current encryptor.
|
49
49
|
|
50
|
+
## Generating Keys/Salts
|
51
|
+
|
52
|
+
For encryptors requiring secret keys/salts, you can generate them via
|
53
|
+
`rails secret`:
|
54
|
+
|
55
|
+
```
|
56
|
+
rails secret
|
57
|
+
ef209071bd76143a75eda57b99425da63ce6c2d44581d652aa4302a90dcd7d7e99cbc22091c01a19f93ea484f40b142612f9bf76de8eb2d51ff9b3eb02a7782c
|
58
|
+
```
|
59
|
+
|
60
|
+
Or manually (this is the same implementation that Rails uses):
|
61
|
+
|
62
|
+
```
|
63
|
+
ruby -e "require 'securerandom'; puts SecureRandom.hex(64)"
|
64
|
+
```
|
65
|
+
|
66
|
+
These values should be stored outside of your application repository for added
|
67
|
+
security. For example, one could use [dotenv][] and reference them as `ENV`
|
68
|
+
variables.
|
69
|
+
|
70
|
+
```
|
71
|
+
# .env
|
72
|
+
CRYPT_KEEPER_KEY=75d942f3d3b3492772e0330f717eaf5e689673ea8b983475ef8f6551f6e99d280cd89972706e46b48240cc01c4d0f7df5ffa3524566b789d147ed04cc4ea4eab
|
73
|
+
CRYPT_KEEPER_SALT=b16a153e99a5db616a861ea5a6febc64d8a758c4aef3b8c8fc6675ac9daf03f7965f16e8b4b2bdfd28ff65f5203afb8102b8f41c514c3667bb3512015b1e77e8
|
74
|
+
```
|
75
|
+
|
76
|
+
Then in your model:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
class MyModel < ActiveRecord::Base
|
80
|
+
crypt_keeper :field, :other_field, encryptor: :active_support, key: ENV["CRYPT_KEEPER_KEY"], salt: ENV["CRYPT_KEEPER_SALT"]
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
[dotenv]: https://github.com/bkeepers/dotenv
|
85
|
+
|
50
86
|
## Encodings
|
51
87
|
|
52
88
|
You can force an encoding on the plaintext before encryption and after decryption by using the `encoding` option. This is useful when dealing with multibyte strings:
|
53
89
|
|
54
90
|
```ruby
|
55
91
|
class MyModel < ActiveRecord::Base
|
56
|
-
crypt_keeper :field, :other_field, :
|
92
|
+
crypt_keeper :field, :other_field, encryptor: :active_support, key: 'super_good_password', salt: 'salt', encoding: 'UTF-8'
|
57
93
|
end
|
58
94
|
|
59
95
|
model = MyModel.new(field: 'Tromsø')
|
@@ -68,7 +104,7 @@ If you are working with an existing table you would like to encrypt, you must us
|
|
68
104
|
|
69
105
|
```ruby
|
70
106
|
class MyExistingModel < ActiveRecord::Base
|
71
|
-
crypt_keeper :field, :other_field, :
|
107
|
+
crypt_keeper :field, :other_field, encryptor: :active_support, key: 'super_good_password', salt: 'salt'
|
72
108
|
end
|
73
109
|
|
74
110
|
MyExistingModel.encrypt_table!
|
@@ -78,10 +114,10 @@ Running `encrypt_table!` will encrypt all rows in the database using the encrypt
|
|
78
114
|
|
79
115
|
## Supported Available Encryptors
|
80
116
|
|
81
|
-
There are four supported encryptors: `
|
117
|
+
There are four supported encryptors: `active_support`, `mysql_aes_new`, `postgres_pgp`, `postgres_pgp_public_key`.
|
82
118
|
|
83
|
-
* [
|
84
|
-
* Encryption is
|
119
|
+
* [ActiveSupport](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/active_support.rb)
|
120
|
+
* Encryption is performed using [ActiveSupport::MessageEncryptor](http://api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html)
|
85
121
|
* Passphrases are derived using [PBKDF2](http://en.wikipedia.org/wiki/PBKDF2)
|
86
122
|
|
87
123
|
* [MySQL AES New](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/mysql_aes_new.rb)
|
@@ -111,14 +147,14 @@ There are four supported encryptors: `aes_new`, `mysql_aes_new`, `postgres_pgp`,
|
|
111
147
|
## Searching
|
112
148
|
Searching ciphertext is a complex problem that varies depending on the encryption algorithm you choose. All of the bundled providers include search support, but they have some caveats.
|
113
149
|
|
114
|
-
*
|
115
|
-
*
|
150
|
+
* ActiveSupport::MessageEncryptor
|
151
|
+
* ActiveSupport's MessageEncryptor uses a random initialization vector when generating keys. The same plaintext encrypted multiple times will have different output each time for the ciphertext. Since this is the case, it is not possible to search leveraging the database. Database rows will need to be filtered in memory. It is suggested that you use a scope or ActiveRecord batches to narrow the results before seaching them.
|
116
152
|
|
117
153
|
* Mysql AES
|
118
|
-
|
154
|
+
* Surprisingly, MySQL's implementation of AES does not use a random initialization vector. The column containing the ciphertext can be indexed and searched quickly.
|
119
155
|
|
120
156
|
* PostgresSQL PGP
|
121
|
-
|
157
|
+
* PGP also uses a random initialization vector which means it generates unique output each time you encrypt plaintext. Although the database can be searched by performing row level decryption and comparing the plaintext, it will not be able to use an index. A scope or batch is suggested when searching.
|
122
158
|
|
123
159
|
## How the search interface is used
|
124
160
|
|
@@ -157,10 +193,35 @@ as a string or an underscored symbol
|
|
157
193
|
|
158
194
|
```ruby
|
159
195
|
class MyModel < ActiveRecord::Base
|
160
|
-
crypt_keeper :field, :other_field, :
|
196
|
+
crypt_keeper :field, :other_field, encryptor: :my_encryptor, key: 'super_good_password'
|
161
197
|
end
|
162
198
|
```
|
163
199
|
|
200
|
+
## Migrating from CryptKeeper 1.x to 2.0
|
201
|
+
|
202
|
+
CryptKeeper 2.0 removes the AES encryptor due to security issues in the
|
203
|
+
underlying AES gem. If you were previously using the `aes_new` encryptor, you
|
204
|
+
will need to follow these instructions to reencrypt your data.
|
205
|
+
|
206
|
+
The general migration path is as follows:
|
207
|
+
|
208
|
+
1. Enable maintenance mode in any live apps
|
209
|
+
2. Backup database
|
210
|
+
3. Decrypt tables: TableName.decrypt_table!
|
211
|
+
4. Update to 2.0.0.rc1 in your app. Update the encryptor to use :active_support
|
212
|
+
5. Encrypt tables: `TableName.encrypt_table!`
|
213
|
+
6. Verify data can be decrypted: `TableName.first`
|
214
|
+
7. Disable maintenance mode if necessary
|
215
|
+
|
216
|
+
In case you experience problems, the rollback procedure is as follows:
|
217
|
+
|
218
|
+
1. Enable maintenance mode
|
219
|
+
2. Backup database again
|
220
|
+
3. Restore first database dump, from before CryptKeeper 2.0.0.rc1
|
221
|
+
4. Verify data can be decrypted
|
222
|
+
5. Disable maintenance mode
|
223
|
+
6. Let us know what happened :(
|
224
|
+
|
164
225
|
## Requirements
|
165
226
|
|
166
227
|
CryptKeeper has been tested against ActiveRecord 4.2 and 5.0 using Ruby
|
data/crypt_keeper.gemspec
CHANGED
@@ -16,10 +16,10 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.require_paths = ["lib"]
|
17
17
|
gem.version = CryptKeeper::VERSION
|
18
18
|
|
19
|
-
gem.
|
20
|
-
|
21
|
-
gem.add_runtime_dependency '
|
22
|
-
gem.add_runtime_dependency '
|
19
|
+
gem.post_install_message = "WARNING: CryptKeeper 2.0 contains breaking changes and may require you to reencrypt your data! Please view the README at https://github.com/jmazzi/crypt_keeper for more information."
|
20
|
+
|
21
|
+
gem.add_runtime_dependency 'activerecord', '>= 4.2', '< 6.2'
|
22
|
+
gem.add_runtime_dependency 'activesupport', '>= 4.2', '< 6.2'
|
23
23
|
|
24
24
|
gem.add_development_dependency 'rspec', '~> 3.5.0'
|
25
25
|
gem.add_development_dependency 'guard', '~> 2.6.1'
|
@@ -35,8 +35,8 @@ Gem::Specification.new do |gem|
|
|
35
35
|
gem.add_development_dependency 'activerecord-jdbcpostgresql-adapter'
|
36
36
|
gem.add_development_dependency 'activerecord-jdbcmysql-adapter'
|
37
37
|
else
|
38
|
-
gem.add_development_dependency 'sqlite3'
|
38
|
+
gem.add_development_dependency 'sqlite3', '>= 1.3.3', '< 1.5'
|
39
39
|
gem.add_development_dependency 'pg', '~> 0.18.0'
|
40
|
-
gem.add_development_dependency 'mysql2', '
|
40
|
+
gem.add_development_dependency 'mysql2', '>= 0.3.13', '< 0.6.0'
|
41
41
|
end
|
42
42
|
end
|
data/lib/crypt_keeper.rb
CHANGED
@@ -4,7 +4,7 @@ require 'crypt_keeper/version'
|
|
4
4
|
require 'crypt_keeper/model'
|
5
5
|
require 'crypt_keeper/helper'
|
6
6
|
require 'crypt_keeper/provider/base'
|
7
|
-
require 'crypt_keeper/provider/
|
7
|
+
require 'crypt_keeper/provider/active_support'
|
8
8
|
require 'crypt_keeper/provider/mysql_aes_new'
|
9
9
|
require 'crypt_keeper/provider/postgres_base'
|
10
10
|
require 'crypt_keeper/provider/postgres_pgp'
|
data/lib/crypt_keeper/helper.rb
CHANGED
@@ -12,6 +12,10 @@ module CryptKeeper
|
|
12
12
|
def escape_and_execute_sql(query, new_transaction: false)
|
13
13
|
query = ::ActiveRecord::Base.send :sanitize_sql_array, query
|
14
14
|
|
15
|
+
# force binary encoding to avoid "invalid byte sequence in UTF-8" errors
|
16
|
+
# when we send binary AES keys (f.ex) to the database
|
17
|
+
query = query.b if query.respond_to?(:b)
|
18
|
+
|
15
19
|
if CryptKeeper.silence_logs?
|
16
20
|
::ActiveRecord::Base.logger.silence do
|
17
21
|
execute_sql(query, new_transaction: new_transaction)
|
@@ -39,10 +43,84 @@ module CryptKeeper
|
|
39
43
|
end
|
40
44
|
|
41
45
|
module DigestPassphrase
|
46
|
+
# Private: Hash algorithm to use when generating a PBKDF2 passphrase.
|
47
|
+
#
|
48
|
+
# Returns a String.
|
49
|
+
HASH_ALGORITHM = "sha512".freeze
|
50
|
+
|
51
|
+
# Private: Iterations to use when generating a PBKDF2 passphrase.
|
52
|
+
#
|
53
|
+
# Returns a String.
|
54
|
+
ITERATIONS = 5000
|
55
|
+
|
56
|
+
private_constant :HASH_ALGORITHM, :ITERATIONS
|
57
|
+
|
58
|
+
# Public: Iterations to use to generate digest passphrase.
|
59
|
+
#
|
60
|
+
# Returns a String.
|
61
|
+
mattr_accessor :iterations do
|
62
|
+
if iter = ENV["ARMOR_ITER"]
|
63
|
+
warn :ARMOR_ITER unless iter == ITERATIONS
|
64
|
+
Integer(iter)
|
65
|
+
else
|
66
|
+
ITERATIONS
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Public: Generates a hex passphrase using the given key and salt.
|
71
|
+
#
|
72
|
+
# key - Encryption key
|
73
|
+
# salt - Encryption salt
|
74
|
+
#
|
75
|
+
# Returns a String.
|
42
76
|
def digest_passphrase(key, salt)
|
43
|
-
raise ArgumentError.new("Missing :key")
|
77
|
+
raise ArgumentError.new("Missing :key") if key.blank?
|
44
78
|
raise ArgumentError.new("Missing :salt") if salt.blank?
|
45
|
-
|
79
|
+
|
80
|
+
require "openssl"
|
81
|
+
|
82
|
+
digest = OpenSSL::Digest.new(hash_algorithm)
|
83
|
+
|
84
|
+
hmac = OpenSSL::PKCS5.pbkdf2_hmac(
|
85
|
+
key,
|
86
|
+
salt,
|
87
|
+
iterations,
|
88
|
+
digest.digest_length,
|
89
|
+
digest
|
90
|
+
)
|
91
|
+
|
92
|
+
hmac.unpack("H*").first
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Private: Hash algorithm to use for digest passphrase.
|
98
|
+
#
|
99
|
+
# Returns a String.
|
100
|
+
def hash_algorithm
|
101
|
+
if hash = ENV["ARMOR_HASH"]
|
102
|
+
warn :ARMOR_HASH unless hash == HASH_ALGORITHM
|
103
|
+
hash
|
104
|
+
else
|
105
|
+
HASH_ALGORITHM
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Private: Warns about the deprecated ENV vars used with the Armor gem.
|
110
|
+
#
|
111
|
+
# key - The ENV variable name
|
112
|
+
#
|
113
|
+
# Returns a String.
|
114
|
+
def warn(key)
|
115
|
+
require "active_support/deprecation"
|
116
|
+
|
117
|
+
ActiveSupport::Deprecation.warn <<-MSG.squish
|
118
|
+
CryptKeeper no longer uses the Armor gem to generate passphrases for
|
119
|
+
MySQL AES encryption. Your installation is using a non-standard
|
120
|
+
value for `ENV["#{key}"]` which affects the way passphrases are
|
121
|
+
generated. You will need to re-encrypt your data with this variable
|
122
|
+
removed prior to CryptKeeper v3.0.0.
|
123
|
+
MSG
|
46
124
|
end
|
47
125
|
end
|
48
126
|
end
|
data/lib/crypt_keeper/model.rb
CHANGED
@@ -3,7 +3,7 @@ require 'active_support/core_ext/array/extract_options'
|
|
3
3
|
|
4
4
|
module CryptKeeper
|
5
5
|
module Model
|
6
|
-
extend ActiveSupport::Concern
|
6
|
+
extend ::ActiveSupport::Concern
|
7
7
|
|
8
8
|
# Public: Ensures that each field exist and is of type text. This prevents
|
9
9
|
# encrypted data from being truncated.
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "active_support/message_encryptor"
|
2
|
+
|
3
|
+
module CryptKeeper
|
4
|
+
module Provider
|
5
|
+
class ActiveSupport < Base
|
6
|
+
attr_reader :encryptor
|
7
|
+
|
8
|
+
# Public: Initializes the encryptor
|
9
|
+
#
|
10
|
+
# options - A hash, :key and :salt are required
|
11
|
+
#
|
12
|
+
# Returns nothing.
|
13
|
+
def initialize(options = {})
|
14
|
+
key = options.fetch(:key)
|
15
|
+
salt = options.fetch(:salt)
|
16
|
+
|
17
|
+
@encryptor = ::ActiveSupport::MessageEncryptor.new \
|
18
|
+
::ActiveSupport::KeyGenerator.new(key).generate_key(salt, 32)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Encrypts a string
|
22
|
+
#
|
23
|
+
# value - Plaintext value
|
24
|
+
#
|
25
|
+
# Returns an encrypted string
|
26
|
+
def encrypt(value)
|
27
|
+
encryptor.encrypt_and_sign(value)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Public: Decrypts a string
|
31
|
+
#
|
32
|
+
# value - Cipher text
|
33
|
+
#
|
34
|
+
# Returns a plaintext string
|
35
|
+
def decrypt(value)
|
36
|
+
encryptor.decrypt_and_verify(value)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: Searches the table
|
40
|
+
#
|
41
|
+
# records - ActiveRecord::Relation
|
42
|
+
# field - Field name to match
|
43
|
+
# criteria - Value to match
|
44
|
+
#
|
45
|
+
# Returns an Enumerable
|
46
|
+
def search(records, field, criteria)
|
47
|
+
records.select { |record| record[field] == criteria }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -12,7 +12,7 @@ module CryptKeeper
|
|
12
12
|
#
|
13
13
|
# options - A hash, :key and :salt are required
|
14
14
|
def initialize(options = {})
|
15
|
-
ActiveSupport.run_load_hooks(:crypt_keeper_mysql_aes_log, self)
|
15
|
+
::ActiveSupport.run_load_hooks(:crypt_keeper_mysql_aes_log, self)
|
16
16
|
@key = digest_passphrase(options[:key], options[:salt])
|
17
17
|
end
|
18
18
|
|
@@ -8,7 +8,7 @@ module CryptKeeper
|
|
8
8
|
#
|
9
9
|
# options - A hash, :key is required
|
10
10
|
def initialize(options = {})
|
11
|
-
ActiveSupport.run_load_hooks(:crypt_keeper_postgres_pgp_log, self)
|
11
|
+
::ActiveSupport.run_load_hooks(:crypt_keeper_postgres_pgp_log, self)
|
12
12
|
|
13
13
|
@key = options.fetch(:key) do
|
14
14
|
raise ArgumentError, "Missing :key"
|
@@ -42,8 +42,14 @@ module CryptKeeper
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def search(records, field, criteria)
|
45
|
-
|
46
|
-
|
45
|
+
if criteria.present?
|
46
|
+
records
|
47
|
+
.where.not("TRIM(BOTH FROM #{field}) = ?", "")
|
48
|
+
.where("(pgp_sym_decrypt(cast(\"#{field}\" AS bytea), ?) = ?)",
|
49
|
+
key, criteria)
|
50
|
+
else
|
51
|
+
records.where(field => criteria)
|
52
|
+
end
|
47
53
|
end
|
48
54
|
|
49
55
|
private
|
@@ -4,7 +4,7 @@ module CryptKeeper
|
|
4
4
|
attr_accessor :key
|
5
5
|
|
6
6
|
def initialize(options = {})
|
7
|
-
ActiveSupport.run_load_hooks(:crypt_keeper_postgres_pgp_log, self)
|
7
|
+
::ActiveSupport.run_load_hooks(:crypt_keeper_postgres_pgp_log, self)
|
8
8
|
|
9
9
|
@key = options.fetch(:key) do
|
10
10
|
raise ArgumentError, "Missing :key"
|
data/lib/crypt_keeper/version.rb
CHANGED
@@ -0,0 +1,94 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe CryptKeeper::Helper::DigestPassphrase do
|
4
|
+
# gem "armor"
|
5
|
+
# require "armor"
|
6
|
+
#
|
7
|
+
# 10.times.with_index(1).map do |_, i|
|
8
|
+
# hash = { key: SecureRandom.hex(64), salt: SecureRandom.hex(64) }
|
9
|
+
# hash[:armor] = Armor.digest(h[:key], h[:salt])
|
10
|
+
# hash
|
11
|
+
# end
|
12
|
+
let :armor_passphrases do
|
13
|
+
[
|
14
|
+
{
|
15
|
+
key: "c458da1abe465b15563e0f72e567594ca760214f2d11c668c6c0b923c8b5cc972b167410da6290feef3cc652a03d46f9ead9ce6da4cd197b4f66dd40f234aabc",
|
16
|
+
salt: "978279087fe82fd33b295df2ca38ae719d93c73d154110f9cd9e6c1859cd1525af1a9f9c25768b4bd7cfe6ce6fcd473d5f702c67fb5552369aeb7b5ab9c0573c",
|
17
|
+
armor: "2493abd50731ffbd80129bd9f2b77bcbffbb42a44f0e263668f9cf5f3d1804e1f9a56a9c85856be87b21b72a8b09aa21e111a646edae4a14b651019a73074066"
|
18
|
+
},
|
19
|
+
{
|
20
|
+
key: "3d4dd8193cf4c0ecaac0f3a1502a1e21a9bad969c97ace34399a4829a3457b56aa598b60a3031b6a10961e1558c7147d3895ec522eba0283a962bd82c6f558ef",
|
21
|
+
salt: "f5e7225b5add4a81e93773d543246ba2f7052e65f5adb651f8f47ba129e71053e04839db2ffb5b491223f589b2c74d4440b2c07d41e064df940aa433591a37b1",
|
22
|
+
armor: "1e9386895b6d33fed5caf24c6db598df888354ad2605679181d7e2b7c4b59611579d2b1a4e72b834f230848dd715a0b7e37280f2304a2fc78c6f5037a0ef3e9c"
|
23
|
+
},
|
24
|
+
{
|
25
|
+
key: "2d19e58724b7bb62d6129e2af692854919daeb6cf477c1e5dd5f8c846dbae3768baaaa8ba79d39c765b88b68d510f3b2fb59389af5f99c68c2e77620fe610393",
|
26
|
+
salt: "a96bfc08af98c063e0f9b3e6498d90a33cf279d12d38d6f5aabd6b7691d5ed25346bc2e9cfee1c98b8cea243b7e79ad69e281800207ea022007dc42e2f49dce3",
|
27
|
+
armor: "e86340f12e833d8cc722249f7190184562d4b7b27755ca3ece1950cf9158629536b16609406f230902f6f15c15afa652c35c0b0239c90167efd438b1e1233fd1"
|
28
|
+
},
|
29
|
+
{
|
30
|
+
key: "e78715a772f930027b0e6353e3be578ebc8fd5631401be9e723f747e03f3bed90cb051fa25024e8179e8fc3c65072d74eee92a636e5c2fa89f61f3f1dc2d043b",
|
31
|
+
salt: "536d128ba1f2453040d31eaddb87d1b1d2ac157f2ec7f6d2ef530a5ae01f19afea43cae3ca5783440c90decc9cedf78ec6aad474b2e67637abd640dc9d8b1665",
|
32
|
+
armor: "96c617786de65bda0d287851a189d50b56a14f3fccc72886943609e0644a593c4d51221d93f10287a79becb3f8f26322413c5c4e65a1e6564b4da55d1f175402"
|
33
|
+
},
|
34
|
+
{
|
35
|
+
key: "941e93a03156121bf565d62d9fadfc5f56ec5ecfc274a5ad1298d358419b8e18b5e4c4b17426ae031676e11633f875a5e3e39e50a4b08685427a72ab6289e465",
|
36
|
+
salt: "114b89e478b19e5e0d4df10b1914892a7434fdd0857bbb236bb21d9d8fae03b5401b1a53d64390ffa18fc74bc80139a8f216eb6a0c79155bcd89955a36be0322",
|
37
|
+
armor: "ead4be935420a772cc2c2637229431ee093ebcadb1f432d394eeca45515c4b74b732eec76c6d8aff33f0aafd33cab7e2d967796a271d3e8a10be5655d1025708"
|
38
|
+
},
|
39
|
+
{
|
40
|
+
key: "c7c7dbd592beb79178cdbbd45f91654a40c44fd632de0d89398dc1f0c8b241210b8db538a3cb3699796323fc1c54877c3df959e94bf5765fafd03c05fb4d4d7f",
|
41
|
+
salt: "1cd67ceaf2c1635eda3d1bbea60f228fd95d59c59a34624b4abf6846a2ae774f359a48695e6a9df5aef34f42dcfa69ad6dbd5be1e7190b050e0c58f5a0ff0cce",
|
42
|
+
armor: "387c92c591510bf56677a4de231065d271c6553c8b75a1a1710bb4a7c1404a527e5cbd9f0b812eedc162e5bd5dae4e11e9b5f3782eb4fd4616e2725fa98093e4"
|
43
|
+
},
|
44
|
+
{
|
45
|
+
key: "6fa240131e1aaf2214dc4a5f52709721c078ef8cb4aba1ab926650b202cfeb8db90acaebb0109667613768be3e0af1decf739c108861aa5ac55248bd307e8cbe",
|
46
|
+
salt: "de1d28b96bf570bcc8c26949d1d9f4c03b30f4d2e7799df3d4f981384b731f15b5494095cb6bd8d71f374d1bde10b6f9048bb6eb6a763af74af6b70a8320e651",
|
47
|
+
armor: "e9b72ed9330477421c63d38a3dfe056fa5ee5d2097af6f024b4452df6010e2563daca906b7ec49c04071973c49cc010f41b5c4503e691d2bc2066a5ff9766be5"
|
48
|
+
},
|
49
|
+
{
|
50
|
+
key: "2accf78dc18db506512c5aa36b6d50950ec98473873fe75a2fd6322883346a02d77f014f9fd5e02d1d3ab34e4c46e93bb17b5d2436f76ec13fade56cdb97ce04",
|
51
|
+
salt: "e1ee82d540543beda0a6f8c362e2890665a5b6cdfcb115bf4af3808ac49e43e7ca286d7fc4921f6ad5344a1b082db2e47913cd4616b23e0299f45c09487c65dc",
|
52
|
+
armor: "ad84346e9bec6885902390f7a45d674e48b53481400d7008b45780c2677f3a93635c6000b2760ccee184839edb2bd2b34914dc70f322005e58216e013d73cdeb"
|
53
|
+
},
|
54
|
+
{
|
55
|
+
key: "05e3508b0f4a1116fadbbd04a6c4c0a38c29724c76d7c406313813300cefbf9dbe426c28f65ca7297927707abaa4788572b1b9835a421d8699f29096055bc156",
|
56
|
+
salt: "371380fde21244493ec04928154c6af685d16e4a7feeb79b5916b484d485f3137eb30ebffdd0bebd77f86a2f092155e798acc0dac91b1704244614b0940da88e",
|
57
|
+
armor: "d573f9bb1ed0f7dac9a4f087abe7400cb366cda2877ed8c1660bd6dd2b96533f1210883079921a35fa09339d5f84fbe8b4977a6621e783ee5c3ac85e80b3685c"
|
58
|
+
},
|
59
|
+
{
|
60
|
+
key: "9dd15dc718b7ae4a87652e51b64aa545b6b6f26726e76fb0c45ec256be9359318d68e94858b0d782ba8505201c1b7a604492ba831ad0e93185fc5ccffbc7007d",
|
61
|
+
salt: "971bc4fada6fdc5ba3484d7a2ef34a4a2c9f7cc7e8c67d00fc2c774b2db2ef3a4f81c4bc4977055248256fe68f1525fa2f8673d56b41a5e15aae45b68d7f6fc6",
|
62
|
+
armor: "3d9d44881595a083efae99e710bd1fe68321152ac61e2d225974d5e2c2d4ae24849cc7b0f7f9af325fb4aa20524d12565e9e4c36cad91f9165df3af2f4692a07"
|
63
|
+
}
|
64
|
+
]
|
65
|
+
end
|
66
|
+
|
67
|
+
let :example_key do
|
68
|
+
"ff17f8eeeb25e8647cda72fc577b1e9f3f68f6f18997efcdb043f171ad93949b39150b886d4866230add5a0ef6285b37c20bca5194a4dc76ae8845ed88ccb2db"
|
69
|
+
end
|
70
|
+
|
71
|
+
let :example_salt do
|
72
|
+
"8f273e1ffae67cf5582dd5fcec9d7f99e996861a985e502aa1283088a27d9fdda63869007474ef689381f10ea4f8f5e9e72255a9e47304cd80fbfafd414d77a7"
|
73
|
+
end
|
74
|
+
|
75
|
+
let :example_passphrase do
|
76
|
+
"01096fb7199ac8051d24a350204df61a0ab5b7c6bc393a3325d5dc686de39df7084d7cab12065eb06f2e09f4bc5b6a3851ce459e7eca47088fd9d1bf1d9f35b5"
|
77
|
+
end
|
78
|
+
|
79
|
+
let :encryptor do
|
80
|
+
Class.new do
|
81
|
+
include CryptKeeper::Helper::DigestPassphrase
|
82
|
+
end.new
|
83
|
+
end
|
84
|
+
|
85
|
+
it "returns passphrases that are backwards compatible with Armor" do
|
86
|
+
armor_passphrases.each do |ex|
|
87
|
+
expect(encryptor.digest_passphrase(ex[:key], ex[:salt])).to eq(ex[:armor])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it "generates a passphrase from key/salt" do
|
92
|
+
expect(encryptor.digest_passphrase(example_key, example_salt)).to eq(example_passphrase)
|
93
|
+
end
|
94
|
+
end
|
@@ -25,8 +25,7 @@ describe CryptKeeper::Model do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
it "allows binary as a valid type" do
|
28
|
-
subject.crypt_keeper :
|
29
|
-
allow(subject.columns_hash['storage']).to receive(:type).and_return(:binary)
|
28
|
+
subject.crypt_keeper :storage_binary, encryptor: :fake_encryptor
|
30
29
|
expect(subject.new.save).to be_truthy
|
31
30
|
end
|
32
31
|
|
@@ -55,6 +54,7 @@ describe CryptKeeper::Model do
|
|
55
54
|
end
|
56
55
|
end
|
57
56
|
|
57
|
+
|
58
58
|
context "Encryption and Decryption" do
|
59
59
|
let(:plain_text) { 'plain_text' }
|
60
60
|
let(:cipher_text) { 'tooltxet_nialp' }
|
@@ -105,6 +105,21 @@ describe CryptKeeper::Model do
|
|
105
105
|
expect_any_instance_of(CryptKeeper::Provider::Encryptor).to_not receive(:decrypt)
|
106
106
|
subject.find(record.id).storage
|
107
107
|
end
|
108
|
+
|
109
|
+
context "with a binary database field" do
|
110
|
+
subject { create_encrypted_model :storage_binary, passphrase: 'tool', encryptor: :encryptor }
|
111
|
+
|
112
|
+
it "encrypts the data" do
|
113
|
+
expect_any_instance_of(CryptKeeper::Provider::Encryptor).to receive(:encrypt).with('testing')
|
114
|
+
subject.create!(storage_binary: 'testing')
|
115
|
+
end
|
116
|
+
|
117
|
+
it "decrypts the data" do
|
118
|
+
record = subject.create!(storage_binary: 'testing')
|
119
|
+
expect_any_instance_of(CryptKeeper::Provider::Encryptor).to receive(:decrypt).at_least(1).times.with('toolgnitset')
|
120
|
+
subject.find(record.id).storage_binary
|
121
|
+
end
|
122
|
+
end
|
108
123
|
end
|
109
124
|
|
110
125
|
context "Search" do
|
@@ -120,7 +135,7 @@ describe CryptKeeper::Model do
|
|
120
135
|
end
|
121
136
|
|
122
137
|
context "Encodings" do
|
123
|
-
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :
|
138
|
+
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :active_support, encoding: 'utf-8' }
|
124
139
|
|
125
140
|
it "forces the encoding on decrypt" do
|
126
141
|
record = subject.create!(storage: 'Tromsø')
|
@@ -137,7 +152,7 @@ describe CryptKeeper::Model do
|
|
137
152
|
end
|
138
153
|
|
139
154
|
context "Initial Table Encryption" do
|
140
|
-
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :
|
155
|
+
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :active_support }
|
141
156
|
|
142
157
|
before do
|
143
158
|
subject.delete_all
|
@@ -146,14 +161,14 @@ describe CryptKeeper::Model do
|
|
146
161
|
end
|
147
162
|
|
148
163
|
it "encrypts the table" do
|
149
|
-
expect { subject.first(5).map(&:storage) }.to raise_error(
|
164
|
+
expect { subject.first(5).map(&:storage) }.to raise_error(ActiveSupport::MessageVerifier::InvalidSignature)
|
150
165
|
subject.encrypt_table!
|
151
166
|
expect { subject.first(5).map(&:storage) }.not_to raise_error
|
152
167
|
end
|
153
168
|
end
|
154
169
|
|
155
170
|
context "Table Decryption (Reverse of Initial Table Encryption)" do
|
156
|
-
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :
|
171
|
+
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :active_support }
|
157
172
|
let!(:storage_entries) { 5.times.map { |i| "testing#{i}" } }
|
158
173
|
|
159
174
|
before do
|
@@ -168,7 +183,7 @@ describe CryptKeeper::Model do
|
|
168
183
|
end
|
169
184
|
|
170
185
|
context "Missing Attributes" do
|
171
|
-
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :
|
186
|
+
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :active_support, encoding: 'utf-8' }
|
172
187
|
|
173
188
|
it "doesn't attempt decryption of missing attributes" do
|
174
189
|
subject.create!(storage: 'blah')
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CryptKeeper::Provider::ActiveSupport do
|
4
|
+
subject { described_class.new(key: 'cake', salt: 'salt') }
|
5
|
+
|
6
|
+
let :plaintext do
|
7
|
+
"string"
|
8
|
+
end
|
9
|
+
|
10
|
+
let :encrypted do
|
11
|
+
subject.encrypt plaintext
|
12
|
+
end
|
13
|
+
|
14
|
+
let :decrypted do
|
15
|
+
subject.decrypt encrypted
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#encrypt" do
|
19
|
+
specify { expect(encrypted).to_not eq(plaintext) }
|
20
|
+
specify { expect(encrypted).to_not be_blank }
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#decrypt" do
|
24
|
+
specify { expect(decrypted).to eq(plaintext) }
|
25
|
+
specify { expect(decrypted).to_not be_blank }
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#search" do
|
29
|
+
let :records do
|
30
|
+
[{ name: 'Bob' }, { name: 'Tim' }]
|
31
|
+
end
|
32
|
+
|
33
|
+
it "finds the matching record" do
|
34
|
+
expect(subject.search(records, :name, 'Bob')).to eql([records.first])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -7,14 +7,16 @@ describe CryptKeeper::Provider::MysqlAesNew do
|
|
7
7
|
|
8
8
|
# MySQL stores AES encrypted strings in binary which you can't paste
|
9
9
|
# into a spec :). This is a Base64 encoded string of 'test' AES encrypted
|
10
|
-
# by AES_ENCRYPT()
|
10
|
+
# by AES_ENCRYPT():
|
11
|
+
#
|
12
|
+
# SELECT TO_BASE64(AES_ENCRYPT('test', 'cd9c9275c80ccaec820f988edafd92bd0403d2055aed2b7094f569bc5314b88720c155f7f377addc35eff90c8bd11c34d5daaab5051c3435d2f5ad0c09d4b43e'));
|
11
13
|
let(:cipher_text) do
|
12
|
-
"
|
14
|
+
"qlwlvp6+otN1qnchZ48zNQ=="
|
13
15
|
end
|
14
16
|
|
15
17
|
subject { described_class.new key: ENCRYPTION_PASSWORD, salt: 'salt' }
|
16
18
|
|
17
|
-
specify { expect(subject.key).to eq("
|
19
|
+
specify { expect(subject.key).to eq("cd9c9275c80ccaec820f988edafd92bd0403d2055aed2b7094f569bc5314b88720c155f7f377addc35eff90c8bd11c34d5daaab5051c3435d2f5ad0c09d4b43e") }
|
18
20
|
|
19
21
|
describe "#initialize" do
|
20
22
|
specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") }
|
@@ -64,6 +64,42 @@ describe CryptKeeper::Provider::PostgresPgp do
|
|
64
64
|
match = subject.create!(storage: 'blah')
|
65
65
|
expect(subject.search_by_plaintext(:storage, 'blah').first).to eq(match)
|
66
66
|
end
|
67
|
+
|
68
|
+
it "successfully accesses result when the data has empty strings and nil" do
|
69
|
+
subject.create!(storage: nil)
|
70
|
+
subject.create!(storage: '')
|
71
|
+
subject.create!(storage: ' ')
|
72
|
+
match = subject.create!(storage: 'blah')
|
73
|
+
|
74
|
+
results = subject.search_by_plaintext(:storage, 'blah')
|
75
|
+
|
76
|
+
expect(results).to match_array([match])
|
77
|
+
end
|
78
|
+
|
79
|
+
it "finds an empty string" do
|
80
|
+
subject.create!(storage: nil)
|
81
|
+
subject.create!(storage: 'blah')
|
82
|
+
match = subject.create!(storage: '')
|
83
|
+
|
84
|
+
expect(subject.search_by_plaintext(:storage, '')).to match_array([match])
|
85
|
+
end
|
86
|
+
|
87
|
+
it "finds the empty string with the right number of spaces" do
|
88
|
+
subject.create!(storage: '')
|
89
|
+
match = subject.create!(storage: ' ')
|
90
|
+
|
91
|
+
results = subject.search_by_plaintext(:storage, ' ')
|
92
|
+
|
93
|
+
expect(results).to match_array([match])
|
94
|
+
end
|
95
|
+
|
96
|
+
it "finds nil results" do
|
97
|
+
subject.create!(storage: '')
|
98
|
+
subject.create!(storage: 'blah')
|
99
|
+
match = subject.create!(storage: nil)
|
100
|
+
|
101
|
+
expect(subject.search_by_plaintext(:storage, nil)).to match_array([match])
|
102
|
+
end
|
67
103
|
end
|
68
104
|
|
69
105
|
describe "Custom pgcrypto options" do
|
data/spec/spec_helper.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
ENV['ARMOR_ITER'] ||= "10"
|
2
1
|
ENV['CRYPT_KEEPER_IGNORE_LEGACY_DEPRECATION'] = "true"
|
3
2
|
require 'coveralls'
|
4
3
|
Coveralls.wear!
|
@@ -15,7 +14,11 @@ RSpec.configure do |config|
|
|
15
14
|
config.filter_run :focus
|
16
15
|
|
17
16
|
config.after :each do
|
18
|
-
ActiveRecord::Base.descendants.
|
17
|
+
descendants = ActiveRecord::Base.descendants.delete_if do |descendant|
|
18
|
+
descendant.to_s.include?("SchemaMigration")
|
19
|
+
end
|
20
|
+
|
21
|
+
descendants.each do |model|
|
19
22
|
model.method(:delete_all).call
|
20
23
|
end
|
21
24
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: crypt_keeper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Mazzi
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -19,7 +19,7 @@ dependencies:
|
|
19
19
|
version: '4.2'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
22
|
+
version: '6.2'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -29,7 +29,7 @@ dependencies:
|
|
29
29
|
version: '4.2'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
32
|
+
version: '6.2'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: activesupport
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -39,7 +39,7 @@ dependencies:
|
|
39
39
|
version: '4.2'
|
40
40
|
- - "<"
|
41
41
|
- !ruby/object:Gem::Version
|
42
|
-
version: '
|
42
|
+
version: '6.2'
|
43
43
|
type: :runtime
|
44
44
|
prerelease: false
|
45
45
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -49,35 +49,7 @@ dependencies:
|
|
49
49
|
version: '4.2'
|
50
50
|
- - "<"
|
51
51
|
- !ruby/object:Gem::Version
|
52
|
-
version: '
|
53
|
-
- !ruby/object:Gem::Dependency
|
54
|
-
name: aes
|
55
|
-
requirement: !ruby/object:Gem::Requirement
|
56
|
-
requirements:
|
57
|
-
- - "~>"
|
58
|
-
- !ruby/object:Gem::Version
|
59
|
-
version: 0.5.0
|
60
|
-
type: :runtime
|
61
|
-
prerelease: false
|
62
|
-
version_requirements: !ruby/object:Gem::Requirement
|
63
|
-
requirements:
|
64
|
-
- - "~>"
|
65
|
-
- !ruby/object:Gem::Version
|
66
|
-
version: 0.5.0
|
67
|
-
- !ruby/object:Gem::Dependency
|
68
|
-
name: armor
|
69
|
-
requirement: !ruby/object:Gem::Requirement
|
70
|
-
requirements:
|
71
|
-
- - "~>"
|
72
|
-
- !ruby/object:Gem::Version
|
73
|
-
version: 0.0.2
|
74
|
-
type: :runtime
|
75
|
-
prerelease: false
|
76
|
-
version_requirements: !ruby/object:Gem::Requirement
|
77
|
-
requirements:
|
78
|
-
- - "~>"
|
79
|
-
- !ruby/object:Gem::Version
|
80
|
-
version: 0.0.2
|
52
|
+
version: '6.2'
|
81
53
|
- !ruby/object:Gem::Dependency
|
82
54
|
name: rspec
|
83
55
|
requirement: !ruby/object:Gem::Requirement
|
@@ -182,14 +154,20 @@ dependencies:
|
|
182
154
|
requirements:
|
183
155
|
- - ">="
|
184
156
|
- !ruby/object:Gem::Version
|
185
|
-
version:
|
157
|
+
version: 1.3.3
|
158
|
+
- - "<"
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '1.5'
|
186
161
|
type: :development
|
187
162
|
prerelease: false
|
188
163
|
version_requirements: !ruby/object:Gem::Requirement
|
189
164
|
requirements:
|
190
165
|
- - ">="
|
191
166
|
- !ruby/object:Gem::Version
|
192
|
-
version:
|
167
|
+
version: 1.3.3
|
168
|
+
- - "<"
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '1.5'
|
193
171
|
- !ruby/object:Gem::Dependency
|
194
172
|
name: pg
|
195
173
|
requirement: !ruby/object:Gem::Requirement
|
@@ -208,16 +186,22 @@ dependencies:
|
|
208
186
|
name: mysql2
|
209
187
|
requirement: !ruby/object:Gem::Requirement
|
210
188
|
requirements:
|
211
|
-
- - "
|
189
|
+
- - ">="
|
190
|
+
- !ruby/object:Gem::Version
|
191
|
+
version: 0.3.13
|
192
|
+
- - "<"
|
212
193
|
- !ruby/object:Gem::Version
|
213
|
-
version: 0.
|
194
|
+
version: 0.6.0
|
214
195
|
type: :development
|
215
196
|
prerelease: false
|
216
197
|
version_requirements: !ruby/object:Gem::Requirement
|
217
198
|
requirements:
|
218
|
-
- - "
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: 0.3.13
|
202
|
+
- - "<"
|
219
203
|
- !ruby/object:Gem::Version
|
220
|
-
version: 0.
|
204
|
+
version: 0.6.0
|
221
205
|
description: Transparent ActiveRecord encryption
|
222
206
|
email:
|
223
207
|
- jmazzi@gmail.com
|
@@ -236,23 +220,28 @@ files:
|
|
236
220
|
- Rakefile
|
237
221
|
- crypt_keeper.gemspec
|
238
222
|
- gemfiles/activerecord_4_2.gemfile
|
223
|
+
- gemfiles/activerecord_5_0.gemfile
|
239
224
|
- gemfiles/activerecord_5_1.gemfile
|
225
|
+
- gemfiles/activerecord_5_2.gemfile
|
226
|
+
- gemfiles/activerecord_6_0.gemfile
|
227
|
+
- gemfiles/activerecord_6_1.gemfile
|
240
228
|
- lib/crypt_keeper.rb
|
241
229
|
- lib/crypt_keeper/helper.rb
|
242
230
|
- lib/crypt_keeper/log_subscriber/mysql_aes.rb
|
243
231
|
- lib/crypt_keeper/log_subscriber/postgres_pgp.rb
|
244
232
|
- lib/crypt_keeper/model.rb
|
245
|
-
- lib/crypt_keeper/provider/
|
233
|
+
- lib/crypt_keeper/provider/active_support.rb
|
246
234
|
- lib/crypt_keeper/provider/base.rb
|
247
235
|
- lib/crypt_keeper/provider/mysql_aes_new.rb
|
248
236
|
- lib/crypt_keeper/provider/postgres_base.rb
|
249
237
|
- lib/crypt_keeper/provider/postgres_pgp.rb
|
250
238
|
- lib/crypt_keeper/provider/postgres_pgp_public_key.rb
|
251
239
|
- lib/crypt_keeper/version.rb
|
240
|
+
- spec/crypt_keeper/helper_spec.rb
|
252
241
|
- spec/crypt_keeper/log_subscriber/mysql_aes_spec.rb
|
253
242
|
- spec/crypt_keeper/log_subscriber/postgres_pgp_spec.rb
|
254
243
|
- spec/crypt_keeper/model_spec.rb
|
255
|
-
- spec/crypt_keeper/provider/
|
244
|
+
- spec/crypt_keeper/provider/active_support_spec.rb
|
256
245
|
- spec/crypt_keeper/provider/mysql_aes_new_spec.rb
|
257
246
|
- spec/crypt_keeper/provider/postgres_pgp_public_key_spec.rb
|
258
247
|
- spec/crypt_keeper/provider/postgres_pgp_spec.rb
|
@@ -267,7 +256,9 @@ homepage: http://jmazzi.github.com/crypt_keeper/
|
|
267
256
|
licenses:
|
268
257
|
- MIT
|
269
258
|
metadata: {}
|
270
|
-
post_install_message:
|
259
|
+
post_install_message: 'WARNING: CryptKeeper 2.0 contains breaking changes and may
|
260
|
+
require you to reencrypt your data! Please view the README at https://github.com/jmazzi/crypt_keeper
|
261
|
+
for more information.'
|
271
262
|
rdoc_options: []
|
272
263
|
require_paths:
|
273
264
|
- lib
|
@@ -282,16 +273,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
282
273
|
- !ruby/object:Gem::Version
|
283
274
|
version: '0'
|
284
275
|
requirements: []
|
285
|
-
|
286
|
-
|
287
|
-
signing_key:
|
276
|
+
rubygems_version: 3.0.9
|
277
|
+
signing_key:
|
288
278
|
specification_version: 4
|
289
279
|
summary: Transparent ActiveRecord encryption
|
290
280
|
test_files:
|
281
|
+
- spec/crypt_keeper/helper_spec.rb
|
291
282
|
- spec/crypt_keeper/log_subscriber/mysql_aes_spec.rb
|
292
283
|
- spec/crypt_keeper/log_subscriber/postgres_pgp_spec.rb
|
293
284
|
- spec/crypt_keeper/model_spec.rb
|
294
|
-
- spec/crypt_keeper/provider/
|
285
|
+
- spec/crypt_keeper/provider/active_support_spec.rb
|
295
286
|
- spec/crypt_keeper/provider/mysql_aes_new_spec.rb
|
296
287
|
- spec/crypt_keeper/provider/postgres_pgp_public_key_spec.rb
|
297
288
|
- spec/crypt_keeper/provider/postgres_pgp_spec.rb
|
@@ -1,53 +0,0 @@
|
|
1
|
-
require 'aes'
|
2
|
-
require 'armor'
|
3
|
-
|
4
|
-
module CryptKeeper
|
5
|
-
module Provider
|
6
|
-
class AesNew < Base
|
7
|
-
include CryptKeeper::Helper::DigestPassphrase
|
8
|
-
|
9
|
-
# Public: The encryption key
|
10
|
-
attr_accessor :key
|
11
|
-
|
12
|
-
# Public: Initializes the class
|
13
|
-
#
|
14
|
-
# options - A hash of options. :key and :salt are required
|
15
|
-
def initialize(options = {})
|
16
|
-
@key = digest_passphrase(options[:key], options[:salt])
|
17
|
-
end
|
18
|
-
|
19
|
-
# Public: Encrypt a string
|
20
|
-
#
|
21
|
-
# Note: nil and empty strings are not encryptable with AES.
|
22
|
-
# When they are encountered, the orignal value is returned.
|
23
|
-
# Otherwise, returns the encrypted string
|
24
|
-
#
|
25
|
-
# Returns a String
|
26
|
-
def encrypt(value)
|
27
|
-
AES.encrypt(value, key)
|
28
|
-
end
|
29
|
-
|
30
|
-
# Public: Decrypt a string
|
31
|
-
#
|
32
|
-
# Note: nil and empty strings are not encryptable with AES (and thus cannot be decrypted).
|
33
|
-
# When they are encountered, the orignal value is returned.
|
34
|
-
# Otherwise, returns the decrypted string
|
35
|
-
#
|
36
|
-
# Returns a String
|
37
|
-
def decrypt(value)
|
38
|
-
AES.decrypt(value, key)
|
39
|
-
end
|
40
|
-
|
41
|
-
# Public: Search for a record
|
42
|
-
#
|
43
|
-
# record - An ActiveRecord collection
|
44
|
-
# field - The field to search
|
45
|
-
# criteria - A string to search with
|
46
|
-
#
|
47
|
-
# Returns an Enumerable
|
48
|
-
def search(records, field, criteria)
|
49
|
-
records.select { |record| record[field] == criteria }
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
@@ -1,41 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe CryptKeeper::Provider::AesNew do
|
4
|
-
subject { described_class.new(key: 'cake', salt: 'salt') }
|
5
|
-
|
6
|
-
describe "#initialize" do
|
7
|
-
let(:digested_key) do
|
8
|
-
::Armor.digest('cake', 'salt')
|
9
|
-
end
|
10
|
-
|
11
|
-
specify { expect(subject.key).to eq(digested_key) }
|
12
|
-
specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") }
|
13
|
-
end
|
14
|
-
|
15
|
-
describe "#encrypt" do
|
16
|
-
let(:encrypted) do
|
17
|
-
subject.encrypt 'string'
|
18
|
-
end
|
19
|
-
|
20
|
-
specify { expect(encrypted).to_not eq('string') }
|
21
|
-
specify { expect(encrypted).to_not be_blank }
|
22
|
-
end
|
23
|
-
|
24
|
-
describe "#decrypt" do
|
25
|
-
let(:decrypted) do
|
26
|
-
subject.decrypt "V02ebRU2wLk25AizasROVg==$kE+IpRaUNdBfYqR+WjMqvA=="
|
27
|
-
end
|
28
|
-
|
29
|
-
specify { expect(decrypted).to eq('string') }
|
30
|
-
end
|
31
|
-
|
32
|
-
describe "#search" do
|
33
|
-
let(:records) do
|
34
|
-
[{ name: 'Bob' }, { name: 'Tim' }]
|
35
|
-
end
|
36
|
-
|
37
|
-
it "finds the matching record" do
|
38
|
-
expect(subject.search(records, :name, 'Bob')).to eql([records.first])
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|