feat: secure KYC storage, Google OAuth, terms gating
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user