Encrypting Sensitive Data in Rails 7 with Encrypted Attributes
Tags: webdev, rails, encryption, security
Published: 2023-08-09 06:20:00

**At Zonmaster, we manage a substantial amount of sensitive data, including Personally Identifiable Information (PII), along with other types of data. As part of our agreement with Amazon, we are committed to storing this information securely. While we previously relied on various gems to achieve this, having encryption features built directly into Rails has greatly enhanced our ability to protect this vital information.**

##Security is a top priority in modern web development

Recently I posted an article about moving some data out of our MySQL database and into S3 files. In that article, I mentioned that these columns were encrypted. Because [Zonmaster](https://public.zonmaster.com/) started out life in 2015 it uses a home-grown encryption solution.

But now Rails 7 has introduced a powerful feature to help developers protect sensitive data: encrypted attributes. This built-in functionality provides an additional layer of security that is both easy to implement and robust.

## What Are Encrypted Attributes?

Encrypted attributes allow you to encrypt specific data within your models, ensuring that sensitive information - like passwords or personal details - is stored securely in the database. This feature is included by default in Rails 7, making it accessible to all developers working with this version of the framework.

## Getting Started with Encrypted Attributes

### Step 1: Initialize Encryption Keys

Before you can begin encrypting attributes, you'll need to generate a set of keys for your Rails credentials. This can be done with the following command:

```
rails db:encryption:init
```

This will output something like this:

```jsx
Add this entry to the credentials of the target environment: 

active_record_encryption:
  primary_key: 71TCMk9YKXkBJQSnbdQsRW0qdOcjeNEM
  deterministic_key: tUJgmCDRZTJKZd35qGBFma2LFdUoH4d5
  key_derivation_salt: XI7SAcXcoGdbaZM3zvNp0Wdpm3mx3xqw
```

As it says, add these to your credentials file using `rails credentials:edit`.

### Step 2: Declare Encrypted Attributes

Next, you'll need to specify which attributes you want to encrypt within your model. Here's an example of how to declare an encrypted attribute called `password`:

```
class Product < ApplicationRecord
  encrypts :description # why you would do this I do not know!
end
```

### Step 3: Encrypt and Decrypt Data

You can create a product with an encrypted description like this:

```jsx
irb(main):001:0> product = Product.new(title: 'A title', description: 'a description', price: 25.1)
=> #<Product:0x000000010bae6b98 id: nil, title: "A title", price: 25.1, description: "a description", created_at: nil, updated_at: nil>
irb(main):002:0> product.save!
  TRANSACTION (0.1ms)  begin transaction
  Product Create (1.7ms)  INSERT INTO "products" ("title", "price", "description", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["title", "A title"], ["price", 25.1], ["description", "{\"p\":\"VeK6h5jQH9BS0KAgYQ==\",\"h\":{\"iv\":\"lLXAYRRDdGxDiR9o\",\"at\":\"Kwbu2PH9b//BoI/sVAPTVw==\"}}"], ["created_at", "2023-08-02 05:04:41.881694"], ["updated_at", "2023-08-02 05:04:41.881694"]]
  TRANSACTION (1.4ms)  commit transaction
=> true
```

When you save a product with an encrypted description, the description will be encrypted using the keys you added earlier. 

When you load the record the decryption of the attribute is transparent.

```
irb(main):003:0> product = Product.last
  Product Load (1.4ms)  SELECT "products".* FROM "products" ORDER BY "products"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 
#<Product:0x000000010d53b610
...
irb(main):004:0> product.description
=> "a description"
```

## Querying

What this does mean is that you cannot query the model.

```jsx
irb(main):005:0> product = Product.where(description: 'a description')
  Product Load (0.5ms)  SELECT "products".* FROM "products" WHERE "products"."description" = ?  [["description", "{\"p\":\"wdnix8h+WE2ffrk9Lg==\",\"h\":{\"iv\":\"t99H28mm/MzcsxuB\",\"at\":\"SHM+/SmpAVV/sBRBopKm9w==\"}}"]]
=> []
```

But, if you make the encrypted column `deterministic` you can!

```jsx
class Product < ApplicationRecord
  validates :title, presence: true

  encrypts :description
  encrypts :title, deterministic: true # now encrypt the title deterministically
end
```

Now you can seamlessly query the model, just as if the column was not encrypted at all (note: this does probably decrease the security of things, but probably only marginally. Unless you work for a three letter organization it is probably fine).

```jsx
irb(main):001:0> product = Product.new(title: 'Phil Title', description: 'phil description', price: 99.99)
=> #<Product:0x00000001050afae0 id: nil, title: "Phil Title", price: 99.99, description: "phil description", created_at: nil, updated_at: nil>
irb(main):002:0> product.save!
  TRANSACTION (0.1ms)  begin transaction
  Product Create (1.2ms)  INSERT INTO "products" ("title", "price", "description", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["title", "{\"p\":\"1RJHqWOus4fGDw==\",\"h\":{\"iv\":\"+/zLK97EHgeIUTg6\",\"at\":\"tzj449jTyTKIwnRXAQboQg==\"}}"], ["price", 99.99], ["description", "{\"p\":\"25jfOg0Z6uXc2gDxdW5O1Q==\",\"h\":{\"iv\":\"mfpYQ31+AdSpsRS9\",\"at\":\"ILCst1xDituOS7eiAQ5PyA==\"}}"], ["created_at", "2023-08-02 05:11:39.280148"], ["updated_at", "2023-08-02 05:11:39.280148"]]
  TRANSACTION (0.9ms)  commit transaction
=> true
irb(main):003:0> Product.where(title: 'Phil Title')
  Product Load (0.2ms)  SELECT "products".* FROM "products" WHERE "products"."title" = ?  [["title", "{\"p\":\"1RJHqWOus4fGDw==\",\"h\":{\"iv\":\"+/zLK97EHgeIUTg6\",\"at\":\"tzj449jTyTKIwnRXAQboQg==\"}}"]]
=> 
[#<Product:0x00000001062b3160
  id: 2,
  title: "Phil Title",
  price: 99.99,
  description: "phil description",
  created_at: Wed, 02 Aug 2023 05:11:39.280148000 UTC +00:00,
  updated_at: Wed, 02 Aug 2023 05:11:39.280148000 UTC +00:00>]
```

## Additional Features

Encrypted attributes in Rails 7 also offer several other features, including:

- Support for ignoring case when querying encrypted data.
- Support for migrating unencrypted data to encrypted data.
    - Especially cool because it lets you turn an unencrypted column into an encrypted one and migrate the data later.
- Support for multiple encryption schemes.

For a comprehensive guide, refer to the [Rails Guides on Active Record Encryption](https://guides.rubyonrails.org/active_record_encryption.html).

## Benefits of Using Encrypted Attributes

Utilizing encrypted attributes in Rails 7 offers a trifecta of compelling advantages. First and foremost, it provides **Enhanced Security**, enabling developers to protect sensitive data such as passwords and personal information from unauthorized access. This encryption ensures that even if data is breached, it remains unreadable without the proper decryption keys. Secondly, the feature boasts **Ease of Use**, with a simple implementation process that doesn't compromise usability. Developers can quickly encrypt attributes without needing extensive knowledge of cryptography. Lastly, encrypted attributes are **Fully Integrated into the Rails Framework**, ensuring seamless compatibility and support within the Rails ecosystem. This integration means that developers can leverage this powerful security feature without having to navigate the complexities of third-party solutions. Together, these benefits make encrypted attributes an attractive option for any Rails developer looking to enhance data security in their applications.

If you're developing an application that handles sensitive data, consider using encrypted attributes in Rails 7. It's a straightforward and effective way to bolster your security measures, ensuring that your data remains safe and secure.

You can find me on [Twitter](https://twitter.com/psmy) where I talk about Ruby on Rails, my company [Zonmaster](https://public.zonmaster.com), and life in general. If you’re looking for help with your Rails project drop me a note on Twitter or [LinkedIn](https://linkedin.com/in/philsmy)