1603 lines
46 KiB
JavaScript
1603 lines
46 KiB
JavaScript
// TODO:
|
|
// * make max packet size configurable
|
|
// * if decompression is enabled, use `._packet` in decipher instances as
|
|
// input to (sync) zlib inflater with appropriate offset and length to
|
|
// avoid an additional copy of payload data before inflation
|
|
// * factor decompression status into packet length checks
|
|
'use strict';
|
|
|
|
const {
|
|
createCipheriv, createDecipheriv, createHmac, randomFillSync, timingSafeEqual
|
|
} = require('crypto');
|
|
|
|
const { readUInt32BE, writeUInt32BE } = require('./utils.js');
|
|
|
|
const FastBuffer = Buffer[Symbol.species];
|
|
const MAX_SEQNO = 2 ** 32 - 1;
|
|
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
const BUF_INT = Buffer.alloc(4);
|
|
const DISCARD_CACHE = new Map();
|
|
const MAX_PACKET_SIZE = 35000;
|
|
|
|
let binding;
|
|
let AESGCMCipher;
|
|
let ChaChaPolyCipher;
|
|
let GenericCipher;
|
|
let AESGCMDecipher;
|
|
let ChaChaPolyDecipher;
|
|
let GenericDecipher;
|
|
try {
|
|
binding = require('./crypto/build/Release/sshcrypto.node');
|
|
({ AESGCMCipher, ChaChaPolyCipher, GenericCipher,
|
|
AESGCMDecipher, ChaChaPolyDecipher, GenericDecipher } = binding);
|
|
} catch {}
|
|
|
|
const CIPHER_STREAM = 1 << 0;
|
|
const CIPHER_INFO = (() => {
|
|
function info(sslName, blockLen, keyLen, ivLen, authLen, discardLen, flags) {
|
|
return {
|
|
sslName,
|
|
blockLen,
|
|
keyLen,
|
|
ivLen: (ivLen !== 0 || (flags & CIPHER_STREAM)
|
|
? ivLen
|
|
: blockLen),
|
|
authLen,
|
|
discardLen,
|
|
stream: !!(flags & CIPHER_STREAM),
|
|
};
|
|
}
|
|
|
|
return {
|
|
'chacha20-poly1305@openssh.com':
|
|
info('chacha20', 8, 64, 0, 16, 0, CIPHER_STREAM),
|
|
|
|
'aes128-gcm': info('aes-128-gcm', 16, 16, 12, 16, 0, CIPHER_STREAM),
|
|
'aes256-gcm': info('aes-256-gcm', 16, 32, 12, 16, 0, CIPHER_STREAM),
|
|
'aes128-gcm@openssh.com':
|
|
info('aes-128-gcm', 16, 16, 12, 16, 0, CIPHER_STREAM),
|
|
'aes256-gcm@openssh.com':
|
|
info('aes-256-gcm', 16, 32, 12, 16, 0, CIPHER_STREAM),
|
|
|
|
'aes128-cbc': info('aes-128-cbc', 16, 16, 0, 0, 0, 0),
|
|
'aes192-cbc': info('aes-192-cbc', 16, 24, 0, 0, 0, 0),
|
|
'aes256-cbc': info('aes-256-cbc', 16, 32, 0, 0, 0, 0),
|
|
'rijndael-cbc@lysator.liu.se': info('aes-256-cbc', 16, 32, 0, 0, 0, 0),
|
|
'3des-cbc': info('des-ede3-cbc', 8, 24, 0, 0, 0, 0),
|
|
'blowfish-cbc': info('bf-cbc', 8, 16, 0, 0, 0, 0),
|
|
'idea-cbc': info('idea-cbc', 8, 16, 0, 0, 0, 0),
|
|
'cast128-cbc': info('cast-cbc', 8, 16, 0, 0, 0, 0),
|
|
|
|
'aes128-ctr': info('aes-128-ctr', 16, 16, 16, 0, 0, CIPHER_STREAM),
|
|
'aes192-ctr': info('aes-192-ctr', 16, 24, 16, 0, 0, CIPHER_STREAM),
|
|
'aes256-ctr': info('aes-256-ctr', 16, 32, 16, 0, 0, CIPHER_STREAM),
|
|
'3des-ctr': info('des-ede3', 8, 24, 8, 0, 0, CIPHER_STREAM),
|
|
'blowfish-ctr': info('bf-ecb', 8, 16, 8, 0, 0, CIPHER_STREAM),
|
|
'cast128-ctr': info('cast5-ecb', 8, 16, 8, 0, 0, CIPHER_STREAM),
|
|
|
|
/* The "arcfour128" algorithm is the RC4 cipher, as described in
|
|
[SCHNEIER], using a 128-bit key. The first 1536 bytes of keystream
|
|
generated by the cipher MUST be discarded, and the first byte of the
|
|
first encrypted packet MUST be encrypted using the 1537th byte of
|
|
keystream.
|
|
|
|
-- http://tools.ietf.org/html/rfc4345#section-4 */
|
|
'arcfour': info('rc4', 8, 16, 0, 0, 1536, CIPHER_STREAM),
|
|
'arcfour128': info('rc4', 8, 16, 0, 0, 1536, CIPHER_STREAM),
|
|
'arcfour256': info('rc4', 8, 32, 0, 0, 1536, CIPHER_STREAM),
|
|
'arcfour512': info('rc4', 8, 64, 0, 0, 1536, CIPHER_STREAM),
|
|
};
|
|
})();
|
|
|
|
const MAC_INFO = (() => {
|
|
function info(sslName, len, actualLen, isETM) {
|
|
return {
|
|
sslName,
|
|
len,
|
|
actualLen,
|
|
isETM,
|
|
};
|
|
}
|
|
|
|
return {
|
|
'hmac-md5': info('md5', 16, 16, false),
|
|
'hmac-md5-96': info('md5', 16, 12, false),
|
|
'hmac-ripemd160': info('ripemd160', 20, 20, false),
|
|
'hmac-sha1': info('sha1', 20, 20, false),
|
|
'hmac-sha1-etm@openssh.com': info('sha1', 20, 20, true),
|
|
'hmac-sha1-96': info('sha1', 20, 12, false),
|
|
'hmac-sha2-256': info('sha256', 32, 32, false),
|
|
'hmac-sha2-256-etm@openssh.com': info('sha256', 32, 32, true),
|
|
'hmac-sha2-256-96': info('sha256', 32, 12, false),
|
|
'hmac-sha2-512': info('sha512', 64, 64, false),
|
|
'hmac-sha2-512-etm@openssh.com': info('sha512', 64, 64, true),
|
|
'hmac-sha2-512-96': info('sha512', 64, 12, false),
|
|
};
|
|
})();
|
|
|
|
|
|
// Should only_be used during the initial handshake
|
|
class NullCipher {
|
|
constructor(seqno, onWrite) {
|
|
this.outSeqno = seqno;
|
|
this._onWrite = onWrite;
|
|
this._dead = false;
|
|
}
|
|
free() {
|
|
this._dead = true;
|
|
}
|
|
allocPacket(payloadLen) {
|
|
let pktLen = 4 + 1 + payloadLen;
|
|
let padLen = 8 - (pktLen & (8 - 1));
|
|
if (padLen < 4)
|
|
padLen += 8;
|
|
pktLen += padLen;
|
|
|
|
const packet = Buffer.allocUnsafe(pktLen);
|
|
|
|
writeUInt32BE(packet, pktLen - 4, 0);
|
|
packet[4] = padLen;
|
|
|
|
randomFillSync(packet, 5 + payloadLen, padLen);
|
|
|
|
return packet;
|
|
}
|
|
encrypt(packet) {
|
|
// `packet` === unencrypted packet
|
|
|
|
if (this._dead)
|
|
return;
|
|
|
|
this._onWrite(packet);
|
|
|
|
this.outSeqno = (this.outSeqno + 1) >>> 0;
|
|
}
|
|
}
|
|
|
|
|
|
const POLY1305_ZEROS = Buffer.alloc(32);
|
|
const POLY1305_OUT_COMPUTE = Buffer.alloc(16);
|
|
let POLY1305_WASM_MODULE;
|
|
let POLY1305_RESULT_MALLOC;
|
|
let poly1305_auth;
|
|
class ChaChaPolyCipherNative {
|
|
constructor(config) {
|
|
const enc = config.outbound;
|
|
this.outSeqno = enc.seqno;
|
|
this._onWrite = enc.onWrite;
|
|
this._encKeyMain = enc.cipherKey.slice(0, 32);
|
|
this._encKeyPktLen = enc.cipherKey.slice(32);
|
|
this._dead = false;
|
|
}
|
|
free() {
|
|
this._dead = true;
|
|
}
|
|
allocPacket(payloadLen) {
|
|
let pktLen = 4 + 1 + payloadLen;
|
|
let padLen = 8 - ((pktLen - 4) & (8 - 1));
|
|
if (padLen < 4)
|
|
padLen += 8;
|
|
pktLen += padLen;
|
|
|
|
const packet = Buffer.allocUnsafe(pktLen);
|
|
|
|
writeUInt32BE(packet, pktLen - 4, 0);
|
|
packet[4] = padLen;
|
|
|
|
randomFillSync(packet, 5 + payloadLen, padLen);
|
|
|
|
return packet;
|
|
}
|
|
encrypt(packet) {
|
|
// `packet` === unencrypted packet
|
|
|
|
if (this._dead)
|
|
return;
|
|
|
|
// Generate Poly1305 key
|
|
POLY1305_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian)
|
|
writeUInt32BE(POLY1305_OUT_COMPUTE, this.outSeqno, 12);
|
|
const polyKey =
|
|
createCipheriv('chacha20', this._encKeyMain, POLY1305_OUT_COMPUTE)
|
|
.update(POLY1305_ZEROS);
|
|
|
|
// Encrypt packet length
|
|
const pktLenEnc =
|
|
createCipheriv('chacha20', this._encKeyPktLen, POLY1305_OUT_COMPUTE)
|
|
.update(packet.slice(0, 4));
|
|
this._onWrite(pktLenEnc);
|
|
|
|
// Encrypt rest of packet
|
|
POLY1305_OUT_COMPUTE[0] = 1; // Set counter to 1 (little endian)
|
|
const payloadEnc =
|
|
createCipheriv('chacha20', this._encKeyMain, POLY1305_OUT_COMPUTE)
|
|
.update(packet.slice(4));
|
|
this._onWrite(payloadEnc);
|
|
|
|
// Calculate Poly1305 MAC
|
|
poly1305_auth(POLY1305_RESULT_MALLOC,
|
|
pktLenEnc,
|
|
pktLenEnc.length,
|
|
payloadEnc,
|
|
payloadEnc.length,
|
|
polyKey);
|
|
const mac = Buffer.allocUnsafe(16);
|
|
mac.set(
|
|
new Uint8Array(POLY1305_WASM_MODULE.HEAPU8.buffer,
|
|
POLY1305_RESULT_MALLOC,
|
|
16),
|
|
0
|
|
);
|
|
this._onWrite(mac);
|
|
|
|
this.outSeqno = (this.outSeqno + 1) >>> 0;
|
|
}
|
|
}
|
|
|
|
class ChaChaPolyCipherBinding {
|
|
constructor(config) {
|
|
const enc = config.outbound;
|
|
this.outSeqno = enc.seqno;
|
|
this._onWrite = enc.onWrite;
|
|
this._instance = new ChaChaPolyCipher(enc.cipherKey);
|
|
this._dead = false;
|
|
}
|
|
free() {
|
|
this._dead = true;
|
|
this._instance.free();
|
|
}
|
|
allocPacket(payloadLen) {
|
|
let pktLen = 4 + 1 + payloadLen;
|
|
let padLen = 8 - ((pktLen - 4) & (8 - 1));
|
|
if (padLen < 4)
|
|
padLen += 8;
|
|
pktLen += padLen;
|
|
|
|
const packet = Buffer.allocUnsafe(pktLen + 16/* MAC */);
|
|
|
|
writeUInt32BE(packet, pktLen - 4, 0);
|
|
packet[4] = padLen;
|
|
|
|
randomFillSync(packet, 5 + payloadLen, padLen);
|
|
|
|
return packet;
|
|
}
|
|
encrypt(packet) {
|
|
// `packet` === unencrypted packet
|
|
|
|
if (this._dead)
|
|
return;
|
|
|
|
// Encrypts in-place
|
|
this._instance.encrypt(packet, this.outSeqno);
|
|
|
|
this._onWrite(packet);
|
|
|
|
this.outSeqno = (this.outSeqno + 1) >>> 0;
|
|
}
|
|
}
|
|
|
|
|
|
class AESGCMCipherNative {
|
|
constructor(config) {
|
|
const enc = config.outbound;
|
|
this.outSeqno = enc.seqno;
|
|
this._onWrite = enc.onWrite;
|
|
this._encSSLName = enc.cipherInfo.sslName;
|
|
this._encKey = enc.cipherKey;
|
|
this._encIV = enc.cipherIV;
|
|
this._dead = false;
|
|
}
|
|
free() {
|
|
this._dead = true;
|
|
}
|
|
allocPacket(payloadLen) {
|
|
let pktLen = 4 + 1 + payloadLen;
|
|
let padLen = 16 - ((pktLen - 4) & (16 - 1));
|
|
if (padLen < 4)
|
|
padLen += 16;
|
|
pktLen += padLen;
|
|
|
|
const packet = Buffer.allocUnsafe(pktLen);
|
|
|
|
writeUInt32BE(packet, pktLen - 4, 0);
|
|
packet[4] = padLen;
|
|
|
|
randomFillSync(packet, 5 + payloadLen, padLen);
|
|
|
|
return packet;
|
|
}
|
|
encrypt(packet) {
|
|
// `packet` === unencrypted packet
|
|
|
|
if (this._dead)
|
|
return;
|
|
|
|
const cipher = createCipheriv(this._encSSLName, this._encKey, this._encIV);
|
|
cipher.setAutoPadding(false);
|
|
|
|
const lenData = packet.slice(0, 4);
|
|
cipher.setAAD(lenData);
|
|
this._onWrite(lenData);
|
|
|
|
// Encrypt pad length, payload, and padding
|
|
const encrypted = cipher.update(packet.slice(4));
|
|
this._onWrite(encrypted);
|
|
const final = cipher.final();
|
|
// XXX: final.length === 0 always?
|
|
if (final.length)
|
|
this._onWrite(final);
|
|
|
|
// Generate MAC
|
|
const tag = cipher.getAuthTag();
|
|
this._onWrite(tag);
|
|
|
|
// Increment counter in IV by 1 for next packet
|
|
ivIncrement(this._encIV);
|
|
|
|
this.outSeqno = (this.outSeqno + 1) >>> 0;
|
|
}
|
|
}
|
|
|
|
class AESGCMCipherBinding {
|
|
constructor(config) {
|
|
const enc = config.outbound;
|
|
this.outSeqno = enc.seqno;
|
|
this._onWrite = enc.onWrite;
|
|
this._instance = new AESGCMCipher(enc.cipherInfo.sslName,
|
|
enc.cipherKey,
|
|
enc.cipherIV);
|
|
this._dead = false;
|
|
}
|
|
free() {
|
|
this._dead = true;
|
|
this._instance.free();
|
|
}
|
|
allocPacket(payloadLen) {
|
|
let pktLen = 4 + 1 + payloadLen;
|
|
let padLen = 16 - ((pktLen - 4) & (16 - 1));
|
|
if (padLen < 4)
|
|
padLen += 16;
|
|
pktLen += padLen;
|
|
|
|
const packet = Buffer.allocUnsafe(pktLen + 16/* authTag */);
|
|
|
|
writeUInt32BE(packet, pktLen - 4, 0);
|
|
packet[4] = padLen;
|
|
|
|
randomFillSync(packet, 5 + payloadLen, padLen);
|
|
|
|
return packet;
|
|
}
|
|
encrypt(packet) {
|
|
// `packet` === unencrypted packet
|
|
|
|
if (this._dead)
|
|
return;
|
|
|
|
// Encrypts in-place
|
|
this._instance.encrypt(packet);
|
|
|
|
this._onWrite(packet);
|
|
|
|
this.outSeqno = (this.outSeqno + 1) >>> 0;
|
|
}
|
|
}
|
|
|
|
|
|
class GenericCipherNative {
|
|
constructor(config) {
|
|
const enc = config.outbound;
|
|
this.outSeqno = enc.seqno;
|
|
this._onWrite = enc.onWrite;
|
|
this._encBlockLen = enc.cipherInfo.blockLen;
|
|
this._cipherInstance = createCipheriv(enc.cipherInfo.sslName,
|
|
enc.cipherKey,
|
|
enc.cipherIV);
|
|
this._macSSLName = enc.macInfo.sslName;
|
|
this._macKey = enc.macKey;
|
|
this._macActualLen = enc.macInfo.actualLen;
|
|
this._macETM = enc.macInfo.isETM;
|
|
this._aadLen = (this._macETM ? 4 : 0);
|
|
this._dead = false;
|
|
|
|
const discardLen = enc.cipherInfo.discardLen;
|
|
if (discardLen) {
|
|
let discard = DISCARD_CACHE.get(discardLen);
|
|
if (discard === undefined) {
|
|
discard = Buffer.alloc(discardLen);
|
|
DISCARD_CACHE.set(discardLen, discard);
|
|
}
|
|
this._cipherInstance.update(discard);
|
|
}
|
|
}
|
|
free() {
|
|
this._dead = true;
|
|
}
|
|
allocPacket(payloadLen) {
|
|
const blockLen = this._encBlockLen;
|
|
|
|
let pktLen = 4 + 1 + payloadLen;
|
|
let padLen = blockLen - ((pktLen - this._aadLen) & (blockLen - 1));
|
|
if (padLen < 4)
|
|
padLen += blockLen;
|
|
pktLen += padLen;
|
|
|
|
const packet = Buffer.allocUnsafe(pktLen);
|
|
|
|
writeUInt32BE(packet, pktLen - 4, 0);
|
|
packet[4] = padLen;
|
|
|
|
randomFillSync(packet, 5 + payloadLen, padLen);
|
|
|
|
return packet;
|
|
}
|
|
encrypt(packet) {
|
|
// `packet` === unencrypted packet
|
|
|
|
if (this._dead)
|
|
return;
|
|
|
|
let mac;
|
|
if (this._macETM) {
|
|
// Encrypt pad length, payload, and padding
|
|
const lenBytes = new Uint8Array(packet.buffer, packet.byteOffset, 4);
|
|
const encrypted = this._cipherInstance.update(
|
|
new Uint8Array(packet.buffer,
|
|
packet.byteOffset + 4,
|
|
packet.length - 4)
|
|
);
|
|
|
|
this._onWrite(lenBytes);
|
|
this._onWrite(encrypted);
|
|
|
|
// TODO: look into storing seqno as 4-byte buffer and incrementing like we
|
|
// do for AES-GCM IVs to avoid having to (re)write all 4 bytes every time
|
|
mac = createHmac(this._macSSLName, this._macKey);
|
|
writeUInt32BE(BUF_INT, this.outSeqno, 0);
|
|
mac.update(BUF_INT);
|
|
mac.update(lenBytes);
|
|
mac.update(encrypted);
|
|
} else {
|
|
// Encrypt length field, pad length, payload, and padding
|
|
const encrypted = this._cipherInstance.update(packet);
|
|
this._onWrite(encrypted);
|
|
|
|
// TODO: look into storing seqno as 4-byte buffer and incrementing like we
|
|
// do for AES-GCM IVs to avoid having to (re)write all 4 bytes every time
|
|
mac = createHmac(this._macSSLName, this._macKey);
|
|
writeUInt32BE(BUF_INT, this.outSeqno, 0);
|
|
mac.update(BUF_INT);
|
|
mac.update(packet);
|
|
}
|
|
|
|
let digest = mac.digest();
|
|
if (digest.length > this._macActualLen)
|
|
digest = digest.slice(0, this._macActualLen);
|
|
this._onWrite(digest);
|
|
|
|
this.outSeqno = (this.outSeqno + 1) >>> 0;
|
|
}
|
|
}
|
|
|
|
class GenericCipherBinding {
|
|
constructor(config) {
|
|
const enc = config.outbound;
|
|
this.outSeqno = enc.seqno;
|
|
this._onWrite = enc.onWrite;
|
|
this._encBlockLen = enc.cipherInfo.blockLen;
|
|
this._macLen = enc.macInfo.len;
|
|
this._macActualLen = enc.macInfo.actualLen;
|
|
this._aadLen = (enc.macInfo.isETM ? 4 : 0);
|
|
this._instance = new GenericCipher(enc.cipherInfo.sslName,
|
|
enc.cipherKey,
|
|
enc.cipherIV,
|
|
enc.macInfo.sslName,
|
|
enc.macKey,
|
|
enc.macInfo.isETM);
|
|
this._dead = false;
|
|
}
|
|
free() {
|
|
this._dead = true;
|
|
this._instance.free();
|
|
}
|
|
allocPacket(payloadLen) {
|
|
const blockLen = this._encBlockLen;
|
|
|
|
let pktLen = 4 + 1 + payloadLen;
|
|
let padLen = blockLen - ((pktLen - this._aadLen) & (blockLen - 1));
|
|
if (padLen < 4)
|
|
padLen += blockLen;
|
|
pktLen += padLen;
|
|
|
|
const packet = Buffer.allocUnsafe(pktLen + this._macLen);
|
|
|
|
writeUInt32BE(packet, pktLen - 4, 0);
|
|
packet[4] = padLen;
|
|
|
|
randomFillSync(packet, 5 + payloadLen, padLen);
|
|
|
|
return packet;
|
|
}
|
|
encrypt(packet) {
|
|
// `packet` === unencrypted packet
|
|
|
|
if (this._dead)
|
|
return;
|
|
|
|
// Encrypts in-place
|
|
this._instance.encrypt(packet, this.outSeqno);
|
|
|
|
if (this._macActualLen < this._macLen) {
|
|
packet = new FastBuffer(packet.buffer,
|
|
packet.byteOffset,
|
|
(packet.length
|
|
- (this._macLen - this._macActualLen)));
|
|
}
|
|
this._onWrite(packet);
|
|
|
|
this.outSeqno = (this.outSeqno + 1) >>> 0;
|
|
}
|
|
}
|
|
|
|
|
|
class NullDecipher {
|
|
constructor(seqno, onPayload) {
|
|
this.inSeqno = seqno;
|
|
this._onPayload = onPayload;
|
|
this._len = 0;
|
|
this._lenBytes = 0;
|
|
this._packet = null;
|
|
this._packetPos = 0;
|
|
}
|
|
free() {}
|
|
decrypt(data, p, dataLen) {
|
|
while (p < dataLen) {
|
|
// Read packet length
|
|
if (this._lenBytes < 4) {
|
|
let nb = Math.min(4 - this._lenBytes, dataLen - p);
|
|
|
|
this._lenBytes += nb;
|
|
while (nb--)
|
|
this._len = (this._len << 8) + data[p++];
|
|
|
|
if (this._lenBytes < 4)
|
|
return;
|
|
|
|
if (this._len > MAX_PACKET_SIZE
|
|
|| this._len < 8
|
|
|| (4 + this._len & 7) !== 0) {
|
|
throw new Error('Bad packet length');
|
|
}
|
|
if (p >= dataLen)
|
|
return;
|
|
}
|
|
|
|
// Read padding length, payload, and padding
|
|
if (this._packetPos < this._len) {
|
|
const nb = Math.min(this._len - this._packetPos, dataLen - p);
|
|
let chunk;
|
|
if (p !== 0 || nb !== dataLen)
|
|
chunk = new Uint8Array(data.buffer, data.byteOffset + p, nb);
|
|
else
|
|
chunk = data;
|
|
if (nb === this._len) {
|
|
this._packet = chunk;
|
|
} else {
|
|
if (!this._packet)
|
|
this._packet = Buffer.allocUnsafe(this._len);
|
|
this._packet.set(chunk, this._packetPos);
|
|
}
|
|
p += nb;
|
|
this._packetPos += nb;
|
|
if (this._packetPos < this._len)
|
|
return;
|
|
}
|
|
|
|
const payload = (!this._packet
|
|
? EMPTY_BUFFER
|
|
: new FastBuffer(this._packet.buffer,
|
|
this._packet.byteOffset + 1,
|
|
this._packet.length
|
|
- this._packet[0] - 1));
|
|
|
|
// Prepare for next packet
|
|
this.inSeqno = (this.inSeqno + 1) >>> 0;
|
|
this._len = 0;
|
|
this._lenBytes = 0;
|
|
this._packet = null;
|
|
this._packetPos = 0;
|
|
|
|
{
|
|
const ret = this._onPayload(payload);
|
|
if (ret !== undefined)
|
|
return (ret === false ? p : ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class ChaChaPolyDecipherNative {
|
|
constructor(config) {
|
|
const dec = config.inbound;
|
|
this.inSeqno = dec.seqno;
|
|
this._onPayload = dec.onPayload;
|
|
this._decKeyMain = dec.decipherKey.slice(0, 32);
|
|
this._decKeyPktLen = dec.decipherKey.slice(32);
|
|
this._len = 0;
|
|
this._lenBuf = Buffer.alloc(4);
|
|
this._lenPos = 0;
|
|
this._packet = null;
|
|
this._pktLen = 0;
|
|
this._mac = Buffer.allocUnsafe(16);
|
|
this._calcMac = Buffer.allocUnsafe(16);
|
|
this._macPos = 0;
|
|
}
|
|
free() {}
|
|
decrypt(data, p, dataLen) {
|
|
// `data` === encrypted data
|
|
|
|
while (p < dataLen) {
|
|
// Read packet length
|
|
if (this._lenPos < 4) {
|
|
let nb = Math.min(4 - this._lenPos, dataLen - p);
|
|
while (nb--)
|
|
this._lenBuf[this._lenPos++] = data[p++];
|
|
if (this._lenPos < 4)
|
|
return;
|
|
|
|
POLY1305_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian)
|
|
writeUInt32BE(POLY1305_OUT_COMPUTE, this.inSeqno, 12);
|
|
|
|
const decLenBytes =
|
|
createDecipheriv('chacha20', this._decKeyPktLen, POLY1305_OUT_COMPUTE)
|
|
.update(this._lenBuf);
|
|
this._len = readUInt32BE(decLenBytes, 0);
|
|
|
|
if (this._len > MAX_PACKET_SIZE
|
|
|| this._len < 8
|
|
|| (this._len & 7) !== 0) {
|
|
throw new Error('Bad packet length');
|
|
}
|
|
}
|
|
|
|
// Read padding length, payload, and padding
|
|
if (this._pktLen < this._len) {
|
|
if (p >= dataLen)
|
|
return;
|
|
const nb = Math.min(this._len - this._pktLen, dataLen - p);
|
|
let encrypted;
|
|
if (p !== 0 || nb !== dataLen)
|
|
encrypted = new Uint8Array(data.buffer, data.byteOffset + p, nb);
|
|
else
|
|
encrypted = data;
|
|
if (nb === this._len) {
|
|
this._packet = encrypted;
|
|
} else {
|
|
if (!this._packet)
|
|
this._packet = Buffer.allocUnsafe(this._len);
|
|
this._packet.set(encrypted, this._pktLen);
|
|
}
|
|
p += nb;
|
|
this._pktLen += nb;
|
|
if (this._pktLen < this._len || p >= dataLen)
|
|
return;
|
|
}
|
|
|
|
// Read Poly1305 MAC
|
|
{
|
|
const nb = Math.min(16 - this._macPos, dataLen - p);
|
|
// TODO: avoid copying if entire MAC is in current chunk
|
|
if (p !== 0 || nb !== dataLen) {
|
|
this._mac.set(
|
|
new Uint8Array(data.buffer, data.byteOffset + p, nb),
|
|
this._macPos
|
|
);
|
|
} else {
|
|
this._mac.set(data, this._macPos);
|
|
}
|
|
p += nb;
|
|
this._macPos += nb;
|
|
if (this._macPos < 16)
|
|
return;
|
|
}
|
|
|
|
// Generate Poly1305 key
|
|
POLY1305_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian)
|
|
writeUInt32BE(POLY1305_OUT_COMPUTE, this.inSeqno, 12);
|
|
const polyKey =
|
|
createCipheriv('chacha20', this._decKeyMain, POLY1305_OUT_COMPUTE)
|
|
.update(POLY1305_ZEROS);
|
|
|
|
// Calculate and compare Poly1305 MACs
|
|
poly1305_auth(POLY1305_RESULT_MALLOC,
|
|
this._lenBuf,
|
|
4,
|
|
this._packet,
|
|
this._packet.length,
|
|
polyKey);
|
|
|
|
this._calcMac.set(
|
|
new Uint8Array(POLY1305_WASM_MODULE.HEAPU8.buffer,
|
|
POLY1305_RESULT_MALLOC,
|
|
16),
|
|
0
|
|
);
|
|
if (!timingSafeEqual(this._calcMac, this._mac))
|
|
throw new Error('Invalid MAC');
|
|
|
|
// Decrypt packet
|
|
POLY1305_OUT_COMPUTE[0] = 1; // Set counter to 1 (little endian)
|
|
const packet =
|
|
createDecipheriv('chacha20', this._decKeyMain, POLY1305_OUT_COMPUTE)
|
|
.update(this._packet);
|
|
|
|
const payload = new FastBuffer(packet.buffer,
|
|
packet.byteOffset + 1,
|
|
packet.length - packet[0] - 1);
|
|
|
|
// Prepare for next packet
|
|
this.inSeqno = (this.inSeqno + 1) >>> 0;
|
|
this._len = 0;
|
|
this._lenPos = 0;
|
|
this._packet = null;
|
|
this._pktLen = 0;
|
|
this._macPos = 0;
|
|
|
|
{
|
|
const ret = this._onPayload(payload);
|
|
if (ret !== undefined)
|
|
return (ret === false ? p : ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class ChaChaPolyDecipherBinding {
|
|
constructor(config) {
|
|
const dec = config.inbound;
|
|
this.inSeqno = dec.seqno;
|
|
this._onPayload = dec.onPayload;
|
|
this._instance = new ChaChaPolyDecipher(dec.decipherKey);
|
|
this._len = 0;
|
|
this._lenBuf = Buffer.alloc(4);
|
|
this._lenPos = 0;
|
|
this._packet = null;
|
|
this._pktLen = 0;
|
|
this._mac = Buffer.allocUnsafe(16);
|
|
this._macPos = 0;
|
|
}
|
|
free() {
|
|
this._instance.free();
|
|
}
|
|
decrypt(data, p, dataLen) {
|
|
// `data` === encrypted data
|
|
|
|
while (p < dataLen) {
|
|
// Read packet length
|
|
if (this._lenPos < 4) {
|
|
let nb = Math.min(4 - this._lenPos, dataLen - p);
|
|
while (nb--)
|
|
this._lenBuf[this._lenPos++] = data[p++];
|
|
if (this._lenPos < 4)
|
|
return;
|
|
|
|
this._len = this._instance.decryptLen(this._lenBuf, this.inSeqno);
|
|
|
|
if (this._len > MAX_PACKET_SIZE
|
|
|| this._len < 8
|
|
|| (this._len & 7) !== 0) {
|
|
throw new Error('Bad packet length');
|
|
}
|
|
|
|
if (p >= dataLen)
|
|
return;
|
|
}
|
|
|
|
// Read padding length, payload, and padding
|
|
if (this._pktLen < this._len) {
|
|
const nb = Math.min(this._len - this._pktLen, dataLen - p);
|
|
let encrypted;
|
|
if (p !== 0 || nb !== dataLen)
|
|
encrypted = new Uint8Array(data.buffer, data.byteOffset + p, nb);
|
|
else
|
|
encrypted = data;
|
|
if (nb === this._len) {
|
|
this._packet = encrypted;
|
|
} else {
|
|
if (!this._packet)
|
|
this._packet = Buffer.allocUnsafe(this._len);
|
|
this._packet.set(encrypted, this._pktLen);
|
|
}
|
|
p += nb;
|
|
this._pktLen += nb;
|
|
if (this._pktLen < this._len || p >= dataLen)
|
|
return;
|
|
}
|
|
|
|
// Read Poly1305 MAC
|
|
{
|
|
const nb = Math.min(16 - this._macPos, dataLen - p);
|
|
// TODO: avoid copying if entire MAC is in current chunk
|
|
if (p !== 0 || nb !== dataLen) {
|
|
this._mac.set(
|
|
new Uint8Array(data.buffer, data.byteOffset + p, nb),
|
|
this._macPos
|
|
);
|
|
} else {
|
|
this._mac.set(data, this._macPos);
|
|
}
|
|
p += nb;
|
|
this._macPos += nb;
|
|
if (this._macPos < 16)
|
|
return;
|
|
}
|
|
|
|
this._instance.decrypt(this._packet, this._mac, this.inSeqno);
|
|
|
|
const payload = new FastBuffer(this._packet.buffer,
|
|
this._packet.byteOffset + 1,
|
|
this._packet.length - this._packet[0] - 1);
|
|
|
|
// Prepare for next packet
|
|
this.inSeqno = (this.inSeqno + 1) >>> 0;
|
|
this._len = 0;
|
|
this._lenPos = 0;
|
|
this._packet = null;
|
|
this._pktLen = 0;
|
|
this._macPos = 0;
|
|
|
|
{
|
|
const ret = this._onPayload(payload);
|
|
if (ret !== undefined)
|
|
return (ret === false ? p : ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class AESGCMDecipherNative {
|
|
constructor(config) {
|
|
const dec = config.inbound;
|
|
this.inSeqno = dec.seqno;
|
|
this._onPayload = dec.onPayload;
|
|
this._decipherInstance = null;
|
|
this._decipherSSLName = dec.decipherInfo.sslName;
|
|
this._decipherKey = dec.decipherKey;
|
|
this._decipherIV = dec.decipherIV;
|
|
this._len = 0;
|
|
this._lenBytes = 0;
|
|
this._packet = null;
|
|
this._packetPos = 0;
|
|
this._pktLen = 0;
|
|
this._tag = Buffer.allocUnsafe(16);
|
|
this._tagPos = 0;
|
|
}
|
|
free() {}
|
|
decrypt(data, p, dataLen) {
|
|
// `data` === encrypted data
|
|
|
|
while (p < dataLen) {
|
|
// Read packet length (unencrypted, but AAD)
|
|
if (this._lenBytes < 4) {
|
|
let nb = Math.min(4 - this._lenBytes, dataLen - p);
|
|
this._lenBytes += nb;
|
|
while (nb--)
|
|
this._len = (this._len << 8) + data[p++];
|
|
if (this._lenBytes < 4)
|
|
return;
|
|
|
|
if ((this._len + 20) > MAX_PACKET_SIZE
|
|
|| this._len < 16
|
|
|| (this._len & 15) !== 0) {
|
|
throw new Error('Bad packet length');
|
|
}
|
|
|
|
this._decipherInstance = createDecipheriv(
|
|
this._decipherSSLName,
|
|
this._decipherKey,
|
|
this._decipherIV
|
|
);
|
|
this._decipherInstance.setAutoPadding(false);
|
|
this._decipherInstance.setAAD(intToBytes(this._len));
|
|
}
|
|
|
|
// Read padding length, payload, and padding
|
|
if (this._pktLen < this._len) {
|
|
if (p >= dataLen)
|
|
return;
|
|
const nb = Math.min(this._len - this._pktLen, dataLen - p);
|
|
let decrypted;
|
|
if (p !== 0 || nb !== dataLen) {
|
|
decrypted = this._decipherInstance.update(
|
|
new Uint8Array(data.buffer, data.byteOffset + p, nb)
|
|
);
|
|
} else {
|
|
decrypted = this._decipherInstance.update(data);
|
|
}
|
|
if (decrypted.length) {
|
|
if (nb === this._len) {
|
|
this._packet = decrypted;
|
|
} else {
|
|
if (!this._packet)
|
|
this._packet = Buffer.allocUnsafe(this._len);
|
|
this._packet.set(decrypted, this._packetPos);
|
|
}
|
|
this._packetPos += decrypted.length;
|
|
}
|
|
p += nb;
|
|
this._pktLen += nb;
|
|
if (this._pktLen < this._len || p >= dataLen)
|
|
return;
|
|
}
|
|
|
|
// Read authentication tag
|
|
{
|
|
const nb = Math.min(16 - this._tagPos, dataLen - p);
|
|
if (p !== 0 || nb !== dataLen) {
|
|
this._tag.set(
|
|
new Uint8Array(data.buffer, data.byteOffset + p, nb),
|
|
this._tagPos
|
|
);
|
|
} else {
|
|
this._tag.set(data, this._tagPos);
|
|
}
|
|
p += nb;
|
|
this._tagPos += nb;
|
|
if (this._tagPos < 16)
|
|
return;
|
|
}
|
|
|
|
{
|
|
// Verify authentication tag
|
|
this._decipherInstance.setAuthTag(this._tag);
|
|
|
|
const decrypted = this._decipherInstance.final();
|
|
|
|
// XXX: this should never output any data since stream ciphers always
|
|
// return data from .update() and block ciphers must end on a multiple
|
|
// of the block length, which would have caused an exception to be
|
|
// thrown if the total input was not...
|
|
if (decrypted.length) {
|
|
if (this._packet)
|
|
this._packet.set(decrypted, this._packetPos);
|
|
else
|
|
this._packet = decrypted;
|
|
}
|
|
}
|
|
|
|
const payload = (!this._packet
|
|
? EMPTY_BUFFER
|
|
: new FastBuffer(this._packet.buffer,
|
|
this._packet.byteOffset + 1,
|
|
this._packet.length
|
|
- this._packet[0] - 1));
|
|
|
|
// Prepare for next packet
|
|
this.inSeqno = (this.inSeqno + 1) >>> 0;
|
|
ivIncrement(this._decipherIV);
|
|
this._len = 0;
|
|
this._lenBytes = 0;
|
|
this._packet = null;
|
|
this._packetPos = 0;
|
|
this._pktLen = 0;
|
|
this._tagPos = 0;
|
|
|
|
{
|
|
const ret = this._onPayload(payload);
|
|
if (ret !== undefined)
|
|
return (ret === false ? p : ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class AESGCMDecipherBinding {
|
|
constructor(config) {
|
|
const dec = config.inbound;
|
|
this.inSeqno = dec.seqno;
|
|
this._onPayload = dec.onPayload;
|
|
this._instance = new AESGCMDecipher(dec.decipherInfo.sslName,
|
|
dec.decipherKey,
|
|
dec.decipherIV);
|
|
this._len = 0;
|
|
this._lenBytes = 0;
|
|
this._packet = null;
|
|
this._pktLen = 0;
|
|
this._tag = Buffer.allocUnsafe(16);
|
|
this._tagPos = 0;
|
|
}
|
|
free() {}
|
|
decrypt(data, p, dataLen) {
|
|
// `data` === encrypted data
|
|
|
|
while (p < dataLen) {
|
|
// Read packet length (unencrypted, but AAD)
|
|
if (this._lenBytes < 4) {
|
|
let nb = Math.min(4 - this._lenBytes, dataLen - p);
|
|
this._lenBytes += nb;
|
|
while (nb--)
|
|
this._len = (this._len << 8) + data[p++];
|
|
if (this._lenBytes < 4)
|
|
return;
|
|
|
|
if ((this._len + 20) > MAX_PACKET_SIZE
|
|
|| this._len < 16
|
|
|| (this._len & 15) !== 0) {
|
|
throw new Error(`Bad packet length: ${this._len}`);
|
|
}
|
|
}
|
|
|
|
// Read padding length, payload, and padding
|
|
if (this._pktLen < this._len) {
|
|
if (p >= dataLen)
|
|
return;
|
|
const nb = Math.min(this._len - this._pktLen, dataLen - p);
|
|
let encrypted;
|
|
if (p !== 0 || nb !== dataLen)
|
|
encrypted = new Uint8Array(data.buffer, data.byteOffset + p, nb);
|
|
else
|
|
encrypted = data;
|
|
if (nb === this._len) {
|
|
this._packet = encrypted;
|
|
} else {
|
|
if (!this._packet)
|
|
this._packet = Buffer.allocUnsafe(this._len);
|
|
this._packet.set(encrypted, this._pktLen);
|
|
}
|
|
p += nb;
|
|
this._pktLen += nb;
|
|
if (this._pktLen < this._len || p >= dataLen)
|
|
return;
|
|
}
|
|
|
|
// Read authentication tag
|
|
{
|
|
const nb = Math.min(16 - this._tagPos, dataLen - p);
|
|
if (p !== 0 || nb !== dataLen) {
|
|
this._tag.set(
|
|
new Uint8Array(data.buffer, data.byteOffset + p, nb),
|
|
this._tagPos
|
|
);
|
|
} else {
|
|
this._tag.set(data, this._tagPos);
|
|
}
|
|
p += nb;
|
|
this._tagPos += nb;
|
|
if (this._tagPos < 16)
|
|
return;
|
|
}
|
|
|
|
this._instance.decrypt(this._packet, this._len, this._tag);
|
|
|
|
const payload = new FastBuffer(this._packet.buffer,
|
|
this._packet.byteOffset + 1,
|
|
this._packet.length - this._packet[0] - 1);
|
|
|
|
// Prepare for next packet
|
|
this.inSeqno = (this.inSeqno + 1) >>> 0;
|
|
this._len = 0;
|
|
this._lenBytes = 0;
|
|
this._packet = null;
|
|
this._pktLen = 0;
|
|
this._tagPos = 0;
|
|
|
|
{
|
|
const ret = this._onPayload(payload);
|
|
if (ret !== undefined)
|
|
return (ret === false ? p : ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: test incremental .update()s vs. copying to _packet and doing a single
|
|
// .update() after entire packet read -- a single .update() would allow
|
|
// verifying MAC before decrypting for ETM MACs
|
|
class GenericDecipherNative {
|
|
constructor(config) {
|
|
const dec = config.inbound;
|
|
this.inSeqno = dec.seqno;
|
|
this._onPayload = dec.onPayload;
|
|
this._decipherInstance = createDecipheriv(dec.decipherInfo.sslName,
|
|
dec.decipherKey,
|
|
dec.decipherIV);
|
|
this._decipherInstance.setAutoPadding(false);
|
|
this._block = Buffer.allocUnsafe(
|
|
dec.macInfo.isETM ? 4 : dec.decipherInfo.blockLen
|
|
);
|
|
this._blockSize = dec.decipherInfo.blockLen;
|
|
this._blockPos = 0;
|
|
this._len = 0;
|
|
this._packet = null;
|
|
this._packetPos = 0;
|
|
this._pktLen = 0;
|
|
this._mac = Buffer.allocUnsafe(dec.macInfo.actualLen);
|
|
this._macPos = 0;
|
|
this._macSSLName = dec.macInfo.sslName;
|
|
this._macKey = dec.macKey;
|
|
this._macActualLen = dec.macInfo.actualLen;
|
|
this._macETM = dec.macInfo.isETM;
|
|
this._macInstance = null;
|
|
|
|
const discardLen = dec.decipherInfo.discardLen;
|
|
if (discardLen) {
|
|
let discard = DISCARD_CACHE.get(discardLen);
|
|
if (discard === undefined) {
|
|
discard = Buffer.alloc(discardLen);
|
|
DISCARD_CACHE.set(discardLen, discard);
|
|
}
|
|
this._decipherInstance.update(discard);
|
|
}
|
|
}
|
|
free() {}
|
|
decrypt(data, p, dataLen) {
|
|
// `data` === encrypted data
|
|
|
|
while (p < dataLen) {
|
|
// Read first encrypted block
|
|
if (this._blockPos < this._block.length) {
|
|
const nb = Math.min(this._block.length - this._blockPos, dataLen - p);
|
|
if (p !== 0 || nb !== dataLen || nb < data.length) {
|
|
this._block.set(
|
|
new Uint8Array(data.buffer, data.byteOffset + p, nb),
|
|
this._blockPos
|
|
);
|
|
} else {
|
|
this._block.set(data, this._blockPos);
|
|
}
|
|
|
|
p += nb;
|
|
this._blockPos += nb;
|
|
if (this._blockPos < this._block.length)
|
|
return;
|
|
|
|
let decrypted;
|
|
let need;
|
|
if (this._macETM) {
|
|
this._len = need = readUInt32BE(this._block, 0);
|
|
} else {
|
|
// Decrypt first block to get packet length
|
|
decrypted = this._decipherInstance.update(this._block);
|
|
this._len = readUInt32BE(decrypted, 0);
|
|
need = 4 + this._len - this._blockSize;
|
|
}
|
|
|
|
if (this._len > MAX_PACKET_SIZE
|
|
|| this._len < 5
|
|
|| (need & (this._blockSize - 1)) !== 0) {
|
|
throw new Error('Bad packet length');
|
|
}
|
|
|
|
// Create MAC up front to calculate in parallel with decryption
|
|
this._macInstance = createHmac(this._macSSLName, this._macKey);
|
|
|
|
writeUInt32BE(BUF_INT, this.inSeqno, 0);
|
|
this._macInstance.update(BUF_INT);
|
|
if (this._macETM) {
|
|
this._macInstance.update(this._block);
|
|
} else {
|
|
this._macInstance.update(new Uint8Array(decrypted.buffer,
|
|
decrypted.byteOffset,
|
|
4));
|
|
this._pktLen = decrypted.length - 4;
|
|
this._packetPos = this._pktLen;
|
|
this._packet = Buffer.allocUnsafe(this._len);
|
|
this._packet.set(
|
|
new Uint8Array(decrypted.buffer,
|
|
decrypted.byteOffset + 4,
|
|
this._packetPos),
|
|
0
|
|
);
|
|
}
|
|
|
|
if (p >= dataLen)
|
|
return;
|
|
}
|
|
|
|
// Read padding length, payload, and padding
|
|
if (this._pktLen < this._len) {
|
|
const nb = Math.min(this._len - this._pktLen, dataLen - p);
|
|
let encrypted;
|
|
if (p !== 0 || nb !== dataLen)
|
|
encrypted = new Uint8Array(data.buffer, data.byteOffset + p, nb);
|
|
else
|
|
encrypted = data;
|
|
if (this._macETM)
|
|
this._macInstance.update(encrypted);
|
|
const decrypted = this._decipherInstance.update(encrypted);
|
|
if (decrypted.length) {
|
|
if (nb === this._len) {
|
|
this._packet = decrypted;
|
|
} else {
|
|
if (!this._packet)
|
|
this._packet = Buffer.allocUnsafe(this._len);
|
|
this._packet.set(decrypted, this._packetPos);
|
|
}
|
|
this._packetPos += decrypted.length;
|
|
}
|
|
p += nb;
|
|
this._pktLen += nb;
|
|
if (this._pktLen < this._len || p >= dataLen)
|
|
return;
|
|
}
|
|
|
|
// Read MAC
|
|
{
|
|
const nb = Math.min(this._macActualLen - this._macPos, dataLen - p);
|
|
if (p !== 0 || nb !== dataLen) {
|
|
this._mac.set(
|
|
new Uint8Array(data.buffer, data.byteOffset + p, nb),
|
|
this._macPos
|
|
);
|
|
} else {
|
|
this._mac.set(data, this._macPos);
|
|
}
|
|
p += nb;
|
|
this._macPos += nb;
|
|
if (this._macPos < this._macActualLen)
|
|
return;
|
|
}
|
|
|
|
// Verify MAC
|
|
if (!this._macETM)
|
|
this._macInstance.update(this._packet);
|
|
let calculated = this._macInstance.digest();
|
|
if (this._macActualLen < calculated.length) {
|
|
calculated = new Uint8Array(calculated.buffer,
|
|
calculated.byteOffset,
|
|
this._macActualLen);
|
|
}
|
|
if (!timingSafeEquals(calculated, this._mac))
|
|
throw new Error('Invalid MAC');
|
|
|
|
const payload = new FastBuffer(this._packet.buffer,
|
|
this._packet.byteOffset + 1,
|
|
this._packet.length - this._packet[0] - 1);
|
|
|
|
// Prepare for next packet
|
|
this.inSeqno = (this.inSeqno + 1) >>> 0;
|
|
this._blockPos = 0;
|
|
this._len = 0;
|
|
this._packet = null;
|
|
this._packetPos = 0;
|
|
this._pktLen = 0;
|
|
this._macPos = 0;
|
|
this._macInstance = null;
|
|
|
|
{
|
|
const ret = this._onPayload(payload);
|
|
if (ret !== undefined)
|
|
return (ret === false ? p : ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class GenericDecipherBinding {
|
|
constructor(config) {
|
|
const dec = config.inbound;
|
|
this.inSeqno = dec.seqno;
|
|
this._onPayload = dec.onPayload;
|
|
this._instance = new GenericDecipher(dec.decipherInfo.sslName,
|
|
dec.decipherKey,
|
|
dec.decipherIV,
|
|
dec.macInfo.sslName,
|
|
dec.macKey,
|
|
dec.macInfo.isETM,
|
|
dec.macInfo.actualLen);
|
|
this._block = Buffer.allocUnsafe(
|
|
dec.macInfo.isETM || dec.decipherInfo.stream
|
|
? 4
|
|
: dec.decipherInfo.blockLen
|
|
);
|
|
this._blockPos = 0;
|
|
this._len = 0;
|
|
this._packet = null;
|
|
this._pktLen = 0;
|
|
this._mac = Buffer.allocUnsafe(dec.macInfo.actualLen);
|
|
this._macPos = 0;
|
|
this._macActualLen = dec.macInfo.actualLen;
|
|
this._macETM = dec.macInfo.isETM;
|
|
}
|
|
free() {
|
|
this._instance.free();
|
|
}
|
|
decrypt(data, p, dataLen) {
|
|
// `data` === encrypted data
|
|
|
|
while (p < dataLen) {
|
|
// Read first encrypted block
|
|
if (this._blockPos < this._block.length) {
|
|
const nb = Math.min(this._block.length - this._blockPos, dataLen - p);
|
|
if (p !== 0 || nb !== dataLen || nb < data.length) {
|
|
this._block.set(
|
|
new Uint8Array(data.buffer, data.byteOffset + p, nb),
|
|
this._blockPos
|
|
);
|
|
} else {
|
|
this._block.set(data, this._blockPos);
|
|
}
|
|
|
|
p += nb;
|
|
this._blockPos += nb;
|
|
if (this._blockPos < this._block.length)
|
|
return;
|
|
|
|
let need;
|
|
if (this._macETM) {
|
|
this._len = need = readUInt32BE(this._block, 0);
|
|
} else {
|
|
// Decrypt first block to get packet length
|
|
this._instance.decryptBlock(this._block);
|
|
this._len = readUInt32BE(this._block, 0);
|
|
need = 4 + this._len - this._block.length;
|
|
}
|
|
|
|
if (this._len > MAX_PACKET_SIZE
|
|
|| this._len < 5
|
|
|| (need & (this._block.length - 1)) !== 0) {
|
|
throw new Error('Bad packet length');
|
|
}
|
|
|
|
if (!this._macETM) {
|
|
this._pktLen = (this._block.length - 4);
|
|
if (this._pktLen) {
|
|
this._packet = Buffer.allocUnsafe(this._len);
|
|
this._packet.set(
|
|
new Uint8Array(this._block.buffer,
|
|
this._block.byteOffset + 4,
|
|
this._pktLen),
|
|
0
|
|
);
|
|
}
|
|
}
|
|
|
|
if (p >= dataLen)
|
|
return;
|
|
}
|
|
|
|
// Read padding length, payload, and padding
|
|
if (this._pktLen < this._len) {
|
|
const nb = Math.min(this._len - this._pktLen, dataLen - p);
|
|
let encrypted;
|
|
if (p !== 0 || nb !== dataLen)
|
|
encrypted = new Uint8Array(data.buffer, data.byteOffset + p, nb);
|
|
else
|
|
encrypted = data;
|
|
if (nb === this._len) {
|
|
this._packet = encrypted;
|
|
} else {
|
|
if (!this._packet)
|
|
this._packet = Buffer.allocUnsafe(this._len);
|
|
this._packet.set(encrypted, this._pktLen);
|
|
}
|
|
p += nb;
|
|
this._pktLen += nb;
|
|
if (this._pktLen < this._len || p >= dataLen)
|
|
return;
|
|
}
|
|
|
|
// Read MAC
|
|
{
|
|
const nb = Math.min(this._macActualLen - this._macPos, dataLen - p);
|
|
if (p !== 0 || nb !== dataLen) {
|
|
this._mac.set(
|
|
new Uint8Array(data.buffer, data.byteOffset + p, nb),
|
|
this._macPos
|
|
);
|
|
} else {
|
|
this._mac.set(data, this._macPos);
|
|
}
|
|
p += nb;
|
|
this._macPos += nb;
|
|
if (this._macPos < this._macActualLen)
|
|
return;
|
|
}
|
|
|
|
// Decrypt and verify MAC
|
|
this._instance.decrypt(this._packet,
|
|
this.inSeqno,
|
|
this._block,
|
|
this._mac);
|
|
|
|
const payload = new FastBuffer(this._packet.buffer,
|
|
this._packet.byteOffset + 1,
|
|
this._packet.length - this._packet[0] - 1);
|
|
|
|
// Prepare for next packet
|
|
this.inSeqno = (this.inSeqno + 1) >>> 0;
|
|
this._blockPos = 0;
|
|
this._len = 0;
|
|
this._packet = null;
|
|
this._pktLen = 0;
|
|
this._macPos = 0;
|
|
this._macInstance = null;
|
|
|
|
{
|
|
const ret = this._onPayload(payload);
|
|
if (ret !== undefined)
|
|
return (ret === false ? p : ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Increments unsigned, big endian counter (last 8 bytes) of AES-GCM IV
|
|
function ivIncrement(iv) {
|
|
// eslint-disable-next-line no-unused-expressions
|
|
++iv[11] >>> 8
|
|
&& ++iv[10] >>> 8
|
|
&& ++iv[9] >>> 8
|
|
&& ++iv[8] >>> 8
|
|
&& ++iv[7] >>> 8
|
|
&& ++iv[6] >>> 8
|
|
&& ++iv[5] >>> 8
|
|
&& ++iv[4] >>> 8;
|
|
}
|
|
|
|
const intToBytes = (() => {
|
|
const ret = Buffer.alloc(4);
|
|
return (n) => {
|
|
ret[0] = (n >>> 24);
|
|
ret[1] = (n >>> 16);
|
|
ret[2] = (n >>> 8);
|
|
ret[3] = n;
|
|
return ret;
|
|
};
|
|
})();
|
|
|
|
function timingSafeEquals(a, b) {
|
|
if (a.length !== b.length) {
|
|
timingSafeEqual(a, a);
|
|
return false;
|
|
}
|
|
return timingSafeEqual(a, b);
|
|
}
|
|
|
|
function createCipher(config) {
|
|
if (typeof config !== 'object' || config === null)
|
|
throw new Error('Invalid config');
|
|
|
|
if (typeof config.outbound !== 'object' || config.outbound === null)
|
|
throw new Error('Invalid outbound');
|
|
|
|
const outbound = config.outbound;
|
|
|
|
if (typeof outbound.onWrite !== 'function')
|
|
throw new Error('Invalid outbound.onWrite');
|
|
|
|
if (typeof outbound.cipherInfo !== 'object' || outbound.cipherInfo === null)
|
|
throw new Error('Invalid outbound.cipherInfo');
|
|
|
|
if (!Buffer.isBuffer(outbound.cipherKey)
|
|
|| outbound.cipherKey.length !== outbound.cipherInfo.keyLen) {
|
|
throw new Error('Invalid outbound.cipherKey');
|
|
}
|
|
|
|
if (outbound.cipherInfo.ivLen
|
|
&& (!Buffer.isBuffer(outbound.cipherIV)
|
|
|| outbound.cipherIV.length !== outbound.cipherInfo.ivLen)) {
|
|
throw new Error('Invalid outbound.cipherIV');
|
|
}
|
|
|
|
if (typeof outbound.seqno !== 'number'
|
|
|| outbound.seqno < 0
|
|
|| outbound.seqno > MAX_SEQNO) {
|
|
throw new Error('Invalid outbound.seqno');
|
|
}
|
|
|
|
const forceNative = !!outbound.forceNative;
|
|
|
|
switch (outbound.cipherInfo.sslName) {
|
|
case 'aes-128-gcm':
|
|
case 'aes-256-gcm':
|
|
return (AESGCMCipher && !forceNative
|
|
? new AESGCMCipherBinding(config)
|
|
: new AESGCMCipherNative(config));
|
|
case 'chacha20':
|
|
return (ChaChaPolyCipher && !forceNative
|
|
? new ChaChaPolyCipherBinding(config)
|
|
: new ChaChaPolyCipherNative(config));
|
|
default: {
|
|
if (typeof outbound.macInfo !== 'object' || outbound.macInfo === null)
|
|
throw new Error('Invalid outbound.macInfo');
|
|
if (!Buffer.isBuffer(outbound.macKey)
|
|
|| outbound.macKey.length !== outbound.macInfo.len) {
|
|
throw new Error('Invalid outbound.macKey');
|
|
}
|
|
return (GenericCipher && !forceNative
|
|
? new GenericCipherBinding(config)
|
|
: new GenericCipherNative(config));
|
|
}
|
|
}
|
|
}
|
|
|
|
function createDecipher(config) {
|
|
if (typeof config !== 'object' || config === null)
|
|
throw new Error('Invalid config');
|
|
|
|
if (typeof config.inbound !== 'object' || config.inbound === null)
|
|
throw new Error('Invalid inbound');
|
|
|
|
const inbound = config.inbound;
|
|
|
|
if (typeof inbound.onPayload !== 'function')
|
|
throw new Error('Invalid inbound.onPayload');
|
|
|
|
if (typeof inbound.decipherInfo !== 'object'
|
|
|| inbound.decipherInfo === null) {
|
|
throw new Error('Invalid inbound.decipherInfo');
|
|
}
|
|
|
|
if (!Buffer.isBuffer(inbound.decipherKey)
|
|
|| inbound.decipherKey.length !== inbound.decipherInfo.keyLen) {
|
|
throw new Error('Invalid inbound.decipherKey');
|
|
}
|
|
|
|
if (inbound.decipherInfo.ivLen
|
|
&& (!Buffer.isBuffer(inbound.decipherIV)
|
|
|| inbound.decipherIV.length !== inbound.decipherInfo.ivLen)) {
|
|
throw new Error('Invalid inbound.decipherIV');
|
|
}
|
|
|
|
if (typeof inbound.seqno !== 'number'
|
|
|| inbound.seqno < 0
|
|
|| inbound.seqno > MAX_SEQNO) {
|
|
throw new Error('Invalid inbound.seqno');
|
|
}
|
|
|
|
const forceNative = !!inbound.forceNative;
|
|
|
|
switch (inbound.decipherInfo.sslName) {
|
|
case 'aes-128-gcm':
|
|
case 'aes-256-gcm':
|
|
return (AESGCMDecipher && !forceNative
|
|
? new AESGCMDecipherBinding(config)
|
|
: new AESGCMDecipherNative(config));
|
|
case 'chacha20':
|
|
return (ChaChaPolyDecipher && !forceNative
|
|
? new ChaChaPolyDecipherBinding(config)
|
|
: new ChaChaPolyDecipherNative(config));
|
|
default: {
|
|
if (typeof inbound.macInfo !== 'object' || inbound.macInfo === null)
|
|
throw new Error('Invalid inbound.macInfo');
|
|
if (!Buffer.isBuffer(inbound.macKey)
|
|
|| inbound.macKey.length !== inbound.macInfo.len) {
|
|
throw new Error('Invalid inbound.macKey');
|
|
}
|
|
return (GenericDecipher && !forceNative
|
|
? new GenericDecipherBinding(config)
|
|
: new GenericDecipherNative(config));
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
CIPHER_INFO,
|
|
MAC_INFO,
|
|
bindingAvailable: !!binding,
|
|
init: (() => {
|
|
// eslint-disable-next-line no-async-promise-executor
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
POLY1305_WASM_MODULE = await require('./crypto/poly1305.js')();
|
|
POLY1305_RESULT_MALLOC = POLY1305_WASM_MODULE._malloc(16);
|
|
poly1305_auth = POLY1305_WASM_MODULE.cwrap(
|
|
'poly1305_auth',
|
|
null,
|
|
['number', 'array', 'number', 'array', 'number', 'array']
|
|
);
|
|
} catch (ex) {
|
|
return reject(ex);
|
|
}
|
|
resolve();
|
|
});
|
|
})(),
|
|
|
|
NullCipher,
|
|
createCipher,
|
|
NullDecipher,
|
|
createDecipher,
|
|
};
|