active_enquo 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e643c69efa8243d95e44c59a6d49f20c40a23dc3a914da1d79342fa5d3969ca9
4
- data.tar.gz: fcb70caad8b2d949fca672198e269e574924cd63748dfa221164ec6a1eb7842d
3
+ metadata.gz: 9d538e223b9e7485c4b08cf8ac38f2cc6a08f5d3c3b72426167606fdc7a5fdf5
4
+ data.tar.gz: 33e80addf4beb8bbe8f5328cbfd093cb70264ec2804d148026cbdabbd36cd1b8
5
5
  SHA512:
6
- metadata.gz: 7ce8dd63cae62013d026ddf7200878975471bf30c157e059e50b269124e886a219ebb526581038bed3fed1de00500416bfdee7740497c1af2b775c90c46dfd53
7
- data.tar.gz: 02a1944917d45d1517cd5143dbe56c05c28f8c8a2fffe5840f317ce865a944aa563a04560f3e39773eadb7f2ca5a0f54835bdab2f4765e5aacc456c9a88f88b8
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, with one exception: you need to wrap the values of the query in `<Model>.enquo` calls, as in the examples below.
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(age: User.enquo(:date_of_birth, Date(1970, 1, 1)))
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(age: User.enquo(:date, Date.today - 50.years)..)
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.6"
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,11 @@
1
+ class CreatePeopleTable < ActiveRecord::Migration[ENV.fetch("AR_VERSION", "7.0").to_f]
2
+ def change
3
+ create_table :people do |t|
4
+ t.string :first_name
5
+ t.string :last_name
6
+ t.date :date_of_birth
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -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
@@ -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 ModelExtension
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
- safety = if attr_opts[:enable_reduced_security_operations]
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
- def enquo(attr_name, value)
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
- t.encrypt(value, "", field, safety: :unsafe)
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, safety: true, no_query: false)
120
- field.encrypt_i64(value, context, safety: safety, no_query: no_query)
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, safety: true, no_query: false)
305
+ def encrypt(value, context, field, enable_reduced_security_operations: false, no_query: false)
134
306
  value = cast_to_date(value)
135
- field.encrypt_date(value, context, safety: safety, no_query: no_query)
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, safety: true, no_query: false)
161
- field.encrypt_text(value, context, safety: safety, no_query: no_query)
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
- field.decrypt_text(value, context)
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 root_key = app.credentials.active_enquo.root_key
175
- ActiveEnquo.root_key = Enquo::RootKey::Static.new(root_key)
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 active_enquo.root_key credential was found for this environment"
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::Base.send :include, ActiveEnquo::ActiveRecord::ModelExtension
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.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: 2022-11-08 00:00:00.000000000 Z
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.6'
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.6'
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: []