active_enquo 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +11 -4
- data/active_enquo.gemspec +1 -1
- data/docs/MIGRATION.md +107 -0
- data/e2e_tests/.gitignore +1 -0
- data/e2e_tests/001_direct_migration/exercise_model +27 -0
- data/e2e_tests/001_direct_migration/migrations/001_create_people_table.rb +11 -0
- data/e2e_tests/001_direct_migration/migrations/002_encrypt_people_data.rb +25 -0
- data/e2e_tests/001_direct_migration/run +18 -0
- data/e2e_tests/helper.sh +34 -0
- data/e2e_tests/init.rb +18 -0
- data/e2e_tests/people.json +1 -0
- data/e2e_tests/run +19 -0
- data/lib/active_enquo.rb +229 -18
- metadata +14 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d538e223b9e7485c4b08cf8ac38f2cc6a08f5d3c3b72426167606fdc7a5fdf5
|
4
|
+
data.tar.gz: 33e80addf4beb8bbe8f5328cbfd093cb70264ec2804d148026cbdabbd36cd1b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af6a5e20a215234ce3bce1656a7e02e94f536bf1a3c8d674732d7d49cd8ab5265fe4c306ea730cfe60bba707df5436cf9048d9ce8b85f021fc92639835a0424d
|
7
|
+
data.tar.gz: e9f6b9d0f5f5f5fb5b6fdecd8ba7fdcadf5b7be0c363787d78a2a989bb753b506d9d914122041614aad4cb3032803d6f5075ec02019a81457104471decd4fec7
|
data/README.md
CHANGED
@@ -116,7 +116,7 @@ You can now, without any further ado, use that attribute in your models as you w
|
|
116
116
|
For example, you can insert a new record:
|
117
117
|
|
118
118
|
```ruby
|
119
|
-
User.create!([{name: "Clara Bloggs", username: "cbloggs", date_of_birth: Date(1970, 1, 1)}])
|
119
|
+
User.create!([{name: "Clara Bloggs", username: "cbloggs", date_of_birth: Date.new(1970, 1, 1)}])
|
120
120
|
```
|
121
121
|
|
122
122
|
When you retrieve a record, the value is there for you to read:
|
@@ -130,18 +130,18 @@ User.where(username: "cbloggs").first.date_of_birth.to_s # => "1970-01-01"
|
|
130
130
|
|
131
131
|
This is where things get *neat*.
|
132
132
|
|
133
|
-
Performing a query on Enquo-encrypted data is done the same way as on unencrypted data
|
133
|
+
Performing a query on Enquo-encrypted data is done the same way as on unencrypted data.
|
134
134
|
|
135
135
|
You can query for records that have the exact value you're looking for:
|
136
136
|
|
137
137
|
```ruby
|
138
|
-
User.where(
|
138
|
+
User.where(date_of_birth: Date(1970, 1, 1))
|
139
139
|
```
|
140
140
|
|
141
141
|
Or you can query for users born less than 50 years ago:
|
142
142
|
|
143
143
|
```ruby
|
144
|
-
User.where(
|
144
|
+
User.where(date_of_birth: (Date.today - 50.years))..)
|
145
145
|
```
|
146
146
|
|
147
147
|
This doesn't seem so magical, until you take a peek in the database, and realise that *all the data is still encrypted*:
|
@@ -154,6 +154,12 @@ psql> SELECT date_of_birth FROM users WHERE username='cbloggs';
|
|
154
154
|
```
|
155
155
|
|
156
156
|
|
157
|
+
## Migrating Existing Data to Encrypted Form
|
158
|
+
|
159
|
+
This is a topic on which a lot of words can be written.
|
160
|
+
For the sake of tidiness, these words are in [a guide of their own](docs/MIGRATION.md).
|
161
|
+
|
162
|
+
|
157
163
|
## Indexing and Ordering
|
158
164
|
|
159
165
|
To maintain [security by default](https://enquo.org/about/threat-models#snapshot-security), ActiveEnquo doesn't provide the ability to `ORDER BY` or index columns by default.
|
@@ -168,6 +174,7 @@ class User < ApplicationRecord
|
|
168
174
|
end
|
169
175
|
```
|
170
176
|
|
177
|
+
|
171
178
|
### Security Considerations
|
172
179
|
|
173
180
|
As the name implies, "reduced security operations" require that the security of the data in the column be lower than [Enquo's default security properties](https://enquo.org/about/threat-models#snapshot-security).
|
data/active_enquo.gemspec
CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |s|
|
|
27
27
|
|
28
28
|
s.required_ruby_version = ">= 2.7.0"
|
29
29
|
|
30
|
-
s.add_runtime_dependency "enquo-core", "~> 0.
|
30
|
+
s.add_runtime_dependency "enquo-core", "~> 0.7"
|
31
31
|
s.add_runtime_dependency "activerecord", ">= 6"
|
32
32
|
|
33
33
|
s.add_development_dependency "bundler"
|
data/docs/MIGRATION.md
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
If you have data already in your database that you wish to protect using ActiveEnquo, you can migrate the data into an encrypted form.
|
2
|
+
|
3
|
+
There are two approaches that can be used:
|
4
|
+
|
5
|
+
* [Direct Migration](#data-migration-with-downtime), which is straightforward but involves application downtime; or
|
6
|
+
|
7
|
+
* [Live Migration](#live-data-migration), which is more complicated, but can be done while keeping the application available at all times.
|
8
|
+
|
9
|
+
|
10
|
+
# Data Migration (with Downtime)
|
11
|
+
|
12
|
+
If your application can withstand being down for a period of time, you can use a simple migration process that encrypts the data and modifies the application appropriate in a single pass.
|
13
|
+
It relies on their being a period with nothing accessing the column(s) being migrated, which usually means that the entire application (including background workers and periodic tasks) being shut down, before being restarted.
|
14
|
+
The total downtime will depend on how long it takes to encrypt and write all the data being migrated, which is mostly a function of the amount of data being stored.
|
15
|
+
|
16
|
+
|
17
|
+
## Step 1: Configure the Encrypted Column(s)
|
18
|
+
|
19
|
+
Create an ActiveRecord migration which renames the existing column and creates a new `enquo_*` column with the old name.
|
20
|
+
For example, if you already had a `date_of_birth` column, your migration would look like this:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class EncryptUsersDateOfBirth < ActiveRecord::Migration[7.0]
|
24
|
+
def change
|
25
|
+
rename_column :users, :date_of_birth, :date_of_birth_plaintext
|
26
|
+
add_column :users, :date_of_birth, :enquo_date
|
27
|
+
User.enquo_encrypt_columns(date_of_birth_plaintext: :date_of_birth)
|
28
|
+
remove_column :users, :date_of_birth_plaintext
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
The `Model.encrypt_columns` method loads all the records in the table, and encrypts the value in the plaintext column and writes it to the corresponding encrypted column.
|
34
|
+
|
35
|
+
If you want to encrypt several columns in a single model, you can do so in a single migration, by renaming all the columns and adding `enquo_*` type columns, and then providing the mapping of all columns together in a single `Model.encrypt_columns` call.
|
36
|
+
This is the recommended approach, as it improves efficiency because the records only have to be loaded and saved once.
|
37
|
+
|
38
|
+
If you want to encrypt columns in multiple models in one downtime, just repeat the above steps for each table and model involved.
|
39
|
+
|
40
|
+
|
41
|
+
## Step 2: Modify Queries
|
42
|
+
|
43
|
+
When providing data to a query on an encrypted column, you need to make a call to `Model.enquo` in order to encrypt the value for querying.
|
44
|
+
|
45
|
+
To continue our `date_of_birth` example above, you need to find any queries that reference the `date_of_birth` column, and modify the code to pass the value for the `date_of_birth` column through a call to `User.enquo(:date_of_birth, <value>)`.
|
46
|
+
|
47
|
+
For a query that found all users with a date of birth equal to a query parameter, that originally looked like this:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
User.where(date_of_birth: params[:dob])
|
51
|
+
```
|
52
|
+
|
53
|
+
You'd modify it to look like this, instead:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
User.where(date_of_birth: User.enquo(:date_of_birth, params[:dob]))
|
57
|
+
```
|
58
|
+
|
59
|
+
If the value for the query was passed in as a positional parameter, you just encrypt the value the same way, so that a query might look like this:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
User.where("date_of_birth > ? OR date_of_birth IS NULL", User.enquo(:date_of_birth, params[:dob]))
|
63
|
+
```
|
64
|
+
|
65
|
+
|
66
|
+
## Step 3: Deploy
|
67
|
+
|
68
|
+
Once the above changes are all made and committed to revision control, it's time to commence the downtime.
|
69
|
+
Shutdown all the application servers, background job workers, and anything else that accesses the database, then perform a normal deployment -- running the database migration process before starting the application again.
|
70
|
+
|
71
|
+
The migration may take some time to run, if the table is large.
|
72
|
+
|
73
|
+
|
74
|
+
## Step 4: Enjoy Fully Encrypted Data
|
75
|
+
|
76
|
+
The column(s) you migrated are now fully protected by Enquo's queryable encryption.
|
77
|
+
Relax and enjoy your preferred beverage in celebration!
|
78
|
+
|
79
|
+
|
80
|
+
# Live Data Migration
|
81
|
+
|
82
|
+
Converting the data in a column to be fully encrypted, while avoiding any application downtime, requires making several changes to the application and database schema in alternating sequence.
|
83
|
+
This is necessary to ensure that parts of the application stack running both older and newer code versions can work with the database schema in place at all times.
|
84
|
+
|
85
|
+
> # WORK IN PROGRESS
|
86
|
+
>
|
87
|
+
> This section has not been written out in detail.
|
88
|
+
> The short version is:
|
89
|
+
>
|
90
|
+
> 1. Rename the unencrypted column:
|
91
|
+
> 1. `create_column :users, :date_of_birth_plaintext, :date`
|
92
|
+
> 2. Modify the model to write changes to `date_of_birth` to `date_of_birth_plaintext` as well
|
93
|
+
> 3. Deploy
|
94
|
+
> 4. Migration to copy all `date_of_birth` values to `date_of_birth_plaintext`
|
95
|
+
> 5. Deploy
|
96
|
+
> 6. Modify app to read/query from `date_of_birth_plaintext`, add `date_of_birth` to `User.ignored_columns`
|
97
|
+
> 7. Deploy
|
98
|
+
> 8. `drop_column :users, :date_of_birth`
|
99
|
+
> 2. Create the encrypted column:
|
100
|
+
> 1. `create_column :users, :date_of_birth, :enquo_date`
|
101
|
+
> 2. Modify the model to write changes to `date_of_birth_plaintext` to `date_of_birth` as well, remove `date_of_birth` from `User.ignored_columns`
|
102
|
+
> 3. Deploy
|
103
|
+
> 4. Migration to encrypt all `date_of_birth_plaintext` values into `date_of_birth`
|
104
|
+
> 5. Deploy
|
105
|
+
> 6. Modify app to read/encrypted query from `date_of_birth`, add `date_of_birth_plaintext` to `User.ignored_columns`
|
106
|
+
> 7. Deploy
|
107
|
+
> 8. `drop_column :users, :date_of_birth_plaintext`
|
@@ -0,0 +1 @@
|
|
1
|
+
/.pgdbenv
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "active_enquo"
|
5
|
+
require "pg"
|
6
|
+
|
7
|
+
require_relative "../init"
|
8
|
+
|
9
|
+
def assert_eq(expected, actual)
|
10
|
+
unless expected == actual
|
11
|
+
$stderr.puts "Expected #{expected.inspect} to equal #{actual.inspect}"
|
12
|
+
exit 1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def value(f, v)
|
17
|
+
if ENV.key?("USING_ENQUO")
|
18
|
+
People.enquo(f, v)
|
19
|
+
else
|
20
|
+
v
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ActiveRecord::Base.establish_connection(adapter: ENV.fetch("DBTYPE"))
|
25
|
+
|
26
|
+
assert_eq(["Meyers"], People.where(first_name: value(:first_name, "Seth")).all.map { |p| p.last_name })
|
27
|
+
assert_eq(8, People.where(date_of_birth: value(:date_of_birth, "1980-01-01")..).count)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class EncryptPeopleData < ActiveRecord::Migration[ENV.fetch("AR_VERSION", "7.0").to_f]
|
2
|
+
def up
|
3
|
+
rename_column :people, :first_name, :first_name_plaintext
|
4
|
+
rename_column :people, :last_name, :last_name_plaintext
|
5
|
+
rename_column :people, :date_of_birth, :date_of_birth_plaintext
|
6
|
+
|
7
|
+
add_column :people, :first_name, :enquo_text
|
8
|
+
add_column :people, :last_name, :enquo_text
|
9
|
+
add_column :people, :date_of_birth, :enquo_date
|
10
|
+
|
11
|
+
People.enquo_encrypt_columns(
|
12
|
+
{
|
13
|
+
first_name_plaintext: :first_name,
|
14
|
+
last_name_plaintext: :last_name,
|
15
|
+
date_of_birth_plaintext: :date_of_birth,
|
16
|
+
},
|
17
|
+
# Smol batch size exercises the batching functionality
|
18
|
+
batch_size: 5
|
19
|
+
)
|
20
|
+
|
21
|
+
remove_column :people, :first_name_plaintext
|
22
|
+
remove_column :people, :last_name_plaintext
|
23
|
+
remove_column :people, :date_of_birth_plaintext
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
set -euo pipefail
|
4
|
+
|
5
|
+
. ../helper.sh
|
6
|
+
|
7
|
+
export DBTYPE="postgresql"
|
8
|
+
|
9
|
+
check_enquo_pg_db
|
10
|
+
clear_pg_db
|
11
|
+
|
12
|
+
ar_db_migrate "001"
|
13
|
+
load_people
|
14
|
+
|
15
|
+
./exercise_model
|
16
|
+
|
17
|
+
ar_db_migrate "002"
|
18
|
+
USING_ENQUO="y" ./exercise_model
|
data/e2e_tests/helper.sh
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
if [ -f "../.pgdbenv" ]; then
|
2
|
+
. ../.pgdbenv
|
3
|
+
fi
|
4
|
+
|
5
|
+
check_enquo_pg_db() {
|
6
|
+
if [ "$(psql -tAc "select count(extname) FROM pg_catalog.pg_extension WHERE extname='pg_enquo'")" != "1" ]; then
|
7
|
+
echo "Specified PostgreSQL database does not have the pg_enquo extension." >&2
|
8
|
+
echo "Check your PG* env vars for correctness, and install the extension if needed." >&2
|
9
|
+
exit 1
|
10
|
+
fi
|
11
|
+
}
|
12
|
+
|
13
|
+
clear_pg_db() {
|
14
|
+
for tbl in $(psql -tAc "select relname from pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n on n.oid = c.relnamespace where c.relkind='r' and n.nspname = 'public'"); do
|
15
|
+
psql -c "DROP TABLE $tbl" >/dev/null
|
16
|
+
done
|
17
|
+
for seq in $(psql -tAc "select relname from pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n on n.oid = c.relnamespace where c.relkind='S' and n.nspname = 'public'"); do
|
18
|
+
psql -c "DROP SEQUENCE $seq" >/dev/null
|
19
|
+
done
|
20
|
+
}
|
21
|
+
|
22
|
+
run_ruby() {
|
23
|
+
ruby -r ../init "$@"
|
24
|
+
}
|
25
|
+
|
26
|
+
ar_db_migrate() {
|
27
|
+
local target_version="$1"
|
28
|
+
|
29
|
+
run_ruby -e "ActiveRecord::MigrationContext.new(['migrations']).up($target_version)" >/dev/null
|
30
|
+
}
|
31
|
+
|
32
|
+
load_people() {
|
33
|
+
run_ruby -e "People.create!(JSON.parse(File.read('../people.json')))"
|
34
|
+
}
|
data/e2e_tests/init.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require "active_enquo"
|
3
|
+
|
4
|
+
ActiveEnquo.root_key = Enquo::RootKey::Static.new("f91c5017a2d946403cc90a688266ff32d186aa2a00efd34dcaa86be802e179d0")
|
5
|
+
|
6
|
+
DBTYPE = ENV.fetch("DBTYPE")
|
7
|
+
|
8
|
+
case DBTYPE
|
9
|
+
when "postgresql"
|
10
|
+
require "pg"
|
11
|
+
else
|
12
|
+
raise "Unsupported DBTYPE: #{DBTYPE.inspect}"
|
13
|
+
end
|
14
|
+
|
15
|
+
class People < ActiveRecord::Base
|
16
|
+
end
|
17
|
+
|
18
|
+
ActiveRecord::Base.establish_connection(adapter: DBTYPE)
|
@@ -0,0 +1 @@
|
|
1
|
+
[{"first_name":"Sergio","last_name":"Leone","date_of_birth":"1929-01-03"},{"first_name":"Bradley","last_name":"Cooper","date_of_birth":"1975-01-05"},{"first_name":"Robert","last_name":"Duvall","date_of_birth":"1931-01-05"},{"first_name":"Kate","last_name":"Moss","date_of_birth":"1974-01-16"},{"first_name":"Laura","last_name":"Schlessinger","date_of_birth":"1947-01-16"},{"first_name":"Jason","last_name":"Segel","date_of_birth":"1980-01-18"},{"first_name":"Pete","last_name":"Buttigieg","date_of_birth":"1982-01-19"},{"first_name":"Mischa","last_name":"Barton","date_of_birth":"1986-01-24"},{"first_name":"Shirley","last_name":"Mason","date_of_birth":"1923-01-25"},{"first_name":"Boris","last_name":"Yeltsin","date_of_birth":"1931-02-01"},{"first_name":"Tom","last_name":"Wilkinson","date_of_birth":"1948-02-05"},{"first_name":"Tony","last_name":"Iommi","date_of_birth":"1948-02-19"},{"first_name":"Sri","last_name":"Srinivasan","date_of_birth":"1967-02-23"},{"first_name":"Tony","last_name":"Randall","date_of_birth":"1920-02-26"},{"first_name":"David","last_name":"Blaine","date_of_birth":"1973-04-04"},{"first_name":"Muddy","last_name":"Waters","date_of_birth":"1915-04-04"},{"first_name":"Danny","last_name":"Almonte","date_of_birth":"1987-04-07"},{"first_name":"Meghann","last_name":"Shaughnessy","date_of_birth":"1979-04-13"},{"first_name":"Maisie","last_name":"Williams","date_of_birth":"1997-04-15"},{"first_name":"Immanuel","last_name":"Kant","date_of_birth":"1724-04-22"},{"first_name":"John","last_name":"Oliver","date_of_birth":"1977-04-23"},{"first_name":"Penelope","last_name":"Cruz","date_of_birth":"1974-04-28"},{"first_name":"Anne","last_name":"Parillaud","date_of_birth":"1961-05-06"},{"first_name":"Cate","last_name":"Blanchett","date_of_birth":"1969-05-14"},{"first_name":"Tucker","last_name":"Carlson","date_of_birth":"1969-05-16"},{"first_name":"Nancy","last_name":"Kwan","date_of_birth":"1939-05-19"},{"first_name":"Melissa","last_name":"Etheridge","date_of_birth":"1961-05-29"},{"first_name":"Marissa","last_name":"Mayer","date_of_birth":"1975-05-30"},{"first_name":"Marvin","last_name":"Hamlisch","date_of_birth":"1944-06-02"},{"first_name":"Gwendolyn","last_name":"Brooks","date_of_birth":"1917-06-07"},{"first_name":"Paul","last_name":"Lynde","date_of_birth":"1927-06-13"},{"first_name":"Paul","last_name":"McCartney","date_of_birth":"1942-06-18"},{"first_name":"Susan","last_name":"Hayward","date_of_birth":"1917-06-30"},{"first_name":"Léa","last_name":"Seydoux","date_of_birth":"1985-07-01"},{"first_name":"Amanda","last_name":"Knox","date_of_birth":"1987-07-09"},{"first_name":"Cindy","last_name":"Sheehan","date_of_birth":"1957-07-10"},{"first_name":"Bill","last_name":"Cosby","date_of_birth":"1937-07-12"},{"first_name":"Raymond","last_name":"Chandler","date_of_birth":"1888-07-23"},{"first_name":"Christopher","last_name":"Nolan","date_of_birth":"1970-07-30"},{"first_name":"Melanie","last_name":"Griffith","date_of_birth":"1957-08-09"},{"first_name":"Freddie","last_name":"Gray","date_of_birth":"1989-08-16"},{"first_name":"Steve","last_name":"Case","date_of_birth":"1958-08-21"},{"first_name":"Cal","last_name":"Ripken","date_of_birth":"1960-08-24"},{"first_name":"Regis","last_name":"Philbin","date_of_birth":"1931-08-25"},{"first_name":"Shania","last_name":"Twain","date_of_birth":"1965-08-28"},{"first_name":"Adam","last_name":"Curry","date_of_birth":"1964-09-03"},{"first_name":"Rachel","last_name":"Hunter","date_of_birth":"1969-09-09"},{"first_name":"Bashar","last_name":"al-Assad","date_of_birth":"1965-09-11"},{"first_name":"Amy","last_name":"Poehler","date_of_birth":"1971-09-16"},{"first_name":"Jim","last_name":"Thompson","date_of_birth":"1906-09-27"},{"first_name":"Matt","last_name":"Damon","date_of_birth":"1970-10-08"},{"first_name":"Leopold","last_name":"Senghor","date_of_birth":"1906-10-09"},{"first_name":"William","last_name":"Penn","date_of_birth":"1644-10-14"},{"first_name":"Rebecca","last_name":"Romijn","date_of_birth":"1971-11-06"},{"first_name":"Auguste","last_name":"Rodin","date_of_birth":"1840-11-12"},{"first_name":"Nate","last_name":"Parker","date_of_birth":"1979-11-18"},{"first_name":"Larry","last_name":"Bird","date_of_birth":"1956-12-07"},{"first_name":"Bobby","last_name":"Flay","date_of_birth":"1964-12-10"},{"first_name":"Chris","last_name":"Evert","date_of_birth":"1954-12-21"},{"first_name":"Frank","last_name":"Zappa","date_of_birth":"1940-12-21"},{"first_name":"Estella","last_name":"Warren","date_of_birth":"1978-12-23"},{"first_name":"Carlos","last_name":"Castaneda","date_of_birth":"1925-12-25"},{"first_name":"Seth","last_name":"Meyers","date_of_birth":"1973-12-28"}]
|
data/e2e_tests/run
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
set -euo pipefail
|
4
|
+
|
5
|
+
cd "$(dirname "${BASH_SOURCE[0]}")"
|
6
|
+
|
7
|
+
for t in [0-9]*; do
|
8
|
+
rv="0"
|
9
|
+
cd "$t"
|
10
|
+
|
11
|
+
./run || rv="$?"
|
12
|
+
|
13
|
+
if [ "$rv" != "0" ]; then
|
14
|
+
echo "Test $i failed with exit code $rv" >&2
|
15
|
+
exit 1
|
16
|
+
fi
|
17
|
+
done
|
18
|
+
|
19
|
+
echo "All tests passed."
|
data/lib/active_enquo.rb
CHANGED
@@ -21,7 +21,57 @@ module ActiveEnquo
|
|
21
21
|
RootKey = Enquo::RootKey
|
22
22
|
|
23
23
|
module ActiveRecord
|
24
|
-
module
|
24
|
+
module QueryFilterMangler
|
25
|
+
class Ciphertext < String
|
26
|
+
end
|
27
|
+
private_constant :Ciphertext
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def mangle_query_filter(a)
|
32
|
+
args = a.first
|
33
|
+
if args.is_a?(Hash)
|
34
|
+
args.keys.each do |attr|
|
35
|
+
next unless enquo_attr?(attr)
|
36
|
+
|
37
|
+
if args[attr].is_a?(Array)
|
38
|
+
args[attr] = args[attr].map { |v| maybe_enquo(attr, v) }
|
39
|
+
elsif args[attr].is_a?(Range)
|
40
|
+
r = args[attr]
|
41
|
+
args[attr] = if r.exclude_end?
|
42
|
+
if r.begin.nil?
|
43
|
+
...maybe_enquo(attr, r.end)
|
44
|
+
elsif r.end.nil?
|
45
|
+
(maybe_enquo(attr.r.begin)...)
|
46
|
+
else
|
47
|
+
maybe_enquo(attr.r.begin)...maybe_enquo(attr, r.end)
|
48
|
+
end
|
49
|
+
else
|
50
|
+
if r.begin.nil?
|
51
|
+
..maybe_enquo(attr, r.end)
|
52
|
+
elsif r.end.nil?
|
53
|
+
maybe_enquo(attr.r.begin)..
|
54
|
+
else
|
55
|
+
maybe_enquo(attr.r.begin)..maybe_enquo(attr, r.end)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
else
|
59
|
+
args[attr] = maybe_enquo(attr, args[attr])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def maybe_enquo(attr, v)
|
66
|
+
if v.nil? || v.is_a?(Ciphertext) || v.is_a?(::ActiveRecord::StatementCache::Substitute)
|
67
|
+
v
|
68
|
+
else
|
69
|
+
Ciphertext.new(enquo(attr, v))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module BaseExtension
|
25
75
|
extend ActiveSupport::Concern
|
26
76
|
|
27
77
|
def _read_attribute(attr_name, &block)
|
@@ -51,10 +101,7 @@ module ActiveEnquo
|
|
51
101
|
relation = self.class.arel_table.name
|
52
102
|
field = ::ActiveEnquo.root.field(relation, attr_name)
|
53
103
|
attr_opts = self.class.enquo_attribute_options.fetch(attr_name.to_sym, {})
|
54
|
-
|
55
|
-
:unsafe
|
56
|
-
end
|
57
|
-
db_value = t.encrypt(value, @attributes.fetch_value(@primary_key).to_s, field, safety: safety, no_query: attr_opts[:no_query])
|
104
|
+
db_value = t.encrypt(value, @attributes.fetch_value(@primary_key).to_s, field, **attr_opts)
|
58
105
|
@attributes.write_from_user(attr_name, db_value)
|
59
106
|
else
|
60
107
|
super
|
@@ -62,28 +109,114 @@ module ActiveEnquo
|
|
62
109
|
end
|
63
110
|
|
64
111
|
module ClassMethods
|
65
|
-
|
112
|
+
include QueryFilterMangler
|
113
|
+
|
114
|
+
def find_by(*a)
|
115
|
+
mangle_query_filter(a)
|
116
|
+
super
|
117
|
+
end
|
118
|
+
|
119
|
+
def enquo(attr_name, value_or_meta_id, maybe_value = nil)
|
120
|
+
meta_id, value = if value_or_meta_id.is_a?(Symbol)
|
121
|
+
[value_or_meta_id, maybe_value]
|
122
|
+
else
|
123
|
+
[nil, value_or_meta_id]
|
124
|
+
end
|
125
|
+
|
66
126
|
t = self.attribute_types[attr_name.to_s]
|
67
127
|
if t.is_a?(::ActiveEnquo::Type)
|
68
128
|
relation = self.arel_table.name
|
69
129
|
field = ::ActiveEnquo.root.field(relation, attr_name)
|
70
|
-
|
130
|
+
if meta_id.nil?
|
131
|
+
t.encrypt(value, "", field, enable_reduced_security_operations: true)
|
132
|
+
else
|
133
|
+
t.encrypt_metadata_value(meta_id, value, field)
|
134
|
+
end
|
71
135
|
else
|
72
136
|
raise ArgumentError, "Cannot produce encrypted value on a non-enquo attribute '#{attr_name}'"
|
73
137
|
end
|
74
138
|
end
|
75
139
|
|
140
|
+
def unenquo(attr_name, value, ctx)
|
141
|
+
t = self.attribute_types[attr_name.to_s]
|
142
|
+
if t.is_a?(::ActiveEnquo::Type)
|
143
|
+
relation = self.arel_table.name
|
144
|
+
field = ::ActiveEnquo.root.field(relation, attr_name)
|
145
|
+
begin
|
146
|
+
t.decrypt(value, ctx, field)
|
147
|
+
rescue Enquo::Error
|
148
|
+
t.decrypt(value, "", field)
|
149
|
+
end
|
150
|
+
else
|
151
|
+
raise ArgumentError, "Cannot decrypt value on a non-enquo attribute '#{attr_name}'"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def enquo_attr?(attr_name)
|
156
|
+
self.attribute_types[attr_name.to_s].is_a?(::ActiveEnquo::Type)
|
157
|
+
end
|
158
|
+
|
76
159
|
def enquo_attr(attr_name, opts)
|
160
|
+
if opts.key?(:default)
|
161
|
+
default_value = opts.delete(:default)
|
162
|
+
after_initialize do
|
163
|
+
next if persisted?
|
164
|
+
next unless self.send(attr_name).nil?
|
165
|
+
self.send(:"#{attr_name}=", default_value.duplicable? ? default_value.dup : default_value)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
77
169
|
enquo_attribute_options[attr_name] = @enquo_attribute_options[attr_name].merge(opts)
|
78
170
|
end
|
79
171
|
|
80
172
|
def enquo_attribute_options
|
81
173
|
@enquo_attribute_options ||= Hash.new({})
|
82
174
|
end
|
175
|
+
|
176
|
+
def enquo_encrypt_columns(column_map, batch_size: 10_000)
|
177
|
+
plaintext_columns = column_map.keys
|
178
|
+
relation = self.arel_table.name
|
179
|
+
in_progress = true
|
180
|
+
self.reset_column_information
|
181
|
+
|
182
|
+
while in_progress
|
183
|
+
self.transaction do
|
184
|
+
# The .where("0=1") here is a dummy condition so that the q.or in the .each will work properly
|
185
|
+
q = self.select(self.primary_key).select(plaintext_columns).where("0=1")
|
186
|
+
column_map.each do |pt_col, ct_col|
|
187
|
+
q = q.or(self.where(ct_col => nil).where.not(pt_col => nil))
|
188
|
+
end
|
189
|
+
|
190
|
+
q = q.limit(batch_size).lock
|
191
|
+
|
192
|
+
rows = ::ActiveRecord::Base.connection.exec_query(q.to_sql)
|
193
|
+
if rows.length == 0
|
194
|
+
in_progress = false
|
195
|
+
else
|
196
|
+
rows.each do |row|
|
197
|
+
values = Hash[column_map.map do |pt_col, ct_col|
|
198
|
+
field = ::ActiveEnquo.root.field(relation, ct_col)
|
199
|
+
attr_opts = self.enquo_attribute_options.fetch(ct_col.to_sym, {})
|
200
|
+
t = self.attribute_types[ct_col.to_s]
|
201
|
+
db_value = t.encrypt(row[pt_col.to_s], row[self.primary_key].to_s, field, **attr_opts)
|
202
|
+
|
203
|
+
[ct_col, db_value]
|
204
|
+
end]
|
205
|
+
|
206
|
+
self.where(self.primary_key => row[self.primary_key]).update_all(values)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
83
212
|
end
|
84
213
|
end
|
85
214
|
|
86
215
|
module TableDefinitionExtension
|
216
|
+
def enquo_boolean(name, **options)
|
217
|
+
column(name, :enquo_boolean, **options)
|
218
|
+
end
|
219
|
+
|
87
220
|
def enquo_bigint(name, **options)
|
88
221
|
column(name, :enquo_bigint, **options)
|
89
222
|
end
|
@@ -96,11 +229,28 @@ module ActiveEnquo
|
|
96
229
|
column(name, :enquo_text, **options)
|
97
230
|
end
|
98
231
|
end
|
232
|
+
|
233
|
+
module RelationExtension
|
234
|
+
include QueryFilterMangler
|
235
|
+
extend ActiveSupport::Concern
|
236
|
+
|
237
|
+
def where(*a)
|
238
|
+
mangle_query_filter(a)
|
239
|
+
super
|
240
|
+
end
|
241
|
+
|
242
|
+
def exists?(*a)
|
243
|
+
mangle_query_filter(a)
|
244
|
+
super
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
99
248
|
end
|
100
249
|
|
101
250
|
module Postgres
|
102
251
|
module ConnectionAdapter
|
103
252
|
def initialize_type_map(m = type_map)
|
253
|
+
m.register_type "enquo_boolean", ActiveEnquo::Type::Boolean.new
|
104
254
|
m.register_type "enquo_bigint", ActiveEnquo::Type::Bigint.new
|
105
255
|
m.register_type "enquo_date", ActiveEnquo::Type::Date.new
|
106
256
|
m.register_type "enquo_text", ActiveEnquo::Type::Text.new
|
@@ -111,13 +261,35 @@ module ActiveEnquo
|
|
111
261
|
end
|
112
262
|
|
113
263
|
class Type < ::ActiveRecord::Type::Value
|
264
|
+
class Boolean < Type
|
265
|
+
def type
|
266
|
+
:enquo_boolean
|
267
|
+
end
|
268
|
+
|
269
|
+
def encrypt(value, context, field, enable_reduced_security_operations: false, no_query: false)
|
270
|
+
if value.nil? || value.is_a?(::ActiveRecord::StatementCache::Substitute)
|
271
|
+
value
|
272
|
+
else
|
273
|
+
field.encrypt_boolean(value, context, safety: enable_reduced_security_operations ? :unsafe : true, no_query: no_query)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def decrypt(value, context, field)
|
278
|
+
field.decrypt_boolean(value, context)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
114
282
|
class Bigint < Type
|
115
283
|
def type
|
116
284
|
:enquo_bigint
|
117
285
|
end
|
118
286
|
|
119
|
-
def encrypt(value, context, field,
|
120
|
-
|
287
|
+
def encrypt(value, context, field, enable_reduced_security_operations: false, no_query: false)
|
288
|
+
if value.nil? || value.is_a?(::ActiveRecord::StatementCache::Substitute)
|
289
|
+
value
|
290
|
+
else
|
291
|
+
field.encrypt_i64(value, context, safety: enable_reduced_security_operations ? :unsafe : true, no_query: no_query)
|
292
|
+
end
|
121
293
|
end
|
122
294
|
|
123
295
|
def decrypt(value, context, field)
|
@@ -130,9 +302,13 @@ module ActiveEnquo
|
|
130
302
|
:enquo_date
|
131
303
|
end
|
132
304
|
|
133
|
-
def encrypt(value, context, field,
|
305
|
+
def encrypt(value, context, field, enable_reduced_security_operations: false, no_query: false)
|
134
306
|
value = cast_to_date(value)
|
135
|
-
|
307
|
+
if value.nil?
|
308
|
+
value
|
309
|
+
else
|
310
|
+
field.encrypt_date(value, context, safety: enable_reduced_security_operations ? :unsafe : true, no_query: no_query)
|
311
|
+
end
|
136
312
|
end
|
137
313
|
|
138
314
|
def decrypt(value, context, field)
|
@@ -146,6 +322,8 @@ module ActiveEnquo
|
|
146
322
|
value
|
147
323
|
elsif value.respond_to?(:to_date)
|
148
324
|
value.to_date
|
325
|
+
elsif value.nil? || (value.respond_to?(:empty?) && value.empty?) || value.is_a?(::ActiveRecord::StatementCache::Substitute)
|
326
|
+
nil
|
149
327
|
else
|
150
328
|
Time.parse(value.to_s).to_date
|
151
329
|
end
|
@@ -157,12 +335,33 @@ module ActiveEnquo
|
|
157
335
|
:enquo_text
|
158
336
|
end
|
159
337
|
|
160
|
-
def encrypt(value, context, field,
|
161
|
-
|
338
|
+
def encrypt(value, context, field, enable_reduced_security_operations: false, no_query: false, enable_ordering: false)
|
339
|
+
if enable_ordering && !enable_reduced_security_operations
|
340
|
+
raise ArgumentError, "Cannot enable ordering on an Enquo attribute unless Reduced Security Operations are enabled"
|
341
|
+
end
|
342
|
+
|
343
|
+
if value.nil? || value.is_a?(::ActiveRecord::StatementCache::Substitute)
|
344
|
+
value
|
345
|
+
else
|
346
|
+
field.encrypt_text(value.respond_to?(:encode) ? value.encode("UTF-8") : value, context, safety: enable_reduced_security_operations ? :unsafe : true, no_query: no_query, order_prefix_length: enable_ordering ? 8 : nil)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def encrypt_metadata_value(name, value, field)
|
351
|
+
case name
|
352
|
+
when :length
|
353
|
+
field.encrypt_text_length_query(value)
|
354
|
+
else
|
355
|
+
raise ArgumentError, "Unknown metadata name for Text field: #{name.inspect}"
|
356
|
+
end
|
162
357
|
end
|
163
358
|
|
164
359
|
def decrypt(value, context, field)
|
165
|
-
|
360
|
+
if value.nil?
|
361
|
+
nil
|
362
|
+
else
|
363
|
+
field.decrypt_text(value, context)
|
364
|
+
end
|
166
365
|
end
|
167
366
|
end
|
168
367
|
end
|
@@ -171,11 +370,21 @@ module ActiveEnquo
|
|
171
370
|
class Initializer < Rails::Railtie
|
172
371
|
initializer "active_enquo.root_key" do |app|
|
173
372
|
if app
|
174
|
-
if
|
175
|
-
|
373
|
+
if app.credentials
|
374
|
+
if app.credentials.active_enquo
|
375
|
+
if root_key = app.credentials.active_enquo.root_key
|
376
|
+
ActiveEnquo.root_key = Enquo::RootKey::Static.new(root_key)
|
377
|
+
else
|
378
|
+
Rails.logger.warn "Could not initialize ActiveEnquo, as no active_enquo.root_key credential was found for this environment"
|
379
|
+
end
|
380
|
+
else
|
381
|
+
Rails.logger.warn "Could not initialize ActiveEnquo, as no active_enquo credentials were found for this environment"
|
382
|
+
end
|
176
383
|
else
|
177
|
-
Rails.warn "Could not initialize ActiveEnquo, as no
|
384
|
+
Rails.logger.warn "Could not initialize ActiveEnquo, as no credentials were found for this environment"
|
178
385
|
end
|
386
|
+
else
|
387
|
+
Rails.logger.warn "Could not initialize ActiveEnquo, as no app was found for this environment"
|
179
388
|
end
|
180
389
|
end
|
181
390
|
end
|
@@ -183,13 +392,15 @@ module ActiveEnquo
|
|
183
392
|
end
|
184
393
|
|
185
394
|
ActiveSupport.on_load(:active_record) do
|
186
|
-
::ActiveRecord::
|
395
|
+
::ActiveRecord::Relation.prepend ActiveEnquo::ActiveRecord::RelationExtension
|
396
|
+
::ActiveRecord::Base.include ActiveEnquo::ActiveRecord::BaseExtension
|
187
397
|
|
188
398
|
::ActiveRecord::ConnectionAdapters::Table.include ActiveEnquo::ActiveRecord::TableDefinitionExtension
|
189
399
|
::ActiveRecord::ConnectionAdapters::TableDefinition.include ActiveEnquo::ActiveRecord::TableDefinitionExtension
|
190
400
|
|
191
401
|
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend ActiveEnquo::Postgres::ConnectionAdapter
|
192
402
|
|
403
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_boolean] = { name: "enquo_boolean" }
|
193
404
|
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_bigint] = { name: "enquo_bigint" }
|
194
405
|
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_date] = { name: "enquo_date" }
|
195
406
|
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_text] = { name: "enquo_text" }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_enquo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Palmer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-04-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: enquo-core
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
19
|
+
version: '0.7'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0.
|
26
|
+
version: '0.7'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activerecord
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -195,6 +195,16 @@ files:
|
|
195
195
|
- README.md
|
196
196
|
- active_enquo.gemspec
|
197
197
|
- docs/DEVELOPMENT.md
|
198
|
+
- docs/MIGRATION.md
|
199
|
+
- e2e_tests/.gitignore
|
200
|
+
- e2e_tests/001_direct_migration/exercise_model
|
201
|
+
- e2e_tests/001_direct_migration/migrations/001_create_people_table.rb
|
202
|
+
- e2e_tests/001_direct_migration/migrations/002_encrypt_people_data.rb
|
203
|
+
- e2e_tests/001_direct_migration/run
|
204
|
+
- e2e_tests/helper.sh
|
205
|
+
- e2e_tests/init.rb
|
206
|
+
- e2e_tests/people.json
|
207
|
+
- e2e_tests/run
|
198
208
|
- lib/active_enquo.rb
|
199
209
|
homepage: https://enquo.org
|
200
210
|
licenses: []
|