54 lines
1.8 KiB
TypeScript
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')
|
|
}
|