ActiveEnquo is a Ruby on Rails ActiveRecord extension that works with the pg_enquo Postgres extension to allow you to query encrypted data. This allows you to keep the data you store safe, by encrypting it, without compromising on your application's ability to search for that data and work with it.

Sounds like magic? Well, maybe a little bit. Read our how it works if you're interested in the gory cryptographic details, or read on for how to use it.

Pre-requisites

In order to make use of ActiveRecord extension, you must be running Postgres 11 or higher, with the pg_enquo extension enabled in the database you're working in. See the pg_enquo installation guide for instructions on how to install pg_enquo.

Also, if you're installing this gem from source, you'll need a reasonably recent Rust toolchain installed.

Installation

It's a gem, so the usual methods should work Just Fine:

gem install active_enquo
# OR
echo "gem 'active_enquo'" >> Gemfile

On macOS, and Linux x86-64/aarch64, you'll get a pre-built binary gem that contains everything you need. For other platforms, you'll need to have Rust 1.59.0 or later installed in order to build the native code portion of the gem.

Configuration

The only setting that ActiveEnquo needs is to be given a "root" key, which is used to derive the keys which are used to actually encrypt data.

Step 1: Generate a Root Key

The ActiveEnquo root key MUST be generated by a cryptographically-secure random number generator, and must also be 64 hex digits in length. A good way to generate this key is with the SecureRandom module:

ruby -r securerandom -e 'puts SecureRandom.hex(32)'

Step 2: Configure Your Application

With this key in hand, you need to store it somewhere.

The recommended way to store your root key, at present, is in the Rails credentials store.

  1. Open up the Rails credentials editor:
   rails credentials:edit
  1. Add a section to that file that looks like this:
   active_enquo:
     root_key: "0000000000000000000000000000000000000000000000000000000000000000"
  1. Save and exit the editor. Commit the changes to your revision control system.

Direct Assignment (Only If You Must)

Using the Rails credential store only works if you are using Rails, of course. If you're using ActiveRecord by itself, you must set the root key yourself during application initialization. You do this by assigning a RootKey to ActiveEnquo.root_key, like this:

# DO NOT ACTUALLY PUT YOUR KEY DIRECTLY IN YOUR CODE!!!
ActiveEnquo.root_key = ActiveEnquo::RootKey::Static.new("0000000000000000000000000000000000000000000000000000000000000000")

Preferably, you would pass the key into your application via, say, an environment variable, and then immediately clear the environment variable:

ActiveEnquo.root_key = ActiveEnquo::RootKey::Static.new(ENV.fetch("ENQUO_ROOT_KEY"))
ENV.delete("ENQUO_ROOT_KEY")

Support for cloud keystores, such as AWS KMS, GCP KMS, Azure KeyVault, and HashiCorp Vault, will be implemented sooner or later. If you have a burning desire to see that more on the "sooner" end than "later", PRs are welcome.

Usage

We try to make using ActiveEnquo as simple as possible.

Create Your Encrypted Column

Start by creating a column in your database that uses one of the available enquo_* types, with a Rails migration:

class AddEncryptedBigintColumn < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :date_of_birth, :enquo_date
  end
end

Apply this migration in the usual fashion (rails db:migrate).

Reading and Writing

You can now, without any further ado, use that attribute in your models as you would normally. For example, you can insert a new record:

User.create!([{name: "Clara Bloggs", username: "cbloggs", date_of_birth: Date.new(1970, 1, 1)}])

When you retrieve a record, the value is there for you to read:

User.where(username: "cbloggs").first.date_of_birth.to_s  # => "1970-01-01"

Querying

This is where things get neat.

Performing a query on Enquo-encrypted data is done the same way as on unencrypted data.

You can query for records that have the exact value you're looking for:

User.where(date_of_birth: Date(1970, 1, 1))

Or you can query for users born less than 50 years ago:

User.where(date_of_birth: (Date.today - 50.years))..)

This doesn't seem so magical, until you take a peek in the database, and realise that all the data is still encrypted:

psql> SELECT date_of_birth FROM users WHERE username='cbloggs';
  age
-------
 {"v1":{"a":[<lots of numbers>],"y":[<lots and LOTS of numbers>],<etc etc>}}

Migrating Existing Data to Encrypted Form

This is a topic on which a lot of words can be written. For the sake of tidiness, these words are in a guide of their own.

Indexing and Ordering

To maintain security by default, ActiveEnquo doesn't provide the ability to ORDER BY or index columns by default. This is fine for many situations -- many columns don't need indexes or to be ordered in a query.

For those columns that do need indexes or ORDER BY support, you can enable support for them by setting the enable_reduced_security_operations flag on the attribute, like this:

class User < ApplicationRecord
  # Enables indexing and ORDER BY for this column, at the cost of reduced security
  enquo_attr :age, enable_reduced_security_operations: true
end

Security Considerations

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. Specifically, extra data needs to be stored in the value to enable indexing and ordering. This extra data can be used by an attacker to:

  • Identify all rows which have the same value for the column (although not what that value actually is); and

  • Perform inference attacks to try and determine the approximate or exact value for the column of some or all of the rows.

The practical implications of these attack vectors varies wildly between different types of data, which makes it harder to decide if it's reasonable to allow reduced security operations. Our recommended rule-of-thumb is that if the features you need can only be implemented if you either enable reduced security operations, or leave the data unencrypted, then enable them. Otherwise, leave the default as-is.

Saving Disk Space

While the power of ActiveEnquo is based around being able to query encrypted data, not all columns necessarily need to be queried. If so, you can reduce the disk space requirements for those columns by setting no_query: true for those columns:

class User < ApplicationRecord
  # Disables querying for this column, and saves a heap of bytes on your disk space
  enquo_attr :age, no_query: true
end

More accurate indications of the disk space requirements for the supported data types can be found in the description of each data type.

Future Developments

ActiveEnquo is far from finished. Many more features are coming in the future. See the Enquo project roadmap for details of what we're still intending to implement.

Contributing

For general guidelines for contributions, see CONTRIBUTING.md. Detailed information on developing ActiveEnquo, including how to run the test suite, can be found in docs/DEVELOPMENT.md.

Licence

Unless otherwise stated, everything in this repo is covered by the following licence statement:

Copyright (C) 2022  Matt Palmer <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.