In the course of helping users access their legal rights through bankruptcy, Upsolve handles a lot of sensitive personal data. This includes social security numbers and other information which would be dangerous in the hands of malicious actors. One precaution we take to preserve user privacy is encrypting the identification numbers stored in our database. This way, even if an unauthorized individual were to somehow access a copy of our database, they would still be unable to obtain certain crucial information. Here’s an overview of how we encrypt identification numbers for user data security.

A basic Objection.js Model

We use Objection.js as our object-relational mapping library, which bridges the gap between models in our Node.js backend and tables in our PostgreSQL database. Each data model in our codebase extends the Model Objection class and tacks on additional methods for data management.

For example, consider the following bare-bones IdentificationNumber class, which could be used to store social security numbers (SSNs), individual taxpayer identification numbers (ITINs), and more:

class IdentificationNumber extends Model {
	id; // integer
	type; // string
	value; // string
	filerId; // integer (foreign key)

	static get tableName() {
		return "identification_numbers";
	}

	static get relationMappings() {
		return {
			filers: {
				relation: Model.BelongsToOneRelation,
				modelClass: Filer,
				join: {
					from: "identification_numbers.filerId",
					to: "filers.id",
				},
			},
		};
	}

	static create = async (newProps) =>
		IdentificationNumber.query().insertAndFetch({ ...newProps });
}

As indicated by the tableName getter, we would have an identification_numbers table in our database. The entries in this table would be represented in code by the IdentificationNumber class, with the following properties:

We would also have a separate filers table containing users of our bankruptcy tool. The entries in this table would be represented by the Filer model class. In the relationMappings getter above, we define a BelongsToOneRelation between the IdentificationNumber and Filer models, stating that each IdentificationNumber instance belongs to one Filer. After all, federal identification numbers must be unique! Finally, we have a create method which inserts a new entry into the identification_numbers table using the Objection.js query builder.

Encrypting strings in Node.js

Now we want to encrypt the value column in the identification_numbers table, which contains the actual identification number. First, let’s see how a standalone string encryption function would work in Node.

Our encryption method and pattern are adapted from the objection-encrypt library by Dialogtrail, which uses Node’s built-in crypto module to compute hashes. This module is itself a wrapper around OpenSSL. Here’s how the encrypt function in objection-encrypt turns a plaintext input into ciphertext using the crypto module:

  1. It generates a cryptographically strong pseudorandom series of bytes, which will be used as an initialization vector (IV) for the cipher.
  2. It creates a cipher—a step-by-step encryption procedure—using the IV, an algorithm of your choice (e.g. AES-256-CBC), and your private encryption key, which you’ve presumably stored in your runtime environment and passed into this function. The initialization vector prevents us from generating the same ciphertext every time we are given the same initial plaintext.
  3. The cipher is used to encrypt your original plaintext string.
  4. The IV is stringified and appended to the ciphertext, with the two parts separated by a delimiting colon. This is the encrypted value to store in the database, and we can later split out the IV and ciphertext during decryption.