Files
setrip/lib/crypto.ts
T

56 lines
1.9 KiB
TypeScript

import crypto from "node:crypto";
const ALGO = "aes-256-gcm";
const IV_LEN = 12;
const TAG_LEN = 16;
function readKey(envName: string): Buffer {
const hex = process.env[envName];
if (!hex) throw new Error(`Missing env ${envName}`);
if (hex.length !== 64) {
throw new Error(`${envName} must be 64 hex chars (32 bytes)`);
}
return Buffer.from(hex, "hex");
}
function getEncKey(): Buffer {
return readKey("KYC_ENCRYPTION_KEY");
}
function getNikPepper(): Buffer {
return readKey("KYC_NIK_PEPPER");
}
/** Encrypt a Buffer with AES-256-GCM. Output layout: [iv(12) | tag(16) | ciphertext]. */
export function encryptBuffer(plain: Buffer): Buffer {
const iv = crypto.randomBytes(IV_LEN);
const cipher = crypto.createCipheriv(ALGO, getEncKey(), iv);
const ct = Buffer.concat([cipher.update(plain), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, ct]);
}
export function decryptBuffer(blob: Buffer): Buffer {
if (blob.length < IV_LEN + TAG_LEN) throw new Error("Ciphertext too short");
const iv = blob.subarray(0, IV_LEN);
const tag = blob.subarray(IV_LEN, IV_LEN + TAG_LEN);
const ct = blob.subarray(IV_LEN + TAG_LEN);
const decipher = crypto.createDecipheriv(ALGO, getEncKey(), iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ct), decipher.final()]);
}
/** Encrypt UTF-8 string -> base64 string. Used for short PII like NIK. */
export function encryptString(plain: string): string {
return encryptBuffer(Buffer.from(plain, "utf8")).toString("base64");
}
export function decryptString(b64: string): string {
return decryptBuffer(Buffer.from(b64, "base64")).toString("utf8");
}
/** Deterministic HMAC-SHA256 of a normalized value, hex-encoded. Used for unique-lookup of NIK without storing plaintext. */
export function hmacHex(value: string): string {
return crypto.createHmac("sha256", getNikPepper()).update(value).digest("hex");
}