import utils from 'node:util';
import crypto from 'crypto';
import { getLogger, Encryption } from '@nangohq/utils';
import db from '../db/database.js';
const logger = getLogger('Encryption.Manager');
export const pbkdf2 = utils.promisify(crypto.pbkdf2);
export const ENCRYPTION_KEY = process.env['NANGO_ENCRYPTION_KEY'] || '';
class EncryptionManager extends Encryption {
    keySalt = 'X89FHEGqR3yNK0+v7rPWxQ==';
    shouldEncrypt() {
        return Boolean(this?.key && this.key.length > 0);
    }
    encryptEnvironment(environment) {
        if (!this.shouldEncrypt()) {
            return environment;
        }
        const encryptedEnvironment = Object.assign({}, environment);
        const [encryptedClientSecret, iv, authTag] = this.encrypt(encryptedEnvironment.secret_key);
        encryptedEnvironment.secret_key = encryptedClientSecret;
        encryptedEnvironment.secret_key_iv = iv;
        encryptedEnvironment.secret_key_tag = authTag;
        if (encryptedEnvironment.pending_secret_key) {
            const [encryptedPendingClientSecret, pendingIv, pendingAuthTag] = this.encrypt(encryptedEnvironment.pending_secret_key);
            encryptedEnvironment.pending_secret_key = encryptedPendingClientSecret;
            encryptedEnvironment.pending_secret_key_iv = pendingIv;
            encryptedEnvironment.pending_secret_key_tag = pendingAuthTag;
        }
        return encryptedEnvironment;
    }
    decryptEnvironment(environment) {
        // Check if the individual row is encrypted.
        if (environment == null || environment.secret_key_iv == null || environment.secret_key_tag == null) {
            return environment;
        }
        const decryptedEnvironment = Object.assign({}, environment);
        decryptedEnvironment.secret_key = this.decrypt(environment.secret_key, environment.secret_key_iv, environment.secret_key_tag);
        if (decryptedEnvironment.pending_secret_key) {
            decryptedEnvironment.pending_secret_key = this.decrypt(environment.pending_secret_key, environment.pending_secret_key_iv, environment.pending_secret_key_tag);
        }
        return decryptedEnvironment;
    }
    encryptApiConnection(connection) {
        if (!this.shouldEncrypt()) {
            return connection;
        }
        const storedConnection = Object.assign({}, connection);
        const [encryptedClientSecret, iv, authTag] = this.encrypt(JSON.stringify(connection.credentials));
        const encryptedCreds = { encrypted_credentials: encryptedClientSecret };
        storedConnection.credentials = encryptedCreds;
        storedConnection.credentials_iv = iv;
        storedConnection.credentials_tag = authTag;
        return storedConnection;
    }
    encryptConnection(connection) {
        if (!this.shouldEncrypt()) {
            return connection;
        }
        const storedConnection = Object.assign({}, connection);
        const [encryptedClientSecret, iv, authTag] = this.encrypt(JSON.stringify(connection.credentials));
        const encryptedCreds = { encrypted_credentials: encryptedClientSecret };
        storedConnection.credentials = encryptedCreds;
        storedConnection.credentials_iv = iv;
        storedConnection.credentials_tag = authTag;
        return storedConnection;
    }
    decryptConnection(connection) {
        // Check if the individual row is encrypted.
        if (connection == null || connection.credentials_iv == null || connection.credentials_tag == null) {
            return connection;
        }
        const decryptedConnection = Object.assign({}, connection);
        decryptedConnection.credentials = JSON.parse(this.decrypt(connection.credentials['encrypted_credentials'], connection.credentials_iv, connection.credentials_tag));
        return decryptedConnection;
    }
    encryptEnvironmentVariables(environmentVariables) {
        if (!this.shouldEncrypt()) {
            return environmentVariables;
        }
        const encryptedEnvironmentVariables = Object.assign([], environmentVariables);
        for (const environmentVariable of encryptedEnvironmentVariables) {
            const [encryptedValue, iv, authTag] = this.encrypt(environmentVariable.value);
            environmentVariable.value = encryptedValue;
            environmentVariable.value_iv = iv;
            environmentVariable.value_tag = authTag;
        }
        return encryptedEnvironmentVariables;
    }
    decryptEnvironmentVariables(environmentVariables) {
        if (environmentVariables === null) {
            return environmentVariables;
        }
        const decryptedEnvironmentVariables = Object.assign([], environmentVariables);
        for (const environmentVariable of decryptedEnvironmentVariables) {
            if (environmentVariable.value_iv == null || environmentVariable.value_tag == null) {
                continue;
            }
            environmentVariable.value = this.decrypt(environmentVariable.value, environmentVariable.value_iv, environmentVariable.value_tag);
        }
        return decryptedEnvironmentVariables;
    }
    encryptProviderConfig(config) {
        if (!this.shouldEncrypt()) {
            return config;
        }
        const encryptedConfig = Object.assign({}, config);
        if (!config.oauth_client_secret) {
            return config;
        }
        const [encryptedClientSecret, iv, authTag] = this.encrypt(config.oauth_client_secret);
        encryptedConfig.oauth_client_secret = encryptedClientSecret;
        encryptedConfig.oauth_client_secret_iv = iv;
        encryptedConfig.oauth_client_secret_tag = authTag;
        if (config.custom) {
            const [encryptedValue, iv, authTag] = this.encrypt(JSON.stringify(config.custom));
            encryptedConfig.custom = { encryptedValue, iv: iv, authTag: authTag };
        }
        return encryptedConfig;
    }
    decryptProviderConfig(config) {
        // Check if the individual row is encrypted.
        if (config == null || config.oauth_client_secret_iv == null || config.oauth_client_secret_tag == null) {
            return config;
        }
        const decryptedConfig = Object.assign({}, config);
        decryptedConfig.oauth_client_secret = this.decrypt(config.oauth_client_secret, config.oauth_client_secret_iv, config.oauth_client_secret_tag);
        if (decryptedConfig.custom && config.custom) {
            decryptedConfig.custom = JSON.parse(this.decrypt(config.custom['encryptedValue'], config.custom['iv'], config.custom['authTag']));
        }
        return decryptedConfig;
    }
    encryptDataRecords(dataRecords) {
        if (!this.shouldEncrypt()) {
            return dataRecords;
        }
        const encryptedDataRecords = Object.assign([], dataRecords);
        for (const dataRecord of encryptedDataRecords) {
            const [encryptedValue, iv, authTag] = this.encrypt(JSON.stringify(dataRecord.json));
            dataRecord.json = { encryptedValue, iv, authTag };
        }
        return encryptedDataRecords;
    }
    decryptDataRecords(dataRecords, field = 'json') {
        if (dataRecords === null) {
            return dataRecords;
        }
        const decryptedDataRecords = [];
        for (const dataRecord of dataRecords) {
            const record = dataRecord[field];
            if (!record.encryptedValue) {
                decryptedDataRecords.push(dataRecord);
                continue;
            }
            const { encryptedValue, iv, authTag } = record;
            const decryptedString = this.decrypt(encryptedValue, iv, authTag);
            let updatedRecord = {
                ...JSON.parse(decryptedString)
            };
            if (record['_nango_metadata']) {
                updatedRecord['_nango_metadata'] = record['_nango_metadata'];
                decryptedDataRecords.push({ [field]: updatedRecord });
            }
            else {
                const { record: _record, ...rest } = dataRecord;
                updatedRecord = {
                    ...rest,
                    record: updatedRecord
                };
                decryptedDataRecords.push(updatedRecord);
            }
        }
        return decryptedDataRecords;
    }
    decryptDataRecord(dataRecord) {
        const record = dataRecord.record;
        if (!record['encryptedValue']) {
            return record;
        }
        const { encryptedValue, iv, authTag } = record;
        const decryptedString = this.decrypt(encryptedValue, iv, authTag);
        return {
            ...JSON.parse(decryptedString)
        };
    }
    async encryptAllDataRecords() {
        const chunkSize = 1000;
        const concurrencyLimit = 5;
        const encryptAndSave = async (tableName, offset) => {
            const dataRecords = await db.knex.select('*').from(tableName).limit(chunkSize).offset(offset);
            if (dataRecords.length === 0) {
                return false;
            }
            const updatePromises = dataRecords.map((dataRecord) => db.knex.transaction(async (trx) => {
                if (dataRecord.json['encryptedValue']) {
                    return;
                }
                const [encryptedValue, iv, authTag] = this.encrypt(JSON.stringify(dataRecord.json));
                dataRecord.json = { encryptedValue, iv, authTag };
                await db.knex.from(tableName).where('id', dataRecord.id).update(dataRecord).transacting(trx);
                await trx.commit();
            }));
            await Promise.all(updatePromises.slice(0, concurrencyLimit));
            return true;
        };
        let offset = 0;
        while (await encryptAndSave('_nango_sync_data_records', offset)) {
            offset += chunkSize;
        }
    }
    async saveDbConfig(dbConfig) {
        await db.knex.from(`_nango_db_config`).del();
        await db.knex.from(`_nango_db_config`).insert(dbConfig);
    }
    async hashEncryptionKey(key, salt) {
        const keyBuffer = await pbkdf2(key, salt, 310000, 32, 'sha256');
        return keyBuffer.toString(this.encoding);
    }
    async encryptDatabaseIfNeeded() {
        const dbConfig = await db.knex.first().from('_nango_db_config');
        const previousEncryptionKeyHash = dbConfig?.encryption_key_hash;
        const encryptionKeyHash = this.key ? await this.hashEncryptionKey(this.key, this.keySalt) : null;
        const isEncryptionKeyNew = dbConfig == null && this.key;
        const isEncryptionIncomplete = dbConfig != null && previousEncryptionKeyHash === encryptionKeyHash && !dbConfig.encryption_complete;
        if (isEncryptionKeyNew || isEncryptionIncomplete) {
            if (isEncryptionKeyNew) {
                logger.info('🔐 Encryption key has been set. Encrypting database...');
                await this.saveDbConfig({ encryption_key_hash: encryptionKeyHash, encryption_complete: false });
            }
            else if (isEncryptionIncomplete) {
                logger.info('🔐 Previously started database encryption is incomplete. Continuing encryption of database...');
            }
            await this.encryptDatabase();
            await this.saveDbConfig({ encryption_key_hash: encryptionKeyHash, encryption_complete: true });
            return;
        }
        const isEncryptionKeyChanged = dbConfig?.encryption_key_hash != null && previousEncryptionKeyHash !== encryptionKeyHash;
        if (isEncryptionKeyChanged) {
            throw new Error('You cannot edit or remove the encryption key once it has been set.');
        }
    }
    async encryptDatabase() {
        logger.info('🔐⚙️ Starting encryption of database...');
        const environments = await db.knex.select('*').from(`_nango_environments`);
        for (let environment of environments) {
            if (environment.secret_key_iv && environment.secret_key_tag) {
                continue;
            }
            environment = this.encryptEnvironment(environment);
            await db.knex.from(`_nango_environments`).where({ id: environment.id }).update(environment);
        }
        const connections = await db.knex.select('*').from(`_nango_connections`);
        for (const connection of connections) {
            if (connection.credentials_iv && connection.credentials_tag) {
                continue;
            }
            const storedConnection = this.encryptConnection(connection);
            await db.knex.from(`_nango_connections`).where({ id: storedConnection.id }).update(storedConnection);
        }
        const providerConfigs = await db.knex.select('*').from(`_nango_configs`);
        for (let providerConfig of providerConfigs) {
            if (providerConfig.oauth_client_secret_iv && providerConfig.oauth_client_secret_tag) {
                continue;
            }
            providerConfig = this.encryptProviderConfig(providerConfig);
            await db.knex.from(`_nango_configs`).where({ id: providerConfig.id }).update(providerConfig);
        }
        const environmentVariables = await db.knex.select('*').from(`_nango_environment_variables`);
        for (const environmentVariable of environmentVariables) {
            if (environmentVariable.value_iv && environmentVariable.value_tag) {
                continue;
            }
            const [encryptedValue, iv, authTag] = this.encrypt(environmentVariable.value);
            environmentVariable.value = encryptedValue;
            environmentVariable.value_iv = iv;
            environmentVariable.value_tag = authTag;
            await db.knex
                .from(`_nango_environment_variables`)
                .where({ id: environmentVariable.id })
                .update(environmentVariable);
        }
        await this.encryptAllDataRecords();
        logger.info('🔐✅ Encryption of database complete!');
    }
}
export default new EncryptionManager(ENCRYPTION_KEY);
//# sourceMappingURL=encryption.manager.js.map