"use strict";
//
//  Copyright (c) Microsoft Corporation. All rights reserved.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebEncryption = void 0;
const buffer_1 = require("buffer");
const encryptionAlgorithm_1 = require("../encryptionAlgorithm");
class WebEncryption extends encryptionAlgorithm_1.EncryptionAlgorithm {
    constructor(name, algorithmName, cipherMode, keySizeInBits) {
        super(name);
        this.algorithmName = algorithmName;
        this.cipherMode = cipherMode;
        this.keySizeInBits = keySizeInBits;
        this.blockSizeInBits = WebEncryption.getBlockSize(algorithmName);
    }
    get keyLength() {
        return this.keySizeInBits / 8;
    }
    get blockLength() {
        return this.blockSizeInBits / 8;
    }
    async createCipher(isEncryption, key, iv) {
        const cipher = new WebCipher(isEncryption, this.algorithmName, this.cipherMode, this.keySizeInBits, this.blockSizeInBits);
        await cipher.init(key, iv);
        return cipher;
    }
    static getBlockSize(algorithmName) {
        if (algorithmName === 'AES') {
            return 128;
        }
        else {
            throw new Error(`Unsupported encryption algorithm: ${algorithmName}`);
        }
    }
}
exports.WebEncryption = WebEncryption;
class WebCipher {
    get blockLength() {
        return this.blockSizeInBits / 8;
    }
    constructor(isEncryption, algorithmName, cipherMode, keySizeInBits, blockSizeInBits) {
        this.isEncryption = isEncryption;
        this.algorithmName = algorithmName;
        this.cipherMode = cipherMode;
        this.keySizeInBits = keySizeInBits;
        this.blockSizeInBits = blockSizeInBits;
        this.tag = null;
        if (this.algorithmName === 'AES' && this.cipherMode === 'CTR') {
            this.transform = this.aesCtr.bind(this, isEncryption);
        }
        else if (this.algorithmName === 'AES' && this.cipherMode === 'CBC') {
            this.transform = this.aesCbc.bind(this, isEncryption);
        }
        else if (this.algorithmName === 'AES' && this.cipherMode === 'GCM') {
            this.transform = this.aesGcm.bind(this, isEncryption);
            this.associatedData = buffer_1.Buffer.alloc(4);
            this.decryptBuffer = buffer_1.Buffer.alloc(this.blockLength * 4);
        }
        else {
            throw new Error(`Unsupported encryption algorithm: ${this.algorithmName}-${this.cipherMode}`);
        }
    }
    async init(key, iv) {
        try {
            const name = `${this.algorithmName}-${this.cipherMode}`;
            this.key = await crypto.subtle.importKey('raw', key, { name, length: this.keySizeInBits }, false, this.isEncryption ? ['encrypt'] : ['decrypt']);
        }
        catch (e) {
            throw new Error('Failed to initialize AES: ' + e);
        }
        if (this.cipherMode === 'GCM') {
            this.iv = buffer_1.Buffer.from(iv.slice(0, 12));
        }
        else {
            this.iv = buffer_1.Buffer.from(iv);
        }
    }
    async aesCtr(isEncryption, data) {
        if (data.length % this.blockLength !== 0) {
            const message = 'Encrypt/decrypt input has invalid length ' +
                `${data.length}, not a multiple of block size ${this.blockLength}.`;
            throw new Error(message);
        }
        let result;
        if (isEncryption) {
            result = buffer_1.Buffer.from(await crypto.subtle.encrypt({ name: 'AES-CTR', counter: this.iv, length: this.blockSizeInBits }, this.key, data));
        }
        else {
            result = buffer_1.Buffer.from(await crypto.subtle.decrypt({ name: 'AES-CTR', counter: this.iv, length: this.blockSizeInBits }, this.key, data));
        }
        if (result.length !== data.length) {
            const message = 'Result from encrypt/decrypt has invalid length ' +
                `${result.length}, expected ${data.length}.`;
            throw new Error(message);
        }
        // A single call to encrypt() or decrypt() internally increments the counter.
        // This code ensures those increments get preserved across multiple calls.
        const incrementCount = data.length / this.blockLength;
        for (let i = 0; i < incrementCount; i++) {
            // Increment the counter that is combined with the IV as a big-endian integer.
            // First increment the last byte, and if it reaches 0 then increment the
            // next-to-last byte, and so on.
            for (let k = this.iv.length - 1; k >= 0; k--) {
                this.iv[k] = this.iv[k] + 1;
                if (this.iv[k])
                    break;
            }
        }
        return result;
    }
    async aesCbc(isEncryption, data) {
        // TODO: Fix padding. Web crypto's AES-CBC uses padding by default,
        // which isn't compatible with SSH.
        if (isEncryption) {
            let result = buffer_1.Buffer.from(await crypto.subtle.encrypt({ name: 'AES-CBC', iv: this.iv }, this.key, data));
            result = result.slice(0, data.length);
            return result;
        }
        else {
            return buffer_1.Buffer.from(await crypto.subtle.decrypt({ name: 'AES-CBC', iv: this.iv }, this.key, data));
        }
    }
    async aesGcm(isEncryption, data) {
        if (data.length % this.blockLength !== 0) {
            const message = 'Encrypt/decrypt input has invalid length ' +
                `${data.length}, not a multiple of block size ${this.blockLength}.`;
            throw new Error(message);
        }
        // Associated data is the 32-bit packet length.
        const packetLength = data.length;
        this.associatedData[0] = packetLength >>> 24;
        this.associatedData[1] = packetLength >>> 16;
        this.associatedData[2] = packetLength >>> 8;
        this.associatedData[3] = packetLength;
        let result;
        if (isEncryption) {
            result = buffer_1.Buffer.from(await crypto.subtle.encrypt({
                name: 'AES-GCM',
                iv: this.iv,
                additionalData: this.associatedData,
                tagLength: this.digestLength * 8, // tagLength is in bits, not bytes
            }, this.key, data));
            this.tag = result.slice(result.length - this.digestLength);
            result = result.slice(0, result.length - this.digestLength);
        }
        else {
            if (!this.tag) {
                throw new Error('AES-GCM tag was not set before decrypting.');
            }
            // The AES-GCM decrypt API expects the ciphertext and tag to be in a contiguous buffer.
            // Re-use a temporary buffer for that purpose, expanding it as needed.
            const inputLength = data.length + this.digestLength;
            if (this.decryptBuffer.length < inputLength) {
                let newLength = this.decryptBuffer.length * 2;
                while (newLength < inputLength)
                    newLength *= 2;
                this.decryptBuffer = buffer_1.Buffer.alloc(newLength);
            }
            const input = this.decryptBuffer.slice(0, inputLength);
            data.copy(input, 0);
            this.tag.copy(input, data.length);
            result = buffer_1.Buffer.from(await crypto.subtle.decrypt({
                name: 'AES-GCM',
                iv: this.iv,
                additionalData: this.associatedData,
                tagLength: this.digestLength * 8, // tagLength is in bits, not bytes
            }, this.key, input));
        }
        if (result.length !== data.length) {
            const message = 'Result from encrypt/decrypt has invalid length ' +
                `${result.length}, expected ${data.length}.`;
            throw new Error(message);
        }
        // Increment the counter (last 8 bytes of the iv/nonce) as a big-endian integer.
        // First increment the last byte, and if it reaches 0 then increment the
        // next-to-last byte, and so on.
        let k = 12;
        while (--k >= 4) {
            this.iv[k]++;
            if (this.iv[k] !== 0) {
                break;
            }
        }
        return result;
    }
    get digestLength() {
        return 16;
    }
    get authenticatedEncryption() {
        return this.cipherMode === 'GCM';
    }
    async sign(data) {
        if (!this.tag) {
            throw new Error('AES-GCM tag was not obtained by encrypting.');
        }
        return this.tag;
    }
    async verify(data, signature) {
        if (signature.length !== this.digestLength) {
            throw new Error('Incorrect AES-GCM tag length.');
        }
        this.tag = signature;
        return true;
    }
    dispose() { }
}
//# sourceMappingURL=https://github.com/VSCodium/sourcemaps/releases/download/stable-824c4c46a288b839f13b24022655329c2aeb9f81/node_modules-@microsoft-dev-tunnels-ssh-algorithms-web-webEncryption.js.map