Node.js, Sequelize and GDPR

The fact is that thanks to GDPR our data are (or at least should be) protected stronger than it was in the past. Because of its rules, we, as developers, had to secure our systems and storages more carefully.

One of the common rule is to encrypt data of our clients in the storage system. Which data? It’s tricky, because we have to anonymize our clients, so each column, which could lead us to specific person, should be encrypted;

I want to show you, how we could do the very simple implementation of such encryption in our node.js APIs, by using Sequilize’s hooks.

What the hooks are?

If you use Sequelize, you definitely define some models of your data. The thing worth to say is that each model has its own lifecycle, described by hooks.

Hooks are simple function, which mostly get reference to our record (or array of records) as an argument. In this point we can do whatever we want, with our data; Hooks are very useful, since we can manipulate our data and transform them before we save or after we get them from the storage.

Sequelize provides us a long list of hooks we could use. The most accurate list you can find inside the source of the library here: https://github.com/sequelize/sequelize/blob/v6/lib/hooks.js#L7.

As you may see, some hooks are just proxies of others, so instead of duplicating logic for updates and creates, we can just use beforeSave hook for both of them.

How to encrypt/decrypt data in Sequelize?

Unfortunately, we need to underline that we operate on the reference, so we are forced to mutate our data (and let’s say it loud - it’s not OK). If you would like to try more declarative way and just map an array and return the new one - it won’t work;

Let’s prepare functions for encrypting and decrypting data; We use crypto library and aes-256-cbc algorithm. The code below is based on this gist, slightly changed:

const crypto = require("crypto");

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY.substr(0, 32);
const IV_LENGTH = 16;

function encrypt(text) {
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv(
    "aes-256-cbc",
    Buffer.from(ENCRYPTION_KEY),
    Buffer.from(iv)
  );
  const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);

  return iv.toString("hex") + ":" + encrypted.toString("hex");
}

function decrypt(text) {
  const textParts = text.split(":");
  const iv = Buffer.from(textParts.shift(), "hex");
  const encryptedText = Buffer.from(textParts.join(":"), "hex");
  const decipher = crypto.createDecipheriv(
    "aes-256-cbc",
    Buffer.from(ENCRYPTION_KEY),
    iv
  );
  const decrypted = Buffer.concat([
    decipher.update(encryptedText),
    decipher.final(),
  ]);

  return decrypted.toString();
}

module.exports = { decrypt, encrypt };

As you may see, we use ENV variable (ENCRYPTION_KEY), wich has to be defined before we start an app. IV is random salt, using to make the method stronger and add element of randomness.

How can we apply these functions in Sequelize? Just by using hooks on particular models:

sequelize.define(
  "clients",
  {
    id: {
      type: Sequelize.UUID,
      defaultValue: Sequelize.UUIDV4,
      allowNull: false,
      primaryKey: true,
    },
    fullName: {
      type: Sequelize.STRING(255),
      allowNull: false,
    },
  },
  {
    hooks: {
      afterFind: (result) => {
        if (!result) {
          return;
        }

        result.fullName = result.fullName && decrypt(result.fullName);

        return result;
      },
      beforeSave: (result) => {
        if (result.fullName) {
          result.fullName = encrypt(result.fullName);
        }

        return result;
      },
    },
  }

We used here two hooks: afterFind for queries and beforeSave for all commands on our Model. That’s everything. Our encrypt/decrypt functions take care of the rest.

This simple way allows us to keep our crucial data a bit more secured. Worth the effort.

Caveats

Encrypting values in the database has some defects. First of all - you have to remember, that any change of the key, enforce you to revalidate all records; If you lose it - you will lose all your data. In terms of usage, you are limited by hashed values, so you are not able to search for any value in these columns by using phrases.

And you have to remember to keep your Secret secret. Storing it in ENV variable is some idea, but if your API is hosted on public server, it could be a vulnerability.

Is this the best method?

No. There are a lot of other solutions, how to secure your data by encryption. Like database encryption etc.. But sometimes you have no access to other methods, sometime you just want to encrypt one column in your database: and here comes above solution. By passing this simple hooks, you can easily protect your data.