Files
fullstack-starter/packages/crypto/src/aes-gcm.ts

54 lines
1.8 KiB
TypeScript

import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'
const GCM_IV_LENGTH = 12 // 96 bits
const GCM_TAG_LENGTH = 16 // 128 bits
const ALGORITHM = 'aes-256-gcm'
/**
* AES-256-GCM encrypt.
*
* Output format (before Base64): [IV (12 bytes)] + [ciphertext] + [auth tag (16 bytes)]
*
* @param plaintext - UTF-8 string to encrypt
* @param key - 32-byte AES key
* @returns Base64-encoded encrypted data
*/
export const aesGcmEncrypt = (plaintext: string, key: Buffer): string => {
const iv = randomBytes(GCM_IV_LENGTH)
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: GCM_TAG_LENGTH })
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()])
const tag = cipher.getAuthTag()
// Layout: IV + ciphertext + tag
const combined = Buffer.concat([iv, encrypted, tag])
return combined.toString('base64')
}
/**
* AES-256-GCM decrypt.
*
* Input format (after Base64 decode): [IV (12 bytes)] + [ciphertext] + [auth tag (16 bytes)]
*
* @param encryptedBase64 - Base64-encoded encrypted data
* @param key - 32-byte AES key
* @returns Decrypted UTF-8 string
*/
export const aesGcmDecrypt = (encryptedBase64: string, key: Buffer): string => {
const data = Buffer.from(encryptedBase64, 'base64')
if (data.length < GCM_IV_LENGTH + GCM_TAG_LENGTH) {
throw new Error('Encrypted data too short: must contain IV + tag at minimum')
}
const iv = data.subarray(0, GCM_IV_LENGTH)
const tag = data.subarray(data.length - GCM_TAG_LENGTH)
const ciphertext = data.subarray(GCM_IV_LENGTH, data.length - GCM_TAG_LENGTH)
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: GCM_TAG_LENGTH })
decipher.setAuthTag(tag)
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()])
return decrypted.toString('utf-8')
}