keyring-node
v1.0.1
Published
Encryption-at-rest with key rotation made easy, incorporating seamless sequelize integration.
Downloads
3
Readme
Keyring Node.js
Project
Basic encryption for data at rest with the ability to rotate keys in Node.js.
Technologies
- Node.js
Functionalities
The keyring is not intended for encrypting passwords, for password encryption, you should use something like bcrypt. It is designed for encrypting sensitive data that you will need to access in plain text, such as storing OAuth tokens from users. Passwords do not fall under this category.
This package is entirely separate from any storage mechanisms; its purpose is to provide a few functions that can be easily integrated with any ORM. However, it does include a small plugin that works with Sequelize.
Installation
Add package to your package.json using Yarn.
yarn add keyring-node
or NPM
npm i keyring-node
Using
Encryption
By default, the encryption algorithm used is AES-128-CBC, which requires 16-byte keys. However, for this encryption process, you must use a key that is double the size (32 bytes) since half of it will be used to generate the HMAC (Hash-based Message Authentication Code). The first 16 bytes of the key will serve as the encryption key, while the last 16 bytes will be used for HMAC generation.
It is advisable to use random data that is base64-encoded for generating these keys. You can conveniently create keys by using the following command:
$ dd if=/dev/urandom bs=32 count=1 2>/dev/null | openssl base64 -A
qUjOJFgZsZbTICsN0TMkKqUvSgObYxnkHDsazTqE5tM=
Include the result of this command in the value
section of the key description
in the keyring. Half this key is used for encryption, and half for the HMAC.
Key size
The key size depends on the algorithm being used. The key size should be double the size as half of it is used for HMAC computation.
aes-128-cbc
: 16 bytes (encryption) + 16 bytes (HMAC).aes-192-cbc
: 24 bytes (encryption) + 24 bytes (HMAC).aes-256-cbc
: 32 bytes (encryption) + 32 bytes (HMAC).
About the encrypted message
The encrypted message generated by the keyring includes an Initialization Vector (IV), which is crucial for ensuring the security of the encryption process. The IV should be both unpredictable and unique, preferably generated using cryptographic random methods. Unlike encryption keys, the IV does not need to be kept secret and is typically included alongside the ciphertext without encryption.
To construct the final message, the keyring uses the following format: base64(hmac(unencrypted_iv + encrypted_message) + unencrypted_iv + encrypted_message)
. This format ensures the integrity of the message and helps prevent certain types of attacks.
The components used in the final message are as follows:
unencrypted_iv
: The unencrypted Initialization Vector (IV) with a length of 16 bytes.encrypted_message
: The encrypted message that results from encrypting the actual sensitive data.hmac
: A 32-byte long HMAC (Hash-based Message Authentication Code) generated using the unencrypted IV concatenated with the encrypted message. When working with encrypted data, it's important to consider this specific format used by the keyring. If you plan to migrate from other encryption mechanisms or need to read encrypted values from the database without using the keyring, make sure to account for this format to maintain data integrity and security.
Keyring
A keyring is a concise JSON document that describes the encryption keys used for data encryption and decryption. It consists of a JSON object that maps numeric IDs of the keys to their corresponding key values. Each key is represented by its unique numeric ID.
Here's an example of a keyring in JSON format:
{
"1": "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=",
"2": "VN8UXRVMNbIh9FWEFVde0q7GUA1SGOie1+FgAKlNYHc="
}
In this example, there are three keys in the keyring, each identified by a numeric ID (1, 2, and 3). The key values are represented in base64-encoded format for simplicity. In a real-world scenario, these keys would be securely managed and stored.
The keyring must always contain at least one key, and each key should be unique and securely generated. The keys will be used by the encryption process, and it's essential to keep them protected and follow best practices for key management.
The id is used to track which key encrypted which piece of data; a key with a larger id is assumed to be newer. The value is the actual bytes of the encryption key.
Key Rotation
With the keyring, you can manage multiple encryption keys simultaneously, making key rotation a straightforward process. When you add a new key to the keyring with a higher ID than any other existing keys, that new key will automatically be used for encryption when creating or updating objects. This allows you to perform seamless key rotation by adding new keys and gradually phasing out the old ones. Keys that are no longer in use can be safely removed from the keyring.
When using the keyring, it is of utmost importance to save the keyring ID returned by the encrypt()
function. This ID is essential for decrypting values in the future. As long as you have all the encryption keys, you will always be able to decrypt values.
For encrypting database columns, it is recommended to use a separate keyring for each table that you plan to encrypt. This approach allows for easier key rotation in case you need to rotate keys due to potential key leakage or other security reasons.
Please note that the examples provided with hardcoded keys are for illustration purposes only. In real-world scenarios, you should never hardcode keys in your codebase for security reasons. Instead, consider retrieving the keyring from environment variables, especially if you're deploying to platforms like Heroku or similar cloud providers. Alternatively, you can manage the keyring configuration using a JSON file and deploy it using configuration management software like Ansible, Puppet, Chef, etc., to ensure proper key management practices and security.
Basic usage of keyring
import { keyring } from "keyring-node";
const keys = { 1: "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=" };
const encryptor = keyring(keys, { salt: "<custom salt>" });
// STEP 1: Encrypt message using latest encryption key.
const [encrypted, keyringId, digest] = encryptor.encrypt("super secret");
console.log(`🔒 ${encrypted}`);
console.log(`🔑 ${keyringId}`);
console.log(`🔎 ${digest}`);
//=> 🔒 Vco48O95YC4jqj44MheY8zFO2NLMPp/KILiUGbKxHvAwLd2/AN+zUG650CJzogttqnF1cGMFb//Idg4+bXoRMQ==
//=> 🔑 1
//=> 🔎 e24fe0dea7f9abe8cbb192702578715079689a3e
// STEP 2: Decrypted message using encryption key defined by keyring id.
const decrypted = encryptor.decrypt(encrypted, keyringId);
console.log(`✉️ ${decrypted}`);
//=> ✉️ super secret
Change encryption algorithm
You can choose between AES-128-CBC
, AES-192-CBC
and AES-256-CBC
. By
default, AES-128-CBC
will be used.
To specify the encryption algorithm, set the encryption
option. The following
example uses AES-256-CBC
.
import { keyring } from "keyring-node";
const keys = { 1: "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=" };
const encryptor = keyring(keys, {
encryption: "aes-256-cbc",
salt: "<custom salt>",
});
Using with Sequelize
If you're using Sequelize, you probably don't want to manually handle the
encryption as above. With that in mind, keyring
ships with a small plugin that
eases all the pain of manually handling encryption/decryption of properties, as
well as key rotation and digesting.
First, you have to load a different file that set ups models.
- The
id
column is mandatory and will store the keyring id (which encryption key was used). - All encrypted columns must be prefixed with
encrypted_
. - Optionally, you can have a
<attribute>_digest
column that will store a SHA1 digest of the value, making unique indexing and searching easier. - You are responsible for setting a VIRTUAL property to every column you are encrypting.
const Sequelize = require("sequelize");
const sequelize = new Sequelize("postgres:///test", { logging: false });
const Keyring = require("keyring-node/sequelize");
const User = await sequelize.define(
"users",
{
id: {
type: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false,
defaultValue: Sequelize.UUIDV4,
},
keyring_id: Sequelize.INTEGER,
encrypted_email: Sequelize.TEXT,
email_digest: Sequelize.TEXT,
email: Sequelize.VIRTUAL,
},
{ timestamps: false },
);
- Retrieve encryption keys from
USER_KEYRING
environment variable. - It's recommended that you use one keyring for each model, to make a rollout easier (e.g. an encryption key leaked).
const keys = JSON.parse(process.env.USER_KEYRING);
- Alternatively, you can load a JSON file deployed by some config management software like Ansible, Chef or Puppet.
const fs = require("fs");
const keys = JSON.parse(fs.readFileSync("user_keyring.json"));
- For the purposes of this example, we're going to set keys manually.
- WARNING: DON'T EVER DO THAT FOR REAL APPS.
const keys = { 1: "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=" };
- This is the step you set up your model with hooks to encrypt/decrypt columns. You can specify the encryption keys, which columns are going to be encrypted, how the column will be encrypted and the name of the keyring id column. You can see below the default values for
encryption
and keyring id column.
Keyring(User, {
keys, // [required]
columns: ["email"], // [required]
salt: "<custom salt>", // [required]
keyringIdColumn: "keyring_id", // [optional]
encryption: "aes-128-cbc", // [optional]
});
- Now you can create records, like you usually do.
const user = await User.create({ email: "[email protected]" });
- Let's update the email address.
await user.update({ email: "[email protected]" });
- Now let's pretend that you set
USER_KEYRING
env var to a {1: old_key, 2: new_key} or rollout a new JSON file via your config management software, and restarted the app.
keys[2] = "VN8UXRVMNbIh9FWEFVde0q7GUA1SGOie1+FgAKlNYHc=";
- To simply roll out a new encryption, just call
.save()
. This will trigger abeforeSave
hook, which will re-encrypt all properties again.
await user.save();
- Attributes are also re-encrypted when you call
.update()
.
await user.update({ email: "[email protected]" });
Lookup
Indeed, when using encryption to protect sensitive data in a database, one challenge arises when there is a need to look up records based on a known secret. To address this issue, keyring
offers a solution by generating SHA1 digests for the encrypted strings and saving them to the database.
Here's how it works:
Encryption and Digest Generation: When you use the
keyring
plugin to encrypt a specific attribute in a model (e.g.,encryptedField
),keyring
will automatically detect the presence of an additional column in the model named<attribute>_digest
(e.g.,encryptedField_digest
). It will then generate a SHA1 digest for the encrypted value of the attribute and save it in the corresponding<attribute>_digest
column. This digest is a fixed-size string (e.g., 40 characters) that can be used for unique indexing or searching.Unique Indexing or Searching: The SHA1 digest can be used to perform lookups and searches without compromising the encrypted data's security. Instead of querying the encrypted value directly, you can query the corresponding
<attribute>_digest
column to find the records you're interested in.
Regarding the use of hashing salts, while it is not strictly required, it is highly recommended. A hashing salt is a random value added to the data before hashing to prevent the use of precomputed tables (rainbow tables) in attacks. By using a salt, you enhance the security of the digests, making it more difficult for attackers to reverse-engineer the original data from the digest.
In summary, by leveraging SHA1 digests and optional hashing salts, keyring
provides a mechanism to safely look up records based on a known secret while still maintaining the security of the encrypted data. It simplifies the process of working with encrypted data in databases, ensuring data privacy and security in applications that handle sensitive information.
const { sha1 } = require("keyring-node");
await User.create({ email: "[email protected]" });
const user = await User.findOne({
where: {
email_digest: sha1("[email protected]", { salt: "<custom salt>" }),
},
});
Development
Clone the project
git clone https://github.com/MatheusWill/keyring-node-npm.git
Install the dependencies
npm i
Test the project
npm run test
Authors
🔗 Links
Feedback
Se você tiver algum feedback, por favor nos deixe saber por meio de [email protected]
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/MatheusWill/keyring-node-npm. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to.