/**
 * KeyManager -- Centralized encryption key management and rotation.
 *
 * Manages the lifecycle of encryption keys used across the governance
 * framework. Supports key rotation with re-encryption of existing data,
 * key versioning, and automated rotation scheduling.
 *
 * T-043: Key management infrastructure.
 * T-044: Key rotation support.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { createLogger } from '../util/log.js';
const log = createLogger('key-manager');
export class KeyManager {
    config;
    keysFile;
    auditFile;
    constructor(config) {
        this.config = {
            rotationIntervalMs: 10 * 60 * 1000, // 10 min (DEV MODE -- production: 90 * 24 * 60 * 60 * 1000 / 90 days)
            maxKeyVersions: 5,
            ...config,
        };
        if (config.masterSecret.length < 16) {
            throw new Error('Master secret must be at least 16 characters');
        }
        if (!fs.existsSync(config.keyDir)) {
            fs.mkdirSync(config.keyDir, { recursive: true });
        }
        this.keysFile = path.join(config.keyDir, 'key-versions.json');
        this.auditFile = path.join(config.keyDir, 'key-audit.jsonl');
        // Initialize with first key if no keys exist
        if (!fs.existsSync(this.keysFile)) {
            this.initializeFirstKey();
        }
    }
    /**
     * Get the currently active key material derived from master + version.
     */
    getCurrentKey() {
        const versions = this.loadVersions();
        const active = versions.find(v => v.active);
        if (!active) {
            throw new Error('No active key version found');
        }
        return {
            key: this.deriveKey(active.version),
            version: active.version,
            keyId: active.keyId,
        };
    }
    /**
     * Get key material for a specific version (for decrypting old data).
     */
    getKeyByVersion(version) {
        const versions = this.loadVersions();
        const v = versions.find(kv => kv.version === version);
        if (!v)
            return null;
        return {
            key: this.deriveKey(v.version),
            keyId: v.keyId,
        };
    }
    /**
     * Rotate to a new key version.
     */
    rotate() {
        const versions = this.loadVersions();
        const current = versions.find(v => v.active);
        if (!current) {
            throw new Error('No active key to rotate from');
        }
        // Deactivate current
        current.active = false;
        current.rotatedAt = new Date().toISOString();
        // Create new version
        const newVersion = current.version + 1;
        const newKeyId = `key_${crypto.randomBytes(8).toString('hex')}`;
        const newKey = {
            version: newVersion,
            keyId: newKeyId,
            createdAt: new Date().toISOString(),
            active: true,
            keyFingerprint: this.computeFingerprint(newVersion),
        };
        versions.push(newKey);
        // Prune old versions beyond max
        this.pruneOldVersions(versions);
        this.saveVersions(versions);
        this.audit('KEY_ROTATED', { from: current.version, to: newVersion, keyId: newKeyId });
        log.info('Key rotated', { from: current.version, to: newVersion });
        return {
            previousVersion: current.version,
            newVersion,
            newKeyId,
            rotatedAt: new Date().toISOString(),
        };
    }
    /**
     * Check if key rotation is due.
     *
     * Distinguishes two distinct unsafe states:
     *   - key-store-empty: no keys exist at all; runtime cannot sign or verify.
     *     Emits audit event `key-store-empty` at CRITICAL severity.
     *   - key-rotation-overdue: keys exist but the active key has exceeded its
     *     rotation interval. Emits audit event `key-rotation-overdue`.
     *
     * Returns true for BOTH states (rotation is needed in either case).
     */
    isRotationDue() {
        const versions = this.loadVersions();
        const active = versions.find(v => v.active);
        if (!active) {
            // SYSTEMIC-M4: empty store is a distinct CRITICAL state -- no key
            // means nothing can be signed or verified. Emit a dedicated event
            // so alert routing can treat it with higher urgency than a stale key.
            this.audit('key-store-empty', {
                severity: 'CRITICAL',
                reason: 'No active key version found. Runtime is in an unsafe state: signing and verification are unavailable.',
            });
            log.error('key-store-empty: no active key version exists. Immediate remediation required.', {
                event: 'key-store-empty',
                severity: 'CRITICAL',
            });
            return true;
        }
        const age = Date.now() - new Date(active.createdAt).getTime();
        if (age > this.config.rotationIntervalMs) {
            // SYSTEMIC-M4: rotation overdue is distinct from empty-store.
            // Keys exist and the runtime can still operate, but the active key
            // is stale and should be rotated as soon as possible.
            this.audit('key-rotation-overdue', {
                severity: 'HIGH',
                activeVersion: active.version,
                keyId: active.keyId,
                ageMs: age,
                rotationIntervalMs: this.config.rotationIntervalMs,
            });
            log.warn('key-rotation-overdue: active key has exceeded rotation interval.', {
                event: 'key-rotation-overdue',
                activeVersion: active.version,
                ageMs: age,
            });
            return true;
        }
        return false;
    }
    /**
     * Get all key versions (metadata only, no key material).
     */
    listVersions() {
        return this.loadVersions();
    }
    /**
     * Get the active key version number.
     */
    getActiveVersion() {
        const versions = this.loadVersions();
        const active = versions.find(v => v.active);
        return active?.version ?? -1;
    }
    /**
     * Verify a key fingerprint matches.
     */
    verifyFingerprint(version) {
        const versions = this.loadVersions();
        const v = versions.find(kv => kv.version === version);
        if (!v)
            return false;
        const expected = this.computeFingerprint(version);
        // F-NEW-CRIT-35: length-check before timingSafeEqual to avoid
        // ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH on corrupted fingerprint state.
        const fpBuf = Buffer.from(v.keyFingerprint, 'hex');
        const expBuf = Buffer.from(expected, 'hex');
        if (fpBuf.length !== expBuf.length)
            return false;
        return crypto.timingSafeEqual(fpBuf, expBuf);
    }
    /**
     * Get key management statistics.
     */
    getStats() {
        const versions = this.loadVersions();
        const active = versions.find(v => v.active);
        const lastRotated = versions
            .filter(v => v.rotatedAt)
            .sort((a, b) => new Date(b.rotatedAt).getTime() - new Date(a.rotatedAt).getTime())[0];
        let nextRotation;
        if (active) {
            const next = new Date(active.createdAt).getTime() + this.config.rotationIntervalMs;
            nextRotation = new Date(next).toISOString();
        }
        return {
            activeVersion: active?.version ?? -1,
            totalVersions: versions.length,
            rotationDue: this.isRotationDue(),
            lastRotation: lastRotated?.rotatedAt,
            nextRotation,
        };
    }
    // --- Private ---
    initializeFirstKey() {
        const keyId = `key_${crypto.randomBytes(8).toString('hex')}`;
        const versions = [{
                version: 1,
                keyId,
                createdAt: new Date().toISOString(),
                active: true,
                keyFingerprint: this.computeFingerprint(1),
            }];
        this.saveVersions(versions);
        this.audit('KEY_INITIALIZED', { version: 1, keyId });
    }
    /**
     * Get or create the persistent random salt used for HKDF key derivation.
     * Generated once at first init and persisted to disk, preventing
     * retroactive key computation if the master secret is compromised alone.
     */
    getPersistedSalt() {
        const saltFile = path.join(this.config.keyDir, 'hkdf-salt.bin');
        if (fs.existsSync(saltFile)) {
            const salt = fs.readFileSync(saltFile);
            if (salt.length === 32)
                return salt;
        }
        // Generate and persist a new random salt
        const salt = crypto.randomBytes(32);
        fs.writeFileSync(saltFile, salt);
        return salt;
    }
    deriveKey(version) {
        // Derive unique key per version using HKDF with persisted random salt.
        // The salt is generated once at first init and stored on disk,
        // preventing retroactive key computation from master secret alone.
        const salt = this.getPersistedSalt();
        const info = `governance-key-v${version}`;
        return Buffer.from(crypto.hkdfSync('sha256', this.config.masterSecret, salt, info, 32));
    }
    computeFingerprint(version) {
        const key = this.deriveKey(version);
        const fingerprintKey = crypto.createHash('sha256')
            .update(this.config.masterSecret + ':fingerprint')
            .digest();
        return crypto.createHmac('sha256', fingerprintKey)
            .update(key)
            .digest('hex');
    }
    pruneOldVersions(versions) {
        const inactive = versions
            .filter(v => !v.active)
            .sort((a, b) => b.version - a.version);
        if (inactive.length > this.config.maxKeyVersions) {
            const toRemove = new Set(inactive.slice(this.config.maxKeyVersions).map(v => v.version));
            for (let i = versions.length - 1; i >= 0; i--) {
                if (toRemove.has(versions[i].version)) {
                    versions.splice(i, 1);
                }
            }
        }
    }
    loadVersions() {
        if (!fs.existsSync(this.keysFile))
            return [];
        try {
            return JSON.parse(fs.readFileSync(this.keysFile, 'utf-8'));
        }
        catch {
            return [];
        }
    }
    saveVersions(versions) {
        fs.writeFileSync(this.keysFile, JSON.stringify(versions, null, 2));
    }
    audit(action, data) {
        const entry = { timestamp: new Date().toISOString(), action, ...data };
        try {
            fs.appendFileSync(this.auditFile, JSON.stringify(entry) + '\n');
        }
        catch {
            log.error('Failed to write key audit', { action });
        }
    }
}
//# sourceMappingURL=key-manager.js.map