"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalEncryptedSecretStore = exports.SECRET_STORE_ALLOW_LOCAL_MASTER_KEY_ENV = exports.SECRET_STORE_KEYRING_ACCOUNT_ENV = exports.SECRET_STORE_KEYRING_SERVICE_ENV = exports.SECRET_STORE_MASTER_KEY_ENV = exports.SECRET_STORE_DIR_ENV = void 0;
const crypto_1 = __importDefault(require("crypto"));
const child_process_1 = require("child_process");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const sodium = require('sodium-native');
exports.SECRET_STORE_DIR_ENV = 'SMARTPERFETTO_SECRET_STORE_DIR';
exports.SECRET_STORE_MASTER_KEY_ENV = 'SMARTPERFETTO_SECRET_STORE_MASTER_KEY';
exports.SECRET_STORE_KEYRING_SERVICE_ENV = 'SMARTPERFETTO_SECRET_STORE_KEYRING_SERVICE';
exports.SECRET_STORE_KEYRING_ACCOUNT_ENV = 'SMARTPERFETTO_SECRET_STORE_KEYRING_ACCOUNT';
exports.SECRET_STORE_ALLOW_LOCAL_MASTER_KEY_ENV = 'SMARTPERFETTO_SECRET_STORE_ALLOW_LOCAL_MASTER_KEY';
function resolveSecretStoreDir() {
    const configured = process.env[exports.SECRET_STORE_DIR_ENV];
    return path_1.default.resolve(configured && configured.trim().length > 0
        ? configured
        : path_1.default.join(process.cwd(), 'data', 'secrets'));
}
function decodeMasterKey(raw) {
    const trimmed = raw.trim();
    if (/^[a-fA-F0-9]{64}$/.test(trimmed)) {
        return Buffer.from(trimmed, 'hex');
    }
    try {
        const decoded = Buffer.from(trimmed, 'base64');
        if (decoded.length === sodium.crypto_secretbox_KEYBYTES)
            return decoded;
    }
    catch {
        // Fall through to passphrase hashing.
    }
    return crypto_1.default.createHash('sha256').update(trimmed, 'utf8').digest();
}
function encodeMasterKey(key) {
    return key.toString('base64');
}
function randomMasterKey() {
    const key = Buffer.alloc(sodium.crypto_secretbox_KEYBYTES);
    sodium.randombytes_buf(key);
    return key;
}
function randomNonce() {
    const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES);
    sodium.randombytes_buf(nonce);
    return nonce;
}
function keyringService() {
    return process.env[exports.SECRET_STORE_KEYRING_SERVICE_ENV]?.trim() || 'SmartPerfetto SecretStore';
}
function keyringAccount() {
    return process.env[exports.SECRET_STORE_KEYRING_ACCOUNT_ENV]?.trim() || 'provider-master-key';
}
function readMacosKeyring(service, account) {
    try {
        return (0, child_process_1.execFileSync)('/usr/bin/security', [
            'find-generic-password',
            '-s',
            service,
            '-a',
            account,
            '-w',
        ], {
            encoding: 'utf-8',
            stdio: ['ignore', 'pipe', 'ignore'],
        }).trim();
    }
    catch {
        return null;
    }
}
function writeMacosKeyring(service, account, secret) {
    (0, child_process_1.execFileSync)('/usr/bin/security', [
        'add-generic-password',
        '-U',
        '-s',
        service,
        '-a',
        account,
        '-w',
        secret,
    ], { stdio: 'ignore' });
}
function readLinuxKeyring(service, account) {
    try {
        return (0, child_process_1.execFileSync)('secret-tool', [
            'lookup',
            'service',
            service,
            'account',
            account,
        ], {
            encoding: 'utf-8',
            stdio: ['ignore', 'pipe', 'ignore'],
        }).trim();
    }
    catch {
        return null;
    }
}
function writeLinuxKeyring(service, account, secret) {
    (0, child_process_1.execFileSync)('secret-tool', [
        'store',
        '--label',
        service,
        'service',
        service,
        'account',
        account,
    ], {
        input: secret,
        stdio: ['pipe', 'ignore', 'ignore'],
    });
}
function readKeyringSecret() {
    const service = keyringService();
    const account = keyringAccount();
    if (process.platform === 'darwin')
        return readMacosKeyring(service, account);
    if (process.platform === 'linux')
        return readLinuxKeyring(service, account);
    return null;
}
function writeKeyringSecret(secret) {
    const service = keyringService();
    const account = keyringAccount();
    if (process.platform === 'darwin') {
        writeMacosKeyring(service, account, secret);
        return;
    }
    if (process.platform === 'linux') {
        writeLinuxKeyring(service, account, secret);
        return;
    }
    throw new Error(`OS keyring is not supported on platform ${process.platform}`);
}
function readLegacyLocalMasterKey(dir) {
    const keyPath = path_1.default.join(dir, '.master-key');
    if (!fs_1.default.existsSync(keyPath))
        return null;
    return decodeMasterKey(fs_1.default.readFileSync(keyPath, 'utf-8'));
}
function readOrCreateLocalDevMasterKey(dir) {
    const existing = readLegacyLocalMasterKey(dir);
    if (existing)
        return existing;
    const key = randomMasterKey();
    const keyPath = path_1.default.join(dir, '.master-key');
    fs_1.default.mkdirSync(dir, { recursive: true });
    fs_1.default.writeFileSync(keyPath, encodeMasterKey(key), { mode: 0o600 });
    try {
        fs_1.default.chmodSync(keyPath, 0o600);
    }
    catch { /* Windows */ }
    return key;
}
function localMasterKeyFallbackAllowed() {
    const configured = process.env[exports.SECRET_STORE_ALLOW_LOCAL_MASTER_KEY_ENV];
    if (!configured)
        return process.env.NODE_ENV === 'test';
    return ['1', 'true', 'yes', 'on', 'enabled'].includes(configured.trim().toLowerCase());
}
function resolveMasterKey(dir) {
    const configured = process.env[exports.SECRET_STORE_MASTER_KEY_ENV];
    if (configured && configured.trim().length > 0) {
        return { key: decodeMasterKey(configured), source: 'env' };
    }
    const keyringSecret = readKeyringSecret();
    if (keyringSecret) {
        return { key: decodeMasterKey(keyringSecret), source: 'keyring' };
    }
    const legacyLocalKey = readLegacyLocalMasterKey(dir);
    if (legacyLocalKey) {
        try {
            writeKeyringSecret(encodeMasterKey(legacyLocalKey));
            return { key: legacyLocalKey, source: 'keyring' };
        }
        catch {
            if (localMasterKeyFallbackAllowed()) {
                return { key: legacyLocalKey, source: 'local-dev-file' };
            }
            throw new Error(`SecretStore master key exists only in local file ${path_1.default.join(dir, '.master-key')}; OS keyring is unavailable. Set ${exports.SECRET_STORE_MASTER_KEY_ENV} for tests/dev or configure OS keyring.`);
        }
    }
    const newKey = randomMasterKey();
    try {
        writeKeyringSecret(encodeMasterKey(newKey));
        return { key: newKey, source: 'keyring' };
    }
    catch {
        if (localMasterKeyFallbackAllowed()) {
            return { key: readOrCreateLocalDevMasterKey(dir), source: 'local-dev-file' };
        }
        throw new Error(`OS keyring is unavailable for SecretStore master key. Set ${exports.SECRET_STORE_MASTER_KEY_ENV} for tests/dev or install/configure an OS keyring provider.`);
    }
}
function emptySecretFile() {
    return { version: 2, entries: {} };
}
function normalizeSecretObject(value) {
    return Object.fromEntries(Object.entries(value)
        .filter(([, v]) => typeof v === 'string' && v.length > 0)
        .sort(([a], [b]) => a.localeCompare(b)));
}
class LocalEncryptedSecretStore {
    constructor(dir = resolveSecretStoreDir()) {
        this.dir = dir;
        this.filePath = path_1.default.join(dir, 'provider-secrets.enc.json');
        const resolved = resolveMasterKey(dir);
        this.key = resolved.key;
        this.masterKeySource = resolved.source;
    }
    info() {
        return {
            filePath: this.filePath,
            masterKeySource: this.masterKeySource,
            algorithm: 'libsodium-secretbox',
        };
    }
    get(ref) {
        const file = this.readFile();
        const entry = file.entries[ref];
        if (!entry)
            return {};
        try {
            const ciphertext = Buffer.from(entry.ciphertext, 'base64');
            if (ciphertext.length < sodium.crypto_secretbox_MACBYTES) {
                throw new Error('ciphertext too short');
            }
            const plaintext = Buffer.alloc(ciphertext.length - sodium.crypto_secretbox_MACBYTES);
            const ok = sodium.crypto_secretbox_open_easy(plaintext, ciphertext, Buffer.from(entry.nonce, 'base64'), this.key);
            if (!ok)
                throw new Error('secretbox authentication failed');
            const parsed = JSON.parse(plaintext.toString('utf-8'));
            return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
                ? parsed
                : {};
        }
        catch (err) {
            console.warn('[LocalSecretStore] Failed to decrypt secret:', err.message);
            return {};
        }
    }
    put(ref, value) {
        return this.writeEncrypted(ref, value);
    }
    rotate(ref, value) {
        const next = value ?? this.get(ref);
        return this.writeEncrypted(ref, next);
    }
    getVersion(ref) {
        return this.readFile().entries[ref]?.version;
    }
    delete(ref) {
        const file = this.readFile();
        const existed = Object.prototype.hasOwnProperty.call(file.entries, ref);
        if (!existed)
            return false;
        delete file.entries[ref];
        this.writeFile(file);
        return true;
    }
    writeEncrypted(ref, value) {
        const file = this.readFile();
        const previous = file.entries[ref];
        const version = (previous?.version ?? 0) + 1;
        file.entries[ref] = this.encryptEntry(Buffer.from(JSON.stringify(normalizeSecretObject(value)), 'utf-8'), version, Date.now());
        this.writeFile(file);
        return version;
    }
    readFile() {
        if (!fs_1.default.existsSync(this.filePath))
            return emptySecretFile();
        try {
            const parsed = JSON.parse(fs_1.default.readFileSync(this.filePath, 'utf-8'));
            if (parsed && parsed.version === 2 && parsed.entries && typeof parsed.entries === 'object') {
                return parsed;
            }
            if (parsed && parsed.version === 1 && parsed.entries && typeof parsed.entries === 'object') {
                const migrated = this.migrateLegacyFile(parsed);
                this.writeFile(migrated);
                return migrated;
            }
            return emptySecretFile();
        }
        catch {
            return emptySecretFile();
        }
    }
    encryptEntry(plaintext, version, updatedAt) {
        const nonce = randomNonce();
        const ciphertext = Buffer.alloc(plaintext.length + sodium.crypto_secretbox_MACBYTES);
        sodium.crypto_secretbox_easy(ciphertext, plaintext, nonce, this.key);
        return {
            version,
            algorithm: 'libsodium-secretbox',
            nonce: nonce.toString('base64'),
            ciphertext: ciphertext.toString('base64'),
            updatedAt,
        };
    }
    migrateLegacyFile(file) {
        const migrated = emptySecretFile();
        for (const [ref, entry] of Object.entries(file.entries)) {
            try {
                const decipher = crypto_1.default.createDecipheriv(entry.algorithm, this.key, Buffer.from(entry.iv, 'base64'));
                decipher.setAuthTag(Buffer.from(entry.tag, 'base64'));
                const plaintext = Buffer.concat([
                    decipher.update(Buffer.from(entry.ciphertext, 'base64')),
                    decipher.final(),
                ]);
                migrated.entries[ref] = this.encryptEntry(plaintext, entry.version, entry.updatedAt);
            }
            catch (err) {
                console.warn('[LocalSecretStore] Failed to migrate legacy AES secret:', err.message);
            }
        }
        return migrated;
    }
    writeFile(file) {
        fs_1.default.mkdirSync(this.dir, { recursive: true });
        const tmp = `${this.filePath}.tmp`;
        fs_1.default.writeFileSync(tmp, JSON.stringify(file, null, 2), { mode: 0o600 });
        fs_1.default.renameSync(tmp, this.filePath);
        try {
            fs_1.default.chmodSync(this.filePath, 0o600);
        }
        catch { /* Windows */ }
    }
}
exports.LocalEncryptedSecretStore = LocalEncryptedSecretStore;
//# sourceMappingURL=localSecretStore.js.map