/**
 * MemoryIntegrity -- Cryptographic verification for agent memory files.
 *
 * Every memory write gets a SHA-256 hash footer. Every read verifies
 * the hash. Tampered files are flagged, backed up copies are restored.
 * Addresses OWASP ASI06 (Memory Poisoning).
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
/**
 * Validate an HMAC key for memory integrity. Mirrors the provenance-signer key
 * discipline: must be a string of at least 32 characters.
 */
export function validateMemoryHmacKey(key) {
    if (typeof key !== 'string' || key.length < 32) {
        return { valid: false, reason: 'MEMORY_INTEGRITY_HMAC_KEY must be a string of at least 32 characters.' };
    }
    return { valid: true, reason: 'ok' };
}
/** Footer algorithm token used when a keyed HMAC is configured. */
const HMAC_ALGO_TOKEN = 'hmac-sha256';
/** Plain-hash algorithms accepted in the backward-compat (no-key) path. */
const ALLOWED_PLAIN_ALGOS = new Set(['sha256', 'sha384', 'sha512']);
/**
 * Constant-time comparison of two hex digest strings (Shield ASI-06 F-1;
 * codebase F-NEW-CRIT-35 standard). Length-mismatch returns false without a
 * timingSafeEqual call (which would throw on unequal lengths).
 */
function hashesEqualConstantTime(a, b) {
    let bufA;
    let bufB;
    try {
        bufA = Buffer.from(a, 'hex');
        bufB = Buffer.from(b, 'hex');
    }
    catch {
        return false;
    }
    if (bufA.length === 0 || bufA.length !== bufB.length)
        return false;
    return crypto.timingSafeEqual(bufA, bufB);
}
const HASH_PATTERN = /<!-- HASH:([\w-]+):([a-f0-9]+) -->$/;
/** Footer-stripping pattern — allows hyphenated algo tokens (e.g. hmac-sha256). */
const HASH_FOOTER_STRIP = /\n<!-- HASH:[\w-]+:[a-f0-9]+ -->\s*$/;
export class MemoryIntegrity {
    config;
    constructor(config) {
        this.config = {
            memoryDir: config.memoryDir,
            agentName: config.agentName,
            auditLogPath: config.auditLogPath || path.join(config.memoryDir, '..', 'audit', 'memory-integrity.jsonl'),
            protectedExtensions: config.protectedExtensions || ['.md'],
            algorithm: config.algorithm || 'sha256',
            hmacKey: config.hmacKey ?? '',
        };
        if (this.config.hmacKey) {
            const check = validateMemoryHmacKey(this.config.hmacKey);
            if (!check.valid) {
                throw new Error(`MemoryIntegrity: ${check.reason}`);
            }
        }
    }
    /** True when this instance is configured for keyed-HMAC integrity (ASI-06). */
    get hmacEnabled() {
        return this.config.hmacKey.length > 0;
    }
    /**
     * Compute the integrity footer for `content`. Returns the algorithm token to
     * embed and the digest. Keyed HMAC when configured, else the plain hash
     * (backward-compatible).
     */
    computeDigest(content) {
        if (this.hmacEnabled) {
            const digest = crypto.createHmac('sha256', this.config.hmacKey).update(content).digest('hex');
            return { token: HMAC_ALGO_TOKEN, digest };
        }
        const digest = crypto.createHash(this.config.algorithm).update(content).digest('hex');
        return { token: this.config.algorithm, digest };
    }
    /**
     * Write content to a memory file with integrity hash.
     * Creates a backup of the previous version.
     */
    writeMemory(filePath, content) {
        const resolved = this.resolveMemoryPath(filePath);
        const dir = path.dirname(resolved);
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        // Create backup of existing file
        let backupCreated = false;
        if (fs.existsSync(resolved)) {
            const backupPath = resolved + '.bak';
            fs.copyFileSync(resolved, backupPath);
            backupCreated = true;
        }
        // Strip any existing hash footer from content
        const cleanContent = content.replace(HASH_FOOTER_STRIP, '');
        // Compute the integrity footer (keyed HMAC when configured, else plain hash).
        const { token, digest: hash } = this.computeDigest(cleanContent);
        // Write content with hash footer
        const finalContent = cleanContent + `\n<!-- HASH:${token}:${hash} -->`;
        fs.writeFileSync(resolved, finalContent);
        this.logEvent('write', resolved, { hash, backupCreated });
        return { file: resolved, hash, backupCreated };
    }
    /**
     * Read a memory file and verify its integrity.
     * Returns the content (without hash footer) if valid.
     * Attempts restoration from backup if tampered.
     */
    readMemory(filePath) {
        const resolved = this.resolveMemoryPath(filePath);
        const verification = this.verifyFile(resolved);
        if (!verification.valid && verification.restoredFromBackup) {
            // Re-read after restoration
            const raw = fs.readFileSync(resolved, 'utf-8');
            const content = raw.replace(HASH_FOOTER_STRIP, '');
            return { content, verification };
        }
        if (!verification.valid && !verification.restoredFromBackup) {
            // Tampered and no backup available
            this.logEvent('tamper-detected', resolved, {
                expectedHash: verification.expectedHash,
                actualHash: verification.actualHash,
                restored: false,
            });
            // Return content anyway but mark as tampered
            const raw = fs.existsSync(resolved) ? fs.readFileSync(resolved, 'utf-8') : '';
            const content = raw.replace(HASH_FOOTER_STRIP, '');
            return { content, verification };
        }
        const raw = fs.readFileSync(resolved, 'utf-8');
        const content = raw.replace(HASH_FOOTER_STRIP, '');
        return { content, verification };
    }
    /**
     * Verify integrity of a single memory file.
     */
    verifyFile(filePath) {
        const resolved = this.resolveMemoryPath(filePath);
        if (!fs.existsSync(resolved)) {
            return {
                file: resolved,
                valid: false,
                hasHash: false,
                restoredFromBackup: false,
                error: 'File does not exist',
            };
        }
        const raw = fs.readFileSync(resolved, 'utf-8');
        const match = raw.match(HASH_PATTERN);
        if (!match) {
            // No hash footer. Backward-compat: a file predating the integrity system
            // is treated as valid-but-unhashed. BUT when a keyed HMAC is configured,
            // a missing footer is a footer-stripping attack — reject it (ASI-06).
            if (this.hmacEnabled) {
                return {
                    file: resolved,
                    valid: false,
                    hasHash: false,
                    restoredFromBackup: false,
                    error: 'HMAC required but file has no integrity footer (possible footer-stripping attack).',
                };
            }
            return {
                file: resolved,
                valid: true, // treat as valid but unhashed
                hasHash: false,
                restoredFromBackup: false,
            };
        }
        const [, algorithm, expectedHash] = match;
        const content = raw.replace(HASH_FOOTER_STRIP, '');
        // ASI-06: recompute with the footer's declared algorithm, but enforce the
        // HMAC policy. When a key is configured, ONLY a keyed hmac-sha256 footer is
        // acceptable — a plain-hash footer is a downgrade attack (an attacker who
        // can write the file can recompute a plain hash, but not a keyed HMAC).
        // When NO key is configured, an hmac-sha256 footer is unverifiable, so it
        // cannot be accepted (no silent pass).
        let actualHash;
        if (algorithm === HMAC_ALGO_TOKEN) {
            if (!this.hmacEnabled) {
                return {
                    file: resolved,
                    valid: false,
                    hasHash: true,
                    expectedHash,
                    restoredFromBackup: false,
                    error: 'File carries an HMAC footer but no MEMORY_INTEGRITY_HMAC_KEY is configured to verify it.',
                };
            }
            actualHash = crypto.createHmac('sha256', this.config.hmacKey).update(content).digest('hex');
        }
        else {
            if (this.hmacEnabled) {
                return {
                    file: resolved,
                    valid: false,
                    hasHash: true,
                    expectedHash,
                    restoredFromBackup: false,
                    error: `Integrity downgrade: footer uses plain '${algorithm}' but a keyed HMAC is required.`,
                };
            }
            // Shield ASI-06 F-3: the algorithm token comes from the (attacker-writable)
            // footer. crypto.createHash() throws on an unknown algo, which would crash
            // the never-throw read path. Allowlist the supported plain hashes and reject
            // anything else as invalid rather than throwing.
            if (!ALLOWED_PLAIN_ALGOS.has(algorithm)) {
                return {
                    file: resolved,
                    valid: false,
                    hasHash: true,
                    expectedHash,
                    restoredFromBackup: false,
                    error: `Unrecognised integrity algorithm '${algorithm}'; rejecting.`,
                };
            }
            actualHash = crypto.createHash(algorithm).update(content).digest('hex');
        }
        // Shield ASI-06 F-1: constant-time compare (codebase F-NEW-CRIT-35 standard).
        if (hashesEqualConstantTime(expectedHash, actualHash)) {
            return {
                file: resolved,
                valid: true,
                hasHash: true,
                expectedHash,
                actualHash,
                restoredFromBackup: false,
            };
        }
        // Hash mismatch - attempt restore from backup
        const backupPath = resolved + '.bak';
        if (fs.existsSync(backupPath)) {
            this.logEvent('tamper-detected', resolved, {
                expectedHash,
                actualHash,
                restored: true,
            });
            fs.copyFileSync(backupPath, resolved);
            return {
                file: resolved,
                valid: false,
                hasHash: true,
                expectedHash,
                actualHash,
                restoredFromBackup: true,
            };
        }
        this.logEvent('tamper-detected', resolved, {
            expectedHash,
            actualHash,
            restored: false,
        });
        return {
            file: resolved,
            valid: false,
            hasHash: true,
            expectedHash,
            actualHash,
            restoredFromBackup: false,
        };
    }
    /**
     * Verify all memory files in the memory directory.
     */
    verifyAll() {
        const results = [];
        if (!fs.existsSync(this.config.memoryDir))
            return results;
        const files = this.listMemoryFiles();
        for (const file of files) {
            results.push(this.verifyFile(file));
        }
        return results;
    }
    /**
     * Add integrity hashes to all existing memory files that lack them.
     * Non-destructive: only adds hash footer, doesn't change content.
     */
    retroHashAll() {
        const results = [];
        if (!fs.existsSync(this.config.memoryDir))
            return results;
        const files = this.listMemoryFiles();
        for (const file of files) {
            const raw = fs.readFileSync(file, 'utf-8');
            if (!HASH_PATTERN.test(raw)) {
                // File has no hash - add one
                const result = this.writeMemory(file, raw);
                results.push(result);
            }
        }
        return results;
    }
    /**
     * Generate a manifest of all memory file hashes.
     */
    generateManifest() {
        const manifest = {};
        const files = this.listMemoryFiles();
        for (const file of files) {
            const raw = fs.readFileSync(file, 'utf-8');
            const content = raw.replace(HASH_FOOTER_STRIP, '');
            // Shield ASI-06 F-2: route through computeDigest so an HMAC-configured
            // instance produces KEYED manifest digests — a plain hash would be forgeable
            // by anyone who can write the memory files, defeating the HMAC upgrade.
            const { digest: hash } = this.computeDigest(content);
            const relative = path.relative(this.config.memoryDir, file);
            manifest[relative] = hash;
        }
        return manifest;
    }
    /**
     * Verify manifest against current files. Returns list of mismatches.
     */
    verifyManifest(manifest) {
        const mismatches = [];
        for (const [relative, expectedHash] of Object.entries(manifest)) {
            const filePath = path.join(this.config.memoryDir, relative);
            if (!fs.existsSync(filePath)) {
                mismatches.push({ file: relative, expected: expectedHash, actual: 'MISSING' });
                continue;
            }
            const raw = fs.readFileSync(filePath, 'utf-8');
            const content = raw.replace(HASH_FOOTER_STRIP, '');
            // Shield ASI-06 F-2: keyed digest in HMAC mode (was plain-hash, forgeable).
            const { digest: actualHash } = this.computeDigest(content);
            // Shield ASI-06 F-1: constant-time compare (codebase F-NEW-CRIT-35 standard).
            if (!hashesEqualConstantTime(expectedHash, actualHash)) {
                mismatches.push({ file: relative, expected: expectedHash, actual: actualHash });
            }
        }
        return mismatches;
    }
    // --- Private helpers ---
    resolveMemoryPath(filePath) {
        if (path.isAbsolute(filePath))
            return filePath;
        return path.join(this.config.memoryDir, filePath);
    }
    listMemoryFiles() {
        const files = [];
        const walk = (dir) => {
            const entries = fs.readdirSync(dir, { withFileTypes: true });
            for (const entry of entries) {
                const full = path.join(dir, entry.name);
                if (entry.isDirectory()) {
                    walk(full);
                }
                else if (this.config.protectedExtensions.some(ext => entry.name.endsWith(ext))) {
                    if (!entry.name.endsWith('.bak')) {
                        files.push(full);
                    }
                }
            }
        };
        walk(this.config.memoryDir);
        return files;
    }
    logEvent(event, file, details) {
        const logDir = path.dirname(this.config.auditLogPath);
        if (!fs.existsSync(logDir)) {
            fs.mkdirSync(logDir, { recursive: true });
        }
        const entry = {
            timestamp: new Date().toISOString(),
            agent: this.config.agentName,
            event: `memory:${event}`,
            file: path.relative(this.config.memoryDir, file),
            ...details,
        };
        fs.appendFileSync(this.config.auditLogPath, JSON.stringify(entry) + '\n');
    }
}
//# sourceMappingURL=memory-integrity.js.map