learning_ai_common_plat/packages/field-encrypt/src/aes-gcm.ts
saravanakumardb1 bb3f5385fc feat(field-encrypt): create @bytelyst/field-encrypt package with AES-256-GCM envelope encryption
- 10 source files: types, aes-gcm, 3 key providers (memory/env/akv), envelope, key-cache, dek-store, guards, migration, factory
- 42 Vitest tests: AES-GCM roundtrips, tamper detection, unicode, 100KB payloads, key providers, DEK cache TTL/LRU, envelope lifecycle, migration (dry-run + idempotent), config validation
- AKV MEK creation script (scripts/create-encryption-keys.sh) for 10 product MEKs
- .env.example updated with FIELD_ENCRYPT_* vars
2026-03-21 09:18:10 -07:00

90 lines
2.4 KiB
TypeScript

/**
* @bytelyst/field-encrypt — AES-256-GCM primitives
*
* Low-level encrypt/decrypt using Node.js native crypto.
* All higher-level APIs delegate to these functions.
*/
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
import type { EncryptedField } from './types.js';
const ALGORITHM = 'aes-256-gcm';
const IV_BYTES = 12;
const KEY_BYTES = 32;
/**
* Encrypt a plaintext string with AES-256-GCM.
*
* @param plaintext - UTF-8 string to encrypt
* @param key - 32-byte AES key
* @param dekId - DEK identifier stored in the output
* @param aad - Optional additional authenticated data (e.g., userId + context)
* @returns EncryptedField object ready for Cosmos/SQLite storage
*/
export function encryptField(
plaintext: string,
key: Buffer,
dekId: string,
aad?: string
): EncryptedField {
if (key.length !== KEY_BYTES) {
throw new Error(`AES-256-GCM requires a ${KEY_BYTES}-byte key, got ${key.length}`);
}
const iv = randomBytes(IV_BYTES);
const cipher = createCipheriv(ALGORITHM, key, iv);
if (aad) {
cipher.setAAD(Buffer.from(aad, 'utf8'));
}
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
__encrypted: true,
v: 1,
alg: 'aes-256-gcm',
ct: encrypted,
iv: iv.toString('hex'),
tag: authTag.toString('hex'),
dekId,
};
}
/**
* Decrypt an EncryptedField back to plaintext.
*
* @param field - EncryptedField object
* @param key - 32-byte AES key (must match the key used to encrypt)
* @param aad - Optional AAD (must match the AAD used during encryption)
* @returns Decrypted UTF-8 string
* @throws Error if authentication tag verification fails (tampered data)
*/
export function decryptField(field: EncryptedField, key: Buffer, aad?: string): string {
if (key.length !== KEY_BYTES) {
throw new Error(`AES-256-GCM requires a ${KEY_BYTES}-byte key, got ${key.length}`);
}
const iv = Buffer.from(field.iv, 'hex');
const authTag = Buffer.from(field.tag, 'hex');
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
if (aad) {
decipher.setAAD(Buffer.from(aad, 'utf8'));
}
let decrypted = decipher.update(field.ct, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/** Generate a random 32-byte AES-256 key. */
export function generateAesKey(): Buffer {
return randomBytes(KEY_BYTES);
}