'use strict';

const crypto = require('crypto');
const assert = require('assert');

function parseEncryptedData(encryptedData, defaultCipher) {
    encryptedData = (encryptedData || '').toString();
    if (!encryptedData) {
        return false;
    }

    if (encryptedData.charAt(0) !== '$') {
        // cleartext
        return {
            format: 'cleartext',
            data: encryptedData
        };
    }

    if (encryptedData.lastIndexOf('$') === 0) {
        // legacy
        return {
            format: 'legacy',
            cipher: defaultCipher || 'aes192',
            data: Buffer.from(encryptedData.substr(1), 'hex')
        };
    }

    let [, format, cipher, authTag, iv, salt, encryptedText] = encryptedData.split('$');
    if (!format || !cipher || !authTag || !iv || !encryptedText) {
        return false;
    }

    authTag = Buffer.from(authTag, 'hex');
    iv = Buffer.from(iv, 'hex');
    salt = Buffer.from(salt, 'hex');
    encryptedText = Buffer.from(encryptedText, 'hex');

    return {
        format,
        cipher,
        authTag,
        iv,
        salt,
        data: encryptedText
    };
}

function getKeyFromPassword(password, salt, keyLen) {
    return new Promise((resolve, reject) => {
        crypto.scrypt(password, salt, keyLen, (err, result) => {
            if (err) {
                return reject(err);
            }
            if (!result) {
                return reject(new Error('Failed to hash key'));
            }
            return resolve(result);
        });
    });
}

async function decrypt(encryptedData, secret, defaultCipher) {
    const decryptData = parseEncryptedData(encryptedData, defaultCipher);
    if (!decryptData) {
        return encryptedData;
    }

    if (!secret && decryptData.format !== 'cleartext') {
        // data is encrypted but we do not have a secret
        let err = new Error('Failed to decrypt data. No secret provided.');
        err.responseCode = 500;
        err.code = 'InternalConfigError';
        throw err;
    }

    switch (decryptData.format) {
        case 'cleartext':
            return encryptedData;

        case 'legacy':
            try {
                let decipher = crypto.createDecipher(decryptData.cipher, secret);
                return Buffer.concat([decipher.update(decryptData.data), decipher.final()]).toString('utf-8');
            } catch (E) {
                let err = new Error('Failed to decrypt data. ' + E.message);
                err.responseCode = 500;
                err.code = 'InternalConfigError';
                throw err;
            }

        case 'wd01':
            try {
                assert.strictEqual(decryptData.authTag.length, 16, 'Invalid auth tag length');
                assert.strictEqual(decryptData.iv.length, 12, 'Invalid iv length');
                assert.strictEqual(decryptData.salt.length, 16, 'Invalid salt length');

                // convert password to 32B key
                const key = await getKeyFromPassword(secret, decryptData.salt, 32);

                const decipher = crypto.createDecipheriv(decryptData.cipher, key, decryptData.iv, { authTagLength: decryptData.authTag.length });
                decipher.setAuthTag(decryptData.authTag);
                return Buffer.concat([decipher.update(decryptData.data), decipher.final()]).toString('utf-8');
            } catch (E) {
                let err = new Error('Failed to decrypt data. ' + E.message);
                err.responseCode = 500;
                err.code = 'InternalConfigError';
                throw err;
            }

        default: {
            let err = new Error('Unknown encryption format: ' + decryptData.format);
            err.responseCode = 500;
            err.code = 'InternalConfigError';
            throw err;
        }
    }
}

async function encrypt(cleartext, secret) {
    if (!secret) {
        return cleartext;
    }

    const iv = crypto.randomBytes(12);
    const salt = crypto.randomBytes(16);

    const key = await getKeyFromPassword(secret, salt, 32);

    const format = 'wd01';
    const algo = 'aes-256-gcm';

    const cipher = crypto.createCipheriv(algo, key, iv, { authTagLength: 16 });
    const encryptedText = Buffer.concat([cipher.update(cleartext), cipher.final()]);

    const authTag = cipher.getAuthTag();

    return ['', format, algo].concat([authTag, iv, salt, encryptedText].map(buf => buf.toString('hex'))).join('$');
}

module.exports = { encrypt, decrypt };
