Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | 1x 1x 1x 1x 1x 1x 7x 7x 1x 7x 1x 7x 1x 1x 1x 1x 8x 5x 5x 5x 5x 5x 5x 1x 3x 3x 2x 2x 3x 2x 2x 2x 2x 2x 2x 2x 3x 1x 1x 3x | import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
// AES-256-GCM encryption for sensitive at-rest data.
//
// Format: enc:v1:<base64(iv||ciphertext||authTag)>
//
// • The "enc:v1:" prefix lets us detect already-encrypted values and roll
// over to v2 later without breaking old rows.
// • decrypt() tolerates plaintext (returns input as-is) so the system can
// read rows that pre-date encryption.
// • encrypt() always produces a tagged ciphertext.
//
// Generate a fresh key with:
// openssl rand -base64 32
const ALGO = 'aes-256-gcm';
const PREFIX = 'enc:v1:';
const IV_LEN = 12;
const TAG_LEN = 16;
let cachedKey: Buffer | null = null;
function getKey(): Buffer {
if (cachedKey) return cachedKey;
const raw = process.env.ENCRYPTION_KEY;
if (!raw) throw new Error('ENCRYPTION_KEY is not set');
const buf = Buffer.from(raw, 'base64');
if (buf.length !== 32) {
throw new Error(`ENCRYPTION_KEY must decode to 32 bytes (got ${buf.length})`);
}
cachedKey = buf;
return buf;
}
export function encrypt(plain: string | null | undefined): string | null {
if (plain == null || plain === '') return null;
const iv = randomBytes(IV_LEN);
const cipher = createCipheriv(ALGO, getKey(), iv);
const ciphertext = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return PREFIX + Buffer.concat([iv, ciphertext, tag]).toString('base64');
}
export function decrypt(value: string | null | undefined): string | null {
if (value == null || value === '') return null;
if (!value.startsWith(PREFIX)) return value; // legacy plaintext → pass through
try {
const raw = Buffer.from(value.slice(PREFIX.length), 'base64');
if (raw.length < IV_LEN + TAG_LEN) return null;
const iv = raw.subarray(0, IV_LEN);
const tag = raw.subarray(raw.length - TAG_LEN);
const ciphertext = raw.subarray(IV_LEN, raw.length - TAG_LEN);
const decipher = createDecipheriv(ALGO, getKey(), iv);
decipher.setAuthTag(tag);
const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
return dec.toString('utf8');
} catch {
return null; // tampered or wrong key — fail closed
}
}
|