@microsoft/dev-tunnels-ssh
Version:
SSH library for Dev Tunnels
208 lines • 8.88 kB
JavaScript
"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