@systemix/cipher
Authenticated AES-256-GCM encryption and decryption built on Web Crypto. Versioned envelope with key ids and key rotation. Requires Node.js 24.7+. For encrypting sensitive data at rest — database fields, config secrets, tokens.
Installation
pnpm add @systemix/cipher
Basic Usage
import { generateKeyBytes, encryptString, decryptString, } from '@systemix/cipher'; const key = generateKeyBytes(); // 32 random bytes (AES-256) const payload = await encryptString('sensitive data', key); // → "U1gBAC..." (base64url of the envelope) await decryptString(payload, key); // → "sensitive data"
The payload is a versioned envelope — SX | version | keyId | IV | ciphertext + tag. It's one value to store, the IV is random per call, and the header is authenticated so it can't be altered undetected. Payloads from 0.1.x still decrypt.
Tamper Detection
AES-GCM is authenticated: any modification to the ciphertext (or wrong key) throws DecryptionError instead of returning garbage.
import { DecryptionError } from '@systemix/cipher'; try { await decryptString(payload, wrongKey); } catch (error) { if (error instanceof DecryptionError) { // wrong key, tampered data, or malformed payload } }
Examples
Additional authenticated data
Bind ciphertext to a context (user ID, record type). Decryption fails unless the exact same AAD is supplied:
const payload = await encrypt(recordBytes, key, { additionalData: new TextEncoder().encode('user:42'), });
Key derivation from a password
import { deriveKeyFromPassword, generateSalt } from '@systemix/cipher'; const salt = generateSalt(); // store alongside the ciphertext const key = await deriveKeyFromPassword('master password', salt);
Key rotation
import { createKeyring } from '@systemix/cipher'; const ring = createKeyring({ keys: { k2: newKey, k1: oldKey }, current: 'k2', }); const blob = await ring.encryptString('secret'); // stamped "k2" await ring.decryptString(oldBlob); // envelope names "k1" → still works if (ring.needsRotation(oldBlob)) { save(await ring.encryptString(await ring.decryptString(oldBlob))); }
Random 96-bit IVs are safe for roughly 2^32 messages per key; rotate well before that.
Key Management
| Function | Description |
|---|---|
generateKeyBytes(length?) | Random AES key — 16, 24, or 32 bytes |
deriveKeyFromPassword(password, salt, iterations?) | PBKDF2-SHA256 → 32-byte AES key |
generateSalt(length?) | Random salt (min 8 bytes) |
Try it
Open the Cipher demo to encrypt, decrypt, and tamper with ciphertexts interactively.