"use strict";
/**
 * EncryptionLayer -- AES-256-GCM encryption for data at rest.
 *
 * AIAG-014 STATUS: Sole owner of at-rest AES-256-GCM. EncryptedStorage
 * delegates here; no other module should call `crypto.createCipheriv`
 * directly. Two formats coexist:
 *
 *   V2 (current, preferred): uses KeyManager for key material. HKDF
 *   derivation with a random persisted salt (AIAG-016). Format:
 *     CONNEXUM-ENC-V2 | keyVersion(4B) | iv(12B) | tag(16B) | ciphertext
 *
 *   V1 (deprecated, read-only after V2 migration complete): passphrase
 *   + PBKDF2 (100k iterations) + per-file random salt. Format:
 *     CONNEXUM-ENC-V1 | salt(32B) | iv(12B) | tag(16B) | ciphertext
 *
 * New code MUST pass `keyManager` so V2 is selected. V1 remains only
 * so callers without KeyManager (test fixtures, legacy file reads)
 * continue to work until the ecosystem finishes migrating.
 *
 * Encrypts audit logs, session state, memory files, and any
 * governance data before writing to disk. Decrypts on read.
 *
 * Required by:
 *   - HIPAA 164.312(a)(2)(iv): Encryption and decryption
 *   - GDPR Art. 32: Security of processing
 *   - PCI DSS Req 3: Protect stored data
 *   - SOC 2 CC6.1: Logical and physical access
 *   - ISO 27001 8.24: Use of cryptography
 *
 * @connexum/ai-governance
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.EncryptionLayer = void 0;
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const MAGIC_HEADER = Buffer.from('CONNEXUM-ENC-V1\0');
const MAGIC_HEADER_V2 = Buffer.from('CONNEXUM-ENC-V2\0');
// Pre-AIAG-014 legacy formats produced by the old EncryptedStorage.
// Kept read-only for backwards compatibility; all new writes go through
// MAGIC_HEADER / MAGIC_HEADER_V2.
const LEGACY_CXENC1 = Buffer.from('CXENC1'); // passphrase + PBKDF2
const LEGACY_CXENC2 = Buffer.from('CXENC2'); // KeyManager version
const SALT_LENGTH = 32;
const IV_LENGTH = 12;
const TAG_LENGTH = 16;
const KEY_LENGTH = 32; // 256 bits
const KEY_VERSION_LENGTH = 4; // uint32 for key version
const PBKDF2_ITERATIONS = 100000;
class EncryptionLayer {
    passphrase;
    iterations;
    logDir;
    keyManager;
    constructor(config) {
        if (!config.passphrase || config.passphrase.length < 16) {
            throw new Error('Passphrase must be at least 16 characters');
        }
        this.passphrase = config.passphrase;
        this.iterations = config.iterations ?? PBKDF2_ITERATIONS;
        this.logDir = config.logDir || null;
        this.keyManager = config.keyManager;
        if (this.logDir && !fs.existsSync(this.logDir)) {
            fs.mkdirSync(this.logDir, { recursive: true });
        }
    }
    /**
     * Encrypt a buffer using AES-256-GCM.
     * Uses KeyManager (V2) when available, falls back to passphrase (V1).
     */
    encrypt(plaintext) {
        if (this.keyManager) {
            // V2: Use KeyManager for key material
            const { key, version } = this.keyManager.getCurrentKey();
            const iv = crypto.randomBytes(IV_LENGTH);
            const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
            const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
            const tag = cipher.getAuthTag();
            const versionBuf = Buffer.alloc(KEY_VERSION_LENGTH);
            versionBuf.writeUInt32BE(version, 0);
            // V2 Format: HEADER_V2 | keyVersion(4B) | IV | TAG | CIPHERTEXT
            return Buffer.concat([MAGIC_HEADER_V2, versionBuf, iv, tag, encrypted]);
        }
        // V1: Legacy passphrase-based
        const salt = crypto.randomBytes(SALT_LENGTH);
        const iv = crypto.randomBytes(IV_LENGTH);
        const key = this.deriveKey(salt);
        const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
        const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
        const tag = cipher.getAuthTag();
        // V1 Format: HEADER | SALT | IV | TAG | CIPHERTEXT
        return Buffer.concat([MAGIC_HEADER, salt, iv, tag, encrypted]);
    }
    /**
     * Decrypt a buffer. Handles:
     *   - CONNEXUM-ENC-V2 (current, KeyManager-backed)
     *   - CONNEXUM-ENC-V1 (current, passphrase + PBKDF2)
     *   - CXENC2 (legacy, KeyManager-backed; pre-AIAG-014)
     *   - CXENC1 (legacy, passphrase + PBKDF2; pre-AIAG-014)
     */
    decrypt(data) {
        try {
            // Legacy CXENC2 (KeyManager)
            if (data.length >= LEGACY_CXENC2.length && data.subarray(0, LEGACY_CXENC2.length).equals(LEGACY_CXENC2)) {
                if (!this.keyManager) {
                    return { success: false, content: Buffer.alloc(0), error: 'CXENC2 (legacy) requires KeyManager for decryption' };
                }
                let offset = LEGACY_CXENC2.length;
                const keyVersion = data.readUInt32BE(offset);
                offset += KEY_VERSION_LENGTH;
                const iv = data.subarray(offset, offset + IV_LENGTH);
                offset += IV_LENGTH;
                const tag = data.subarray(offset, offset + TAG_LENGTH);
                offset += TAG_LENGTH;
                const ciphertext = data.subarray(offset);
                const keyData = this.keyManager.getKeyByVersion(keyVersion);
                if (!keyData) {
                    return { success: false, content: Buffer.alloc(0), error: `Key version ${keyVersion} not found (legacy CXENC2)` };
                }
                const decipher = crypto.createDecipheriv('aes-256-gcm', keyData.key, iv);
                decipher.setAuthTag(tag);
                const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
                return { success: true, content: decrypted };
            }
            // Legacy CXENC1 (passphrase + PBKDF2)
            if (data.length >= LEGACY_CXENC1.length && data.subarray(0, LEGACY_CXENC1.length).equals(LEGACY_CXENC1)) {
                let offset = LEGACY_CXENC1.length;
                const salt = data.subarray(offset, offset + SALT_LENGTH);
                offset += SALT_LENGTH;
                const iv = data.subarray(offset, offset + IV_LENGTH);
                offset += IV_LENGTH;
                const tag = data.subarray(offset, offset + TAG_LENGTH);
                offset += TAG_LENGTH;
                const ciphertext = data.subarray(offset);
                const key = this.deriveKey(salt);
                const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
                decipher.setAuthTag(tag);
                const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
                return { success: true, content: decrypted };
            }
            // Check for V2 format first
            if (data.length >= MAGIC_HEADER_V2.length && data.subarray(0, MAGIC_HEADER_V2.length).equals(MAGIC_HEADER_V2)) {
                if (!this.keyManager) {
                    return { success: false, content: Buffer.alloc(0), error: 'V2 encrypted data requires KeyManager for decryption' };
                }
                let offset = MAGIC_HEADER_V2.length;
                const keyVersion = data.readUInt32BE(offset);
                offset += KEY_VERSION_LENGTH;
                const iv = data.subarray(offset, offset + IV_LENGTH);
                offset += IV_LENGTH;
                const tag = data.subarray(offset, offset + TAG_LENGTH);
                offset += TAG_LENGTH;
                const ciphertext = data.subarray(offset);
                const keyData = this.keyManager.getKeyByVersion(keyVersion);
                if (!keyData) {
                    return { success: false, content: Buffer.alloc(0), error: `Key version ${keyVersion} not found` };
                }
                const decipher = crypto.createDecipheriv('aes-256-gcm', keyData.key, iv);
                decipher.setAuthTag(tag);
                const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
                return { success: true, content: decrypted };
            }
            // V1 format
            if (data.length < MAGIC_HEADER.length + SALT_LENGTH + IV_LENGTH + TAG_LENGTH) {
                return { success: false, content: Buffer.alloc(0), error: 'Data too short for encrypted format' };
            }
            const header = data.subarray(0, MAGIC_HEADER.length);
            if (!header.equals(MAGIC_HEADER)) {
                return { success: false, content: Buffer.alloc(0), error: 'Not an encrypted file (missing header)' };
            }
            let offset = MAGIC_HEADER.length;
            const salt = data.subarray(offset, offset + SALT_LENGTH);
            offset += SALT_LENGTH;
            const iv = data.subarray(offset, offset + IV_LENGTH);
            offset += IV_LENGTH;
            const tag = data.subarray(offset, offset + TAG_LENGTH);
            offset += TAG_LENGTH;
            const ciphertext = data.subarray(offset);
            const key = this.deriveKey(salt);
            const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
            decipher.setAuthTag(tag);
            const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
            return { success: true, content: decrypted };
        }
        catch (err) {
            return { success: false, content: Buffer.alloc(0), error: `Decryption failed: ${err.message}` };
        }
    }
    /**
     * Encrypt a string and return as a buffer.
     */
    encryptString(plaintext) {
        return this.encrypt(Buffer.from(plaintext, 'utf-8'));
    }
    /**
     * Decrypt a buffer and return as a string.
     */
    decryptString(data) {
        const result = this.decrypt(data);
        if (!result.success)
            return null;
        return result.content.toString('utf-8');
    }
    /**
     * Encrypt a file in place. Creates .bak backup first.
     */
    encryptFile(filePath) {
        try {
            if (!fs.existsSync(filePath)) {
                return { success: false, path: filePath, originalSize: 0, encryptedSize: 0, error: 'File not found' };
            }
            const plaintext = fs.readFileSync(filePath);
            // Don't double-encrypt
            if (this.isEncrypted(plaintext)) {
                return { success: true, path: filePath, originalSize: plaintext.length, encryptedSize: plaintext.length, error: 'Already encrypted' };
            }
            // Backup
            fs.copyFileSync(filePath, filePath + '.bak');
            const encrypted = this.encrypt(plaintext);
            fs.writeFileSync(filePath, encrypted);
            this.logOperation('encrypt', filePath, plaintext.length, encrypted.length);
            return { success: true, path: filePath, originalSize: plaintext.length, encryptedSize: encrypted.length };
        }
        catch (err) {
            return { success: false, path: filePath, originalSize: 0, encryptedSize: 0, error: err.message };
        }
    }
    /**
     * Decrypt a file in place.
     */
    decryptFile(filePath) {
        try {
            if (!fs.existsSync(filePath)) {
                return { success: false, path: filePath, originalSize: 0, encryptedSize: 0, error: 'File not found' };
            }
            const encrypted = fs.readFileSync(filePath);
            if (!this.isEncrypted(encrypted)) {
                return { success: true, path: filePath, originalSize: encrypted.length, encryptedSize: encrypted.length, error: 'Not encrypted' };
            }
            const result = this.decrypt(encrypted);
            if (!result.success) {
                return { success: false, path: filePath, originalSize: encrypted.length, encryptedSize: 0, error: result.error };
            }
            fs.writeFileSync(filePath, result.content);
            this.logOperation('decrypt', filePath, encrypted.length, result.content.length);
            return { success: true, path: filePath, originalSize: encrypted.length, encryptedSize: result.content.length };
        }
        catch (err) {
            return { success: false, path: filePath, originalSize: 0, encryptedSize: 0, error: err.message };
        }
    }
    /**
     * Check if a buffer is encrypted with our format (V1 or V2).
     */
    isEncrypted(data) {
        if (data.length < LEGACY_CXENC1.length)
            return false;
        return (data.subarray(0, MAGIC_HEADER.length).equals(MAGIC_HEADER) ||
            data.subarray(0, MAGIC_HEADER_V2.length).equals(MAGIC_HEADER_V2) ||
            data.subarray(0, LEGACY_CXENC1.length).equals(LEGACY_CXENC1) ||
            data.subarray(0, LEGACY_CXENC2.length).equals(LEGACY_CXENC2));
    }
    /**
     * Check if a file on disk is encrypted (V1 or V2).
     */
    isFileEncrypted(filePath) {
        if (!fs.existsSync(filePath))
            return false;
        const fd = fs.openSync(filePath, 'r');
        const header = Buffer.alloc(MAGIC_HEADER_V2.length);
        fs.readSync(fd, header, 0, MAGIC_HEADER_V2.length, 0);
        fs.closeSync(fd);
        return header.equals(MAGIC_HEADER) || header.equals(MAGIC_HEADER_V2);
    }
    /**
     * Read an encrypted file, decrypt, and return content as string.
     * If file is not encrypted, returns content as-is.
     */
    readEncrypted(filePath) {
        if (!fs.existsSync(filePath))
            return null;
        const data = fs.readFileSync(filePath);
        if (this.isEncrypted(data)) {
            return this.decryptString(data);
        }
        return data.toString('utf-8');
    }
    /**
     * Write content encrypted to a file.
     */
    writeEncrypted(filePath, content) {
        const dir = path.dirname(filePath);
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        const encrypted = this.encryptString(content);
        fs.writeFileSync(filePath, encrypted);
    }
    /**
     * Append to an encrypted JSONL file.
     * Reads existing content, decrypts, appends line, re-encrypts.
     */
    appendEncryptedJsonl(filePath, entry) {
        let existing = '';
        if (fs.existsSync(filePath)) {
            const data = fs.readFileSync(filePath);
            if (this.isEncrypted(data)) {
                existing = this.decryptString(data) || '';
            }
            else {
                existing = data.toString('utf-8');
            }
        }
        const updated = existing + JSON.stringify(entry) + '\n';
        this.writeEncrypted(filePath, updated);
    }
    deriveKey(salt) {
        return crypto.pbkdf2Sync(this.passphrase, salt, this.iterations, KEY_LENGTH, 'sha256');
    }
    logOperation(operation, filePath, originalSize, resultSize) {
        if (!this.logDir)
            return;
        const logFile = path.join(this.logDir, `encryption-${new Date().toISOString().split('T')[0]}.jsonl`);
        const entry = {
            timestamp: new Date().toISOString(),
            operation,
            file: filePath,
            originalSize,
            resultSize,
        };
        try {
            fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
        }
        catch {
            // Log failure is non-critical for encryption operations
        }
    }
}
exports.EncryptionLayer = EncryptionLayer;
//# sourceMappingURL=encryption-layer.js.map