import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto"; import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { resolve } from "node:path"; import { homedir } from "node:os"; const ALGORITHM = "aes-256-gcm"; const KEY_LENGTH = 32; const IV_LENGTH = 12; const AUTH_TAG_LENGTH = 16; const KEY_FILE_NAME = ".property-data-encryption-key"; function resolveConfigDir(): string { try { const platformRoot = process.env.PLATFORM_ROOT; if (platformRoot) { const brandPath = resolve(platformRoot, "config/brand.json"); const brand = JSON.parse(readFileSync(brandPath, "utf-8")); if (brand.configDir) return brand.configDir; } } catch { // Fall back to default } return ".maxy"; } function keyFilePath(): string { return resolve(homedir(), resolveConfigDir(), KEY_FILE_NAME); } function loadOrGenerateKey(): Buffer { const path = keyFilePath(); if (existsSync(path)) { const hex = readFileSync(path, "utf-8").trim(); const buf = Buffer.from(hex, "hex"); if (buf.length !== KEY_LENGTH) { throw new Error( `Encryption key at ${path} is ${buf.length} bytes, expected ${KEY_LENGTH}. ` + `Delete the file and re-register the PropertyData API key.`, ); } return buf; } const key = randomBytes(KEY_LENGTH); writeFileSync(path, key.toString("hex"), { mode: 0o600 }); console.error(`[property-data] encryption key generated at ${path}`); return key; } export function encrypt(plaintext: string): string { const key = loadOrGenerateKey(); const iv = randomBytes(IV_LENGTH); const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH }); let encrypted = cipher.update(plaintext, "utf-8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag().toString("hex"); return `${iv.toString("hex")}:${encrypted}:${authTag}`; } export function decrypt(encryptedValue: string): string { const parts = encryptedValue.split(":"); if (parts.length !== 3) { throw new Error("Malformed encrypted value — expected iv:ciphertext:authTag"); } const [ivHex, ciphertextHex, authTagHex] = parts; const path = keyFilePath(); if (!existsSync(path)) { throw new Error( `Encryption key not found at ${path}. The stored PropertyData API key is unrecoverable. Re-register.`, ); } const key = loadOrGenerateKey(); const iv = Buffer.from(ivHex, "hex"); const authTag = Buffer.from(authTagHex, "hex"); const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH }); decipher.setAuthTag(authTag); let decrypted = decipher.update(ciphertextHex, "hex", "utf-8"); decrypted += decipher.final("utf-8"); return decrypted; }