Skip to content

Instantly share code, notes, and snippets.

@lauslim12
Last active March 21, 2022 07:35
Show Gist options
  • Save lauslim12/4ec0552c8e3bbb690f4e527ccf49ab72 to your computer and use it in GitHub Desktop.
Save lauslim12/4ec0552c8e3bbb690f4e527ccf49ab72 to your computer and use it in GitHub Desktop.
TypeScript way to implement AES-256-GCM encryption in an object-oriented way in Node.js. Complete with code comments and sample runner.
/**
* You need Node.js, TypeScript, and NanoID. Use `npm i nanoid` in a sample project to test things out!
* Steps:
* - Create a folder, for example `mkdir aes-test` then `cd aes-test`.
* - Copy this script and create a file, for example `index.ts`.
* - Paste the file there.
* - `npm i nanoid ts-node-dev`.
* - `npm run ts-node-dev .`.
* - Profit!
*/
import { nanoid } from 'nanoid';
import crypto from 'node:crypto';
/**
* Handles AES-256-GCM operations, such as string encryption, string
* decryption, IV generation, and secure secret generation.
*/
class AES256GCM {
/**
* Default algorithm for the encryption is AES-256-GCM.
*/
public algorithm = 'aes-256-gcm' as const;
/**
* Empty constructor, we do not actually need anything.
*/
constructor() {}
/**
* Generates a secure IV for AES. In GCM, IV has to be
* unique for every data to prevent forgery attacks.
*
* @returns 128 bits random IV.
*/
generateIV() {
return Buffer.from(nanoid(16), 'utf-8');
}
/**
* Generates a random 256 bits secret.
*
* @returns 256 bits random secret.
*/
generateSecret() {
return nanoid(32);
}
/**
* Encrypts a plaintext using a secret as its key using the AES-256 Galois Counter Mode.
*
* @param plaintext - Plaintext to be encrypted.
* @param secret - Secret to be used as a key.
* @returns An object returning the ciphertext, IV and the authentication tag in Base64
* format.
*/
encrypt(plaintext: string, secret: string) {
// Generate a secure IV and create a basic cipher.
const iv = this.generateIV();
const cipher = crypto.createCipheriv(this.algorithm, secret, iv);
// Update cipher.
const data = cipher.update(plaintext, 'utf-8', 'base64');
const final = cipher.final('base64');
return {
ciphertext: `${data}${final}`,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64'),
};
}
/**
* Decrypts a ciphertext using a secret, an IV, and an authentication tag.
*
* @param ciphertext - Ciphertext to be fed to the input.
* @param secret - Key/secret to be used.
* @param iv - IV returned from the previous encryption process. Has to be in Base64.
* @param tag - Authentication tag returned from the previous encryption process. Has to be in Base64.
* @returns The string, decrypted in plaintext format (UTF-8).
*/
decrypt(ciphertext: string, secret: string, iv: string, tag: string) {
// Attempt to decipher with IV, secret, and authentication tag.
const decipher = crypto
.createDecipheriv(this.algorithm, secret, Buffer.from(iv, 'base64'))
.setAuthTag(Buffer.from(tag, 'base64'));
// Updates the previously created cipher with the ciphertext.
const data = decipher.update(ciphertext, 'base64', 'utf-8');
// Returns the decrypted ciphertext.
return `${data}${decipher.final('utf-8')}`;
}
}
/**
* Driver code.
*/
function main() {
// Initialize parameters.
const AES = new AES256GCM();
const text = 'Hello, World! This is AES-256 GCM with Node.js!';
const secret = AES.generateSecret();
// Perform encryption and decryption.
const encrypted = AES.encrypt(text, secret);
const decrypted = AES.decrypt(
encrypted.ciphertext,
secret,
encrypted.iv,
encrypted.tag
);
// Match results.
console.log(`Plaintext: ${text}.`);
console.log(`Ciphertext (Base64): ${encrypted.ciphertext}.`);
console.log(`IV (Base64): ${encrypted.iv}.`);
console.log(`Authentication tag (Base64): ${encrypted.tag}.`);
console.log(`Decrypted text: ${decrypted}.`);
console.log(`Plaintext equals decrypted text: ${text === decrypted}.`);
}
/**
* Run program.
*/
void main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment