Skip to content

Instantly share code, notes, and snippets.

@vlucas
Last active March 24, 2025 13:09
Show Gist options
  • Save vlucas/2bd40f62d20c1d49237a109d491974eb to your computer and use it in GitHub Desktop.
Save vlucas/2bd40f62d20c1d49237a109d491974eb to your computer and use it in GitHub Desktop.
Stronger Encryption and Decryption in Node.js
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
const ENCRYPTION_KEY: string = process.env.ENCRYPTION_KEY || ""; // Must be 256 bits (32 characters)
const IV_LENGTH: number = 16; // For AES, this is always 16
/**
* Will generate valid encryption keys for use
* Not used in the code below, but generate one and store it in ENV for your own purposes
*/
export function keyGen() {
return randomBytes(32).toString("hex");
}
/**
* Encrypt a string
* Uses a random IV + encryption key for unique encrypted end result
*/
export function encrypt(plainText: string, encryptionKey: string = ENCRYPTION_KEY): string {
const iv = randomBytes(IV_LENGTH); // Directly use Buffer returned by randomBytes
const cipher = createCipheriv("aes-256-cbc", Buffer.from(encryptionKey, "hex"), iv);
const encrypted = Buffer.concat([cipher.update(plainText, "utf8"), cipher.final()]);
// Return iv and encrypted data as hex, combined in one line
return iv.toString("hex") + ":" + encrypted.toString("hex");
}
/**
* Decrypt a string
*/
export function decrypt(text: string, encryptionKey: string = ENCRYPTION_KEY): string {
const [ivHex, encryptedHex] = text.split(":");
if (!ivHex || !encryptedHex) {
throw new Error("Invalid or corrupted cipher format");
}
const encryptedText = Buffer.from(encryptedHex, "hex");
const decipher = createDecipheriv("aes-256-cbc", Buffer.from(encryptionKey, "hex"), Buffer.from(ivHex, "hex"));
let decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]);
return decrypted.toString();
}
@Vishu2108
Copy link

I am unable to run the code.....whenever I am entering the string.....it gives me an error...

@Venipa
Copy link

Venipa commented Apr 10, 2023

made this package a while ago for server side encrypted payloads to use for backend only: https://www.npmjs.com/package/encryption.js

@nijynot
Copy link

nijynot commented Feb 15, 2024

I'm surprised by the glaring mistakes (?) in how the encryption key is actually 16 bytes and IV is 8 bytes?
This is largely because he's reading a string as a buffer, without using the hex argument.

Fixes

  • Because he's reading the hex strings without the hex argument, it's parsed incorrectly. All hex values are read half their size - so 32 bytes is actually 16 bytes and 16 bytes is 8 bytes. Amending this also fixes the Invalid key length errors that people have in the thread.
// Previously
Buffer.from(encryptionKey)

// Now
Buffer.from(encryptionKey, 'hex')
  • Take note of the line, there's absolutely no reason as why you should need to .slice your string if it's 16 bytes. If you generate a string of 16 bytes, that should fit fully into the createCipheriv function from Node's crypto package.
let iv = Buffer.from(crypto.randomBytes(IV_LENGTH)).toString('hex').slice(0, IV_LENGTH);
  • Added keyGen() function that generates a key for you, so that there's no misunderstanding or ambiguity in how you should generate this key. Saw some people mention other random string generation packages, but there's no reason to have that dependency when you can use crypto's randomBytes directly.
  • Use const for variables instead of let.

Contribute

There are ways to improve the code futher. If you want to improve this, then you could:

  • Add better error messages for e.g. when cipher formats are incorrect
  • Read and write hex with the prefix 0x for less ambiguity
  • Better TypeScript types for Hex values, etc.

I'll leave that to someone else to figure out although, if someone wants to.

Updated code (2024-02-15)

You should use this instead of OP and OP's TypeScript version posted above.

import crypto from 'crypto';

const ENCRYPTION_KEY: string = process.env.SC_ENCRYPTION_KEY || ''; // Must be 256 bits (32 characters)
const IV_LENGTH: number = 16; // For AES, this is always 16

export function keyGen() {
  return crypto.randomBytes(32).toString('hex');
}

export function encrypt(plainText: string, keyHex: string = ENCRYPTION_KEY): string {
  const iv = crypto.randomBytes(IV_LENGTH); // Directly use Buffer returned by randomBytes
  const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(keyHex, 'hex'), iv);
  const encrypted = Buffer.concat([cipher.update(plainText, 'utf8'), cipher.final()]);

  // Return iv and encrypted data as hex, combined in one line
  return iv.toString('hex') + ':' + encrypted.toString('hex');
}

export function decrypt(text: string, keyHex: string = ENCRYPTION_KEY): string {
  const [ivHex, encryptedHex] = text.split(':');
  if (!ivHex || !encryptedHex) {
    throw new Error('Invalid or corrupted cipher format');
  }

  const encryptedText = Buffer.from(encryptedHex, 'hex');
  const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(keyHex, 'hex'), Buffer.from(ivHex, 'hex'));
  let decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]);

  return decrypted.toString();
}

@isaackogan
Copy link

isaackogan commented Feb 19, 2024

Great work @nijynot !

@kavindajayakody
Copy link

Nice work by @nijynot! If anyone is facing "TypeError: Cannot read properties of undefined (reading 'randomBytes')", change the import import crypto from 'crypto'; to import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'. Wonder why ES gives this error, but this fix should work

@vlucas
Copy link
Author

vlucas commented Feb 28, 2025

Updated the main gist with your suggestions @nijynot. Thanks!

@meccar
Copy link

meccar commented Mar 3, 2025

I have one business question. In the case you need to update the ENCRYPTION_KEY in the future
I have 2 ideas:

Option 1: Create a discrete api to update all fields in database, but the transaction is massive and might not ideal for a product.
Option 2: Create a middleware to check the fields when you query and re-encrypt and update the row, but the problem is the old key is still required for decryption.

Then what would be more efficient method for this case? I'm truly stuck!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment