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.
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:
id
is an integer referring to the entry’s ID in the table.type
is a string (or enum) referring to the type of identification number represented — e.g. “SSN”, “ITIN”, etc.value
is a string containing the actual value of the identification number — e.g. the hypothetical SSN “123456789”.filerId
is an integer representing a foreign key, referring to the ID of the filer (user) to whom this identification number belongs.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.
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: