Node.js + AWS Secrets Manager: Encryption Lessons and Insights

I recently encrypted personal data for a client project using Node’s crypto module and we ran into a couple issues when decrypting and storing secrets in AWS Secrets Manager. If you are new to encryption or AWS you may run into the same problems, so here’s what I learned.

Storing Secrets in AWS Secrets Manager

There are several ways you can store secrets in Secrets Manager. You can manually create a secret in the AWS console, AWS CLI, or in your Node project using the Secrets Manager API. Initially we tried creating a string in the console as the secret value, but when retrieving the secret string from AWS, there was no way to guarantee the byte length would be the same. Our encryption methods expect the secret string to always have a byte length of 32. You cannot choose the encoding type of the inputted string when using the AWS console. To avoid this, use the Secrets Manager API to create a secret as a Base64 array buffer.

import { CreateSecretCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";

const createSecretsManagerClient = () => {
  return new SecretsManagerClient({
	region: "region",
	credentials: {
  	accessKeyId: "access-key",
  	secretAccessKey: "secret-access-key",
},
  });
};
const createSecret = async () => {
const secretValue = crypto.randomBytes(32).toString("base64");
const secretBuffer = Buffer.from(secretValue, "base64");
const client = createSecretsManagerClient({});
const command = new CreateSecretCommand({
	Name: "secretName",
	SecretBinary: secretBuffer,
});
const response = await client.send(command);
};

The secret value will be stored as binary, preserving the original byte length.

Converting Encrypted Strings

Whether you’re retrieving strings from Secrets Manager or a database, it’s important to keep track of the string encoding type. Incorrect encoding can corrupt the original data or produce invalid byte lengths, especially for cryptographic keys. For example, using .toString() on a buffer without specifying an encoding like Base64 can result in data loss. AES encryption algorithms require keys of specific byte lengths, and these issues can trigger invalid key length errors.

In the project database, encrypted data is stored as a JSON string containing a plaintext Base64 string. When we want to retrieve the data, it needs to be converted to a UTF-8 string so that it is in a human readable format.

In the snippet below on line 12 we specify the plaintext input encoding as “base64” and output encoding as “utf8”.

import crypto from "crypto";

const decryptSymmetric = (
  key: Uint8Array,
  ciphertext: string,
  iv: string,
  tag: Buffer,
) => {
  const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64"));
  decipher.setAuthTag(tag);

  let plaintext = decipher.update(ciphertext, "base64", "utf8");
  plaintext += decipher.final("utf8");

  return plaintext;
};

Consistently handling encoding and decoding is crucial when doing encryption. Hopefully these tips help you avoid the same headaches I ran into during the initial setup.

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *