"use strict";
/*
 * @adonisjs/encryption
 *
 * (c) Harminder Virk <virk@adonisjs.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Encryption = void 0;
/// <reference path="../../adonis-typings/encryption.ts" />
const utils_1 = require("@poppinss/utils");
const crypto_1 = require("crypto");
const helpers_1 = require("@poppinss/utils/build/helpers");
const Hmac_1 = require("../Hmac");
const MessageVerifier_1 = require("../MessageVerifier");
const AppKeyException_1 = require("../Exceptions/AppKeyException");
/**
 * The encryption class allows encrypting and decrypting values using `aes-256-cbc` or `aes-128-cbc`
 * algorithms. The encrypted value uses a unique iv for every encryption and this ensures semantic
 * security (read more https://en.wikipedia.org/wiki/Semantic_security).
 */
class Encryption {
    constructor(options) {
        this.options = options;
        /**
         * Use `dot` as a separator for joining encrypted value, iv and the
         * hmac hash. The idea is borrowed from JWT's in which each part
         * of the payload is concatenated with a dot.
         */
        this.separator = '.';
        /**
         * Reference to base64 object for base64 encoding/decoding values
         */
        this.base64 = helpers_1.base64;
        /**
         * The algorithm in use
         */
        this.algorithm = this.options.algorithm || 'aes-256-cbc';
        this.validateSecret();
        this.cryptoKey = (0, crypto_1.createHash)('sha256').update(this.options.secret).digest();
        this.verifier = new MessageVerifier_1.MessageVerifier(this.options.secret);
    }
    /**
     * Validates the app secret
     */
    validateSecret() {
        if (typeof this.options.secret !== 'string') {
            throw AppKeyException_1.AppKeyException.missingAppKey();
        }
        if (this.options.secret.length < 16) {
            throw AppKeyException_1.AppKeyException.insecureAppKey();
        }
    }
    /**
     * Encrypt value with optional expiration and purpose
     */
    encrypt(value, expiresAt, purpose) {
        /**
         * Using a random string as the iv for generating unpredictable values
         */
        const iv = helpers_1.string.generateRandom(16);
        /**
         * Creating chiper
         */
        const cipher = (0, crypto_1.createCipheriv)(this.algorithm, this.cryptoKey, iv);
        /**
         * Encoding value to a string so that we can set it on the cipher
         */
        const encodedValue = new helpers_1.MessageBuilder().build(value, expiresAt, purpose);
        /**
         * Set final to the cipher instance and encrypt it
         */
        const encrypted = Buffer.concat([cipher.update(encodedValue, 'utf-8'), cipher.final()]);
        /**
         * Concatenate `encrypted value` and `iv` by urlEncoding them. The concatenation is required
         * to generate the HMAC, so that HMAC checks for integrity of both the `encrypted value`
         * and the `iv`.
         */
        const result = `${this.base64.urlEncode(encrypted)}${this.separator}${this.base64.urlEncode(iv)}`;
        /**
         * Returns the result + hmac
         */
        return `${result}${this.separator}${new Hmac_1.Hmac(this.cryptoKey).generate(result)}`;
    }
    /**
     * Decrypt value and verify it against a purpose
     */
    decrypt(value, purpose) {
        if (typeof value !== 'string') {
            throw new utils_1.Exception('"Encryption.decrypt" expects a string value', 500, 'E_RUNTIME_EXCEPTION');
        }
        /**
         * Make sure the encrypted value is in correct format. ie
         * [encrypted value]--[iv]--[hash]
         */
        const [encryptedEncoded, ivEncoded, hash] = value.split(this.separator);
        if (!encryptedEncoded || !ivEncoded || !hash) {
            return null;
        }
        /**
         * Make sure we are able to urlDecode the encrypted value
         */
        const encrypted = this.base64.urlDecode(encryptedEncoded, 'base64');
        if (!encrypted) {
            return null;
        }
        /**
         * Make sure we are able to urlDecode the iv
         */
        const iv = this.base64.urlDecode(ivEncoded);
        if (!iv) {
            return null;
        }
        /**
         * Make sure the hash is correct, it means the first 2 parts of the
         * string are not tampered.
         */
        const isValidHmac = new Hmac_1.Hmac(this.cryptoKey).compare(`${encryptedEncoded}${this.separator}${ivEncoded}`, hash);
        if (!isValidHmac) {
            return null;
        }
        /**
         * The Decipher can raise exceptions with malformed input, so we wrap it
         * to avoid leaking sensitive information
         */
        try {
            const decipher = (0, crypto_1.createDecipheriv)(this.algorithm, this.cryptoKey, iv);
            const decrypted = decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8');
            return new helpers_1.MessageBuilder().verify(decrypted, purpose);
        }
        catch (error) {
            return null;
        }
    }
    /**
     * Returns a new instance of encryption with custom secret key
     */
    child(options) {
        return new Encryption(options);
    }
}
exports.Encryption = Encryption;
