/**
 * src/security/cred-store.ts
 *
 * Cross-platform credential encryption at rest.
 *
 * Threat model
 * ────────────
 * Without this layer, a customer's WhatsApp/Telegram tokens, API recipes,
 * and `teach_api`-saved auth headers were written to SQLite or JSON in
 * cleartext. An attacker with disk read access (e.g., compromised machine,
 * borrowed laptop, exposed backup) walks away with live session credentials
 * for every connected service. PDPL Article 18 requires reasonable-care
 * encryption of personal/business data — plaintext credentials don't meet
 * that bar.
 *
 * Design
 * ──────
 * • Per-install master key: 32 random bytes generated once on first boot,
 *   written to `<dataDir>/.muassis-master.key` with mode 0600 (owner read
 *   only). Cross-platform without native deps.
 * • AES-256-GCM for confidentiality + integrity. 12-byte random IV per
 *   message. 16-byte auth tag verified on decrypt.
 * • Wire format: `mc1:<iv-b64>:<tag-b64>:<ciphertext-b64>` so we can
 *   distinguish encrypted blobs from plaintext during migration and bump
 *   the algorithm version (`mc1` → `mc2`) without breaking old data.
 * • If decryption fails (wrong key, truncated), throws — never silently
 *   returns garbage. Callers must handle.
 *
 * Out of scope (intentional, for v2.1)
 * ─────────────────────────────────────
 * • OS keystore integration (Keychain / DPAPI / libsecret) — adds native
 *   deps (`keytar`) and postinstall friction. Defer to v2.2 as an opt-in
 *   `MUASSIS_USE_OS_KEYSTORE=1` upgrade path.
 * • Baileys per-file auth-state encryption — covered by OS FDE (FileVault
 *   on macOS, BitLocker on Windows, LUKS on Linux are all default-on for
 *   modern installs). v2.2 will wrap the auth-dir in an encrypted blob.
 */
import { createCipheriv, createDecipheriv, createHash, randomBytes, timingSafeEqual } from 'node:crypto';
import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { getDataDir } from '../utils/data-dir.js';
import { createChildLogger } from '../utils/logger.js';
const log = createChildLogger('security:cred-store');
const VERSION = 'mc1';
const KEY_BYTES = 32;
const IV_BYTES = 12;
const TAG_BYTES = 16;
let cachedKey = null;
function getKeyPath() {
    return join(getDataDir(), '.muassis-master.key');
}
/**
 * Read the master key, or generate it on first boot. The key file is
 * chmod-600'd so only the owning user can read. On Windows where chmod
 * is a no-op, ACL inheritance from the user profile dir provides
 * comparable protection.
 */
function getMasterKey() {
    if (cachedKey)
        return cachedKey;
    const path = getKeyPath();
    if (!existsSync(path)) {
        mkdirSync(dirname(path), { recursive: true });
        const fresh = randomBytes(KEY_BYTES);
        writeFileSync(path, fresh, { mode: 0o600 });
        try {
            chmodSync(path, 0o600);
        }
        catch { /* Windows */ }
        log.info({ path }, '🔐 master credential key generated');
        cachedKey = fresh;
        return fresh;
    }
    const buf = readFileSync(path);
    if (buf.length !== KEY_BYTES) {
        throw new Error(`master key file at ${path} is corrupt (expected ${KEY_BYTES} bytes, got ${buf.length})`);
    }
    try {
        chmodSync(path, 0o600);
    }
    catch { /* idempotent */ }
    cachedKey = buf;
    return buf;
}
/**
 * Encrypt arbitrary string. Returns a self-describing token of the form
 * `mc1:<iv>:<tag>:<ciphertext>`. Empty string → empty string (allows
 * "remove credential" semantics without special-casing callers).
 */
export function encrypt(plaintext) {
    if (plaintext === '')
        return '';
    const key = getMasterKey();
    const iv = randomBytes(IV_BYTES);
    const cipher = createCipheriv('aes-256-gcm', key, iv);
    const enc = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
    const tag = cipher.getAuthTag();
    return [
        VERSION,
        iv.toString('base64'),
        tag.toString('base64'),
        enc.toString('base64'),
    ].join(':');
}
/**
 * Decrypt a token produced by `encrypt`. Throws if the token is corrupt,
 * tampered with, or encrypted under a different master key. Never returns
 * partial / garbage data — that's the GCM tag's job.
 */
export function decrypt(token) {
    if (token === '')
        return '';
    const parts = token.split(':');
    if (parts.length !== 4 || parts[0] !== VERSION) {
        throw new Error(`cred-store: unrecognized token format (expected ${VERSION}:iv:tag:ct)`);
    }
    const key = getMasterKey();
    const iv = Buffer.from(parts[1], 'base64');
    const tag = Buffer.from(parts[2], 'base64');
    const ct = Buffer.from(parts[3], 'base64');
    if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) {
        throw new Error('cred-store: token has wrong iv/tag length');
    }
    const decipher = createDecipheriv('aes-256-gcm', key, iv);
    decipher.setAuthTag(tag);
    const dec = Buffer.concat([decipher.update(ct), decipher.final()]);
    return dec.toString('utf-8');
}
/** Heuristic: "is this string an encrypted token of ours?" Used by migration code. */
export function isEncrypted(s) {
    return typeof s === 'string' && s.startsWith(`${VERSION}:`) && s.split(':').length === 4;
}
/** Constant-time string compare. Use when matching webhook signatures or HMAC tags. */
export function safeEqual(a, b) {
    const ab = Buffer.from(a, 'utf-8');
    const bb = Buffer.from(b, 'utf-8');
    if (ab.length !== bb.length)
        return false;
    return timingSafeEqual(ab, bb);
}
/**
 * Diagnostic: returns the master key fingerprint (first 8 hex chars of
 * SHA-256 of the key). Lets the customer verify backup integrity without
 * exposing the key itself.
 */
export function masterKeyFingerprint() {
    const key = getMasterKey();
    return createHash('sha256').update(key).digest('hex').slice(0, 16);
}
//# sourceMappingURL=cred-store.js.map