UNPKG

@microsoft/dev-tunnels-ssh

Version:
208 lines 8.88 kB
"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=webEncryption.js.map