cipherstash-archive / cipherstash-rails-demo-app

Use CipherStash to encrypt and query fake medical data for patients

Home Page:https://docs.cipherstash.com/tutorials/rails-getting-started/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

CipherStash Rails App Demo

This repo contains a Rails app which can be used to demonstrate how to configure CipherStash Protect for a Rails app.

The demo app is an admin view of generated fake medical data for patients.

To view the the Demo app with the completed code checkout the branch completed-demo-app.

Running the Demo

Prerequisites

If you're using asdf, we ship a .tool-versions you can use to set these up:

asdf install

Get started

  1. Install dependencies:
bundle install
  1. Create db, run migrations and seed db with dummy patient data:
rails db:setup
  1. Run rails server:
rails s
  1. Go to the patients dashboard.

We now have a running Rails application to use to learn how to configure CipherStash to encrypt data.

Install the CipherStash CLI

The CipherStash CLI is used to manage your encryption schema.

The encryption schema defines what encrypted indexes exist, and what queries you can perform on those indexes.

On macOS

Install via Homebrew:

brew install cipherstash/tap/stash

Note

If macOS asks you whether you are sure you want to open "stash", please select "Open".

On Linux

Download the binary for your platform:

  1. Make the binary executable:

    # on x86_64
    chmod +x $path_to/stash-x86_64-unknown-linux-gnu
    
    # on ARM64
    chmod +x $path_to/stash-aarch64-unknown-linux-gnu
  2. Rename the binary:

    # on x86_64
    mv stash-x86_64-unknown-linux-gnu stash
    
    # on ARM64
    mv stash-aarch64-unknown-linux-gnu stash
  3. Place the binary on your $PATH, so you can run it.

Get a CipherStash account and workspace

To use CipherStash you'll need a CipherStash account and workspace.

You can signup from the CLI:

stash signup

Your browser will open to https://cipherstash.com/signup/stash where you can sign up with either your GitHub account, or a standalone email.

Install the CipherStash database driver

The CipherStash database driver transparently maps SQL statements to encrypted database columns.

We need to add it to the Rails app, and tell Rails to use it.

Add the activerecord-cipherstash-pg-adapter to your Gemfile:

gem "activerecord-cipherstash-pg-adapter"

Run bundle install.

And update the default adapter settings in config/database.yml with postgres_cipherstash:

default: &default
  adapter: postgres_cipherstash

Log in

Make sure stash is logged in:

stash login

This will save a special token stash will use for talking to CipherStash.

Create a dataset

Next, we need to create a dataset for tracking what data needs to be encrypted.

A dataset holds configuration for one or more database tables that contain data to be encrypted.

Create our first dataset by running:

stash datasets create patients --description "Data about patients"

The output will look like this:

Dataset created:
ID         : <a UUID style ID>
Name       : patients
Description: Data about patients

Note down the dataset ID, as you'll need it in step 3.

Create a client

Next we need to create a client.

A client allows an application to programatically access a dataset.

A dataset can have many clients (for example, different applications working with the same data), but a client belongs to exactly one dataset.

Use the dataset ID from step 2 to create a client (making sure you substitute your own dataset ID):

stash clients create --dataset-id $DATASET_ID "Rails app"

The output will look like this:

Client created:
Client ID  : <a UUID style ID>
Name       : Rails
Description:
Dataset ID : <your provided dataset ID>

#################################################
#                                               #
#  Copy and store these credentials securely.   #
#                                               #
#  THIS IS THE LAST TIME YOU WILL SEE THE KEY.  #
#                                               #
#################################################

Client ID          : <a UUID style ID>

Client Key [hex]   : <a long hex string>

Note down the client key somewhere safe, like a password vault. You will only ever see this credential once. This is your personal key, and you should not share it.

Set these as environment variables in a .envrc file using the below variable names:

export CS_CLIENT_KEY=
export CS_CLIENT_ID=

If you are using direnv run:

direnv allow

If you're not using direnv, source these variables by running:

source .envrc

Push the dataset configuration

Now we need to configure what columns are encrypted, and what indexes we want on those columns.

This configuration is used by the CipherStash driver to transparently rewrite your app's SQL queries to use the underlying encrypted columns.

Our demo rails app has a schema that looks like this:

class CreatePatients < ActiveRecord::Migration[7.1]
  def change
    create_table :patients do |t|
      t.string :full_name
      t.string :email
      t.date :dob
      t.float :weight
      t.string :allergies
      t.string :medications

      t.timestamps
    end
  end
end

We will want to encrypt all columns, as they contain sensitive information.

We do this with a configuration file which is in the root of the rails demo titled dataset.yml:

This configuration file defines two types of encrypted indexes for the columns we want to protect:

  • A match index on the full_name, email, allergies and medications columns, for full text matches
  • A ore index on the full_name, email, dob and weight columns, for sorting and range queries

Now we push this configuration to CipherStash:

stash datasets config upload --file dataset.yml --client-id $CS_CLIENT_ID --client-key $CS_CLIENT_KEY

Add and apply migrations

The first migration to run, is the install of the Protect custom types into your database.

This migration adds in the custom types ore_64_8_v1 and ore_64_8_v1_term.

  • ore_64_8_v1 is used for string and text types.
  • ore_64_8_v1_term is used for non string types.

We do this by creating a Rails migration:

rails generate migration AddProtectDatabaseExtensions

And adding the following code:

class AddProtectDatabaseExtensions < ActiveRecord::Migration[7.0]
  def up
    ActiveRecord::ConnectionAdapters::CipherStashPG.install
  end

  def down
    ActiveRecord::ConnectionAdapters::CipherStashPG.uninstall
  end
end

Apply the migration:

rails db:migrate

The CipherStash driver works by rewriting your app's SQL queries to use the underlying encrypted columns.

To set up those encrypted columns, generate another Rails migration:

rails generate migration AddProtectEncryptedColumnsToPatientsTable

And add the following code:

class AddProtectEncryptedColumnsToPatientsTable < ActiveRecord::Migration[7.0]
  def change
    add_column :patients, :__full_name_encrypted, :text
    add_column :patients, :__full_name_match, :integer, limit: 2, array: true
    add_column :patients, :__full_name_ore, :ore_64_8_v1
    add_column :patients, :__full_name_unique, :text

    add_column :patients, :__email_encrypted, :text
    add_column :patients, :__email_match, :integer, limit: 2, array: true
    add_column :patients, :__email_ore, :ore_64_8_v1
    add_column :patients, :__email_unique, :text

    add_column :patients, :__dob_encrypted, :text
    add_column :patients, :__dob_ore, :ore_64_8_v1_term

    add_column :patients, :__weight_encrypted, :text
    add_column :patients, :__weight_ore, :ore_64_8_v1_term

    add_column :patients, :__allergies_encrypted, :text
    add_column :patients, :__allergies_match, :integer, limit: 2, array: true
    add_column :patients, :__allergies_ore, :ore_64_8_v1
    add_column :patients, :__allergies_unique, :text

    add_column :patients, :__medications_encrypted, :text
    add_column :patients, :__medications_match, :integer, limit: 2, array: true
    add_column :patients, :__medications_ore, :ore_64_8_v1
    add_column :patients, :__medications_unique, :text

    # Add indexes to the encrypted columns.
    add_index :patients, :__full_name_ore
    add_index :patients, :__email_ore
    add_index :patients, :__dob_ore
    add_index :patients, :__weight_ore
    add_index :patients, :__allergies_ore
    add_index :patients, :__medications_ore

    add_index :patients, :__full_name_match, using: :gin
    add_index :patients, :__email_match, using: :gin
    add_index :patients, :__allergies_match, using: :gin
    add_index :patients, :__medications_match, using: :gin
  end
end

The _encrypted columns are the encrypted values, and the _match and _ore columns are the encrypted indexes.

Apply the migration:

rails db:migrate

Encrypt the sensitive data

Now we have the necessary database structure in place, it's time to encrypt your data.

rails 'cipherstash:migrate[Patient]'

This will pull the unencrypted data, encrypt it, and write it back to the new columns.

Update your model

Add the below to the Patient model.

class Patient < ApplicationRecord
  self.ignored_columns = %w[wildcardoperatorfix]
end

Test querying records via Rails console

The provided CipherStash configuration in the dataset.yml file sets all columns to the plaintext-duplicate mode.

In this mode, all data is read from the plaintext fields, but writes will save both plaintext and ciphertext.

To test that queries are working properly, change all columns in the dataset.yml to use encrypted-duplicate mode.

mode: encrypted-duplicate

In this mode all data is read from ciphertext fields and writes will save both plaintext and ciphertext.

Push this configration to CipherStash:

stash datasets config upload --file dataset.yml --client-id $CS_CLIENT_ID --client-key $CS_CLIENT_KEY

Open your Rails console:

rails console

Create a patient:

Patient.create(full_name: "Grace Hopper", email: "grace@hopper.example", dob: Date.parse("9 December 1906"))

In psql rails_demo, verify that the data is encrypted;

SELECT __full_name_encrypted, __full_name_match, __full_name_ore FROM patients LIMIT 5;

Now back in the Rails console, to find that new record by email address:

Patient.where(email: "grace@hopper.example")

This will return a result that looks like this:

Patient Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."email" = ?  [["email", "grace@hopper.example"]]
=>
[#<User:0x0000000119cd47d0
  id: 1,
  full_name: "Grace Hopper",
  email: "grace@hopper.example",
  created_at: Wed, 15 Feb 2023 22:37:08.134554000 UTC +00:00,
  updated_at: Wed, 15 Feb 2022 22:37:08.134554000 UTC +00:00]

To order users alphabetically by name, do:

Patient.order(:full_name)

Test querying records via UI

Start your Rails server:

rails s

Go to the patients dashboard.

Patient Dashboard

Create a patient:

  • Click on new patient
  • Complete patient details
  • Click on Create Patient

Use the filters on the side to perform queries.

Patient Filters

Dropping plaintext columns

Once you are sure that the app is working correctly, update the column mode to encrypted mode in the dataset.yml file.

mode: encrypted

This tells the CipherStash driver to only read and write from the encrypted columns.

Push this configration to CipherStash:

stash datasets config upload --file dataset.yml --client-id $CS_CLIENT_ID --client-key $CS_CLIENT_KEY

In this mode all data is encrypted and plaintext columns are completely ignored.

Once you have verified that the app is working correctly, you can create a migration that drops the original columns.

rails generate migration DropPlaintextColumnsFromPatientsTable

And add the following code:

class DropPlaintextColumnsFromPatientsTable < ActiveRecord::Migration[7.0]
  def change
    remove_column :patients, :full_name
    remove_column :patients, :email
    remove_column :patients, :dob
    remove_column :patients, :weight
    remove_column :patients, :allergies
    remove_column :patients, :medications
  end
end

Warning

Once you remove the plaintext columns, anything that hasn't been encrypted will be lost.

Before you drop plaintext columns in a real-world application, it is very important that you:

Once you're sure that you're ready to drop the plaintext columns, run the migration:

Run:

rails db:migrate

In order for the encrypted mode to work after the plaintext columns have been dropped, the types of the CipherStash encrypted columns must be specified in the model.

Uncomment this in your Patient model in app/models/patient.rb:

  # Note that the types of CipherStash-protected columns must be specified here in
  # order to drop the original plaintext columns and for "encrypted" mode to work.
  attribute :full_name, :string
  attribute :email, :string
  attribute :dob, :date
  attribute :weight, :float
  attribute :allergies, :string
  attribute :medications, :string
  # The rails demo uses ActiveAdmin, which uses Ransack for the filters.
  # For the dob and weight filters to continue to work, the below types need to be added to the model.
  ransacker :dob, type: :date
  ransacker :weight, type: :numeric

Start up the Rails server:

rails s

Go to the patients dashboard.

Viewing logs of encryptions and decryptions

The CipherStash driver creates a local log of encryptions and decryptions for a given workspace in ~/.cipherstash/<your workspace id>.

To see a real time log of cryptography operations, run:

tail -F ~/.cipherstash/*/decryptions.log

The above guide is also published in our getting started guide.

About

Use CipherStash to encrypt and query fake medical data for patients

https://docs.cipherstash.com/tutorials/rails-getting-started/

License:MIT License


Languages

Language:Ruby 80.1%Language:HTML 15.0%Language:JavaScript 2.4%Language:CSS 1.4%Language:SCSS 1.1%