27
loading...
This website collects cookies to deliver better user experience
encrypts
declaration in a model. This new feature provides a layer of encryption that sits between our application code and the database. In essence, when our data using Active Record Encryption has been loaded into an AR object, it will be unencrypted, and when it is sitting in the database it will be encrypted.bin/rails db:encryption:init
, which will output something like:Add this entry to the credentials of the target environment:
active_record_encryption:
primary_key: zxMXS0hBbpa5BzRKPv9HOSF9etBySiHQ
deterministic_key: 0pM2UHHBQr1kf1irO6JgakcSOXu0r1Vn
key_derivation_salt: I5AkViD0UJhSqK3NY49Zvsls3ZoifyXx
primary key
is used to derive the root encryption key for non-deterministic encryption. Note that the primary_key
value in the credentials file can also be a list of keys.deterministic_key
is used for deterministic encryption. If you recall from the section on determinism above, we'll get the same result if we encrypt the same data with this key multiple times. Currently, encrypts does not support using a list of keys for deterministic encryption. If we want to completely disable deterministic encryption, not providing the key is a sure-fire off switch.key_derivation_salt
is used to derive encryption keys.config.active_record.encryption
namespace and I'd encourage reading through them if you are going to use this feature. I believe you'll find that most of the options have pretty reasonable defaults.config.active_record.encryption.extend_queries
option as it defaults to false
, but enabling it has several implications:config.active_record.encryption.support_unencrypted_data
for this)Dog
model with a toy_location
field (dogs like to hide their toys, you know) that needs encryption, our model would look like:class Dog < ApplicationRecord
encrypts :toy_location
> dog = Dog.create!(name: 'Bruno', toy_location: 'top secret')
> result = Dog.connection.execute('SELECT toy_location FROM dogs LIMIT 1').first
(1.4ms) SELECT toy_location FROM dogs LIMIT 1
=> {"toy_location"=>"{\"p\":\"oVgEJvRaX6DJvA==\",\"h\":{\"iv\":\"WYypcKysgBY05Tum\",\"at\":\"OaBswq+wyriuRQO8yCVD3w==\"}}"}
> JSON.parse(result['toy_location'])
=> {"p"=>"oVgEJvRaX6DJvA==", "h"=>{"iv"=>"WYypcKysgBY05Tum", "at"=>"OaBswq+wyriuRQO8yCVD3w=="}}
ActiveRecord::Encryption::Properties::DEFAULT_PROPERTIES
constant. p
is the payload, aka the encrypted plaintext. h
is a Hash of headers that contain information relating to the encryption operation. Here, iv
is the initialization vector the plaintext was encrypted with - more about this in the next section on searching, and at
is an auth_tag
that will be used during the decryption process to verify that the encrypted text hasn't been altered. You may notice other headers from the DEFAULT_PROPERTIES
Hash above depending on how your encryption is set up and being used.Dog
we created above by his name:> Dog.find_by!(name: 'Bruno').toy_location
=> <Dog id: 1, name: "Bruno", toy_location: "top secret", created_at: "2021-05-28 22:41:23.142635000 +0000", updated_at: "2021-05-28 22:41:23.142635000 +0000">
toy_location
instead of his name? We can do that just like we would if the field were not encrypted:> dog = Dog.find_by!(toy_location: 'top secret')
Dog Load (2.1ms) SELECT "dogs".* FROM "dogs" WHERE "dogs"."toy_location" = ? LIMIT ? [["toy_location", "{\"p\":\"oVgEJvRaX6DJvA==\",\"h\":{\"iv\":\"WYypcKysgBY05Tum\",\"at\":\"OaBswq+wyriuRQO8yCVD3w==\"}}"], ["LIMIT", 1]]
=> #<Dog id: 1, name: "Bruno", toy_location: "top secret", created_at: "2021-05-28 22:41:23.142635000 +0000", updated_at: "2021-05-28 22:41:23.142635000 +0000">
Dog
table already existed and had a pre-existing toy_location
column on it that was not encrypted?Dog
record with top secret
(unencrypted) as its toy_location
, that query aint gonna find it. Also, it seems pretty likely that if we try to load a Dog
record with stored plaintext into memory, Rails is going to have problems when it attempts to decrypt the plaintext.config.active_record.encryption.support_unencrypted_data
configuration option.Dog Load (0.3ms) SELECT "dogs".* FROM "dogs" WHERE "dogs"."toy_location" IN (?, ?) LIMIT ? [["toy_location", "{\"p\":\"Bd+/TzEysF2CCQ==\",\"h\":{\"iv\":\"R2IUJJ+EmnDnZvQP\",\"at\":\"zqG5WAJql1zgctRCPpoBkQ==\"}}"], ["toy_location", "top secret"], ["LIMIT", 1]]
Dog.where(toy_location: ['Top secret', 'top secret'])
.downcase: true
on our encrypts declaration. This will cause the text to be downcased before it is stored. ActiveRecord will automatically downcase our search text when performing queries. The downside here is that all case information is lost when it is downcased. Sorry to be a downer.ignore_case: true
on the encrypts declaration and add an original_column_name
column to our database (eg. original_toy_location
). With this in place, if we created a dog with an uppercase letter in the encrypted field:Dog.create!(name: 'Max', toy_location: 'Top secret')
toy_location
column would be populated with the encrypted form of 'top secret'
(the value downcased), and the original_toy_location
column will have the encrypted form of 'Top secret'
(the value we set).toy_location
column, and the model's toy_location
attribute would be populated from the original_toy_location
column when it is loaded into memory.toy_location
column is encrypted deterministically in this situation (so it can be searched), the original_toy_location
column appears to be encrypted non-deterministically. This makes sense, since that column does not need to support searching. This can be confirmed by comparing the toy_location
and original_toy_location
values for two records with the same plaintext value. As you can see below, they have the same stored values (initialization vector, payload, etc) for the toy_location
column (searchable and downcased), and different stored values for the original_toy_location
column (not searchable, case preserved):{ "toy_location" => "{\"p\":\"Bd+/TzEysF2CCQ==\",\"h\":{\"iv\":\"R2IUJJ+EmnDnZvQP\",\"at\":\"zqG5WAJql1zgctRCPpoBkQ==\"}}",
"original_toy_location" => "{\"p\":\"5syLqDK6GCbBDw==\",\"h\":{\"iv\":\"KBGp4FrI7oL4/a3p\",\"at\":\"JnH6hxLX35cAwroImk2XqQ==\"}}" },
"toy_location" => "{\"p\":\"Bd+/TzEysF2CCQ==\",\"h\":{\"iv\":\"R2IUJJ+EmnDnZvQP\",\"at\":\"zqG5WAJql1zgctRCPpoBkQ==\"}}",
"original_toy_location" => "{\"p\":\"0246w4+SSqqlJw==\",\"h\":{\"iv\":\"1uEnjlCNot9sYNgR\",\"at\":\"UhkhK6YlOTxJg75juqIMGA==\"}}" }
unique
constraints. If we need to ensure uniqueness in any of our encrypted columns, there are some things to be aware of. Be sure to read up on it first.LIKE
query, for example, won't work. It also means that any queries done on encrypted columns will need to go through Rails and ActiveRecord vs being manually crafted in SQL.