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: []
         |