56 lines
1.9 KiB
TypeScript
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");
|
|
}
|