1552 lines
45 KiB
JavaScript
1552 lines
45 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 crypto = require('../../shims/crypto/index.js')
|
|
const {
|
|
createHmac,
|
|
randomFillSync,
|
|
createCipheriv: nodeCreateCipheriv,
|
|
createDecipheriv: nodeCreateDecipheriv
|
|
} = crypto
|
|
|
|
function timingSafeEqual(a, b) {
|
|
if (typeof crypto.timingSafeEqual === 'function') {
|
|
try {
|
|
return crypto.timingSafeEqual(a, b)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
const ab = Buffer.isBuffer(a) ? a : Buffer.from(a)
|
|
const bb = Buffer.isBuffer(b) ? b : Buffer.from(b)
|
|
if (ab.length !== bb.length) return false
|
|
let diff = 0
|
|
for (let i = 0; i < ab.length; i++) diff |= ab[i] ^ bb[i]
|
|
return diff === 0
|
|
}
|
|
|
|
const { readUInt32BE, writeUInt32BE } = require('./utils.js')
|
|
|
|
const FastBuffer = Buffer[Symbol.species]
|
|
|
|
let JsChacha20Cipher
|
|
try {
|
|
nodeCreateCipheriv('chacha20', Buffer.alloc(32), Buffer.alloc(16))
|
|
} catch {
|
|
JsChacha20Cipher = require('chacha20').Cipher
|
|
}
|
|
|
|
function chachaStreamFromIv(key, iv) {
|
|
const keyBuf = Buffer.isBuffer(key) ? key : Buffer.from(key)
|
|
const ivBuf = Buffer.isBuffer(iv) ? iv : Buffer.from(iv)
|
|
const counter = ivBuf.readUInt32LE(0)
|
|
const nonce = Buffer.allocUnsafe(12)
|
|
ivBuf.copy(nonce, 0, 4, 16)
|
|
const c = new JsChacha20Cipher(keyBuf, nonce, counter)
|
|
return {
|
|
update(data) {
|
|
const inBuf = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
|
const out = Buffer.allocUnsafe(inBuf.length)
|
|
c.encrypt(out, inBuf, inBuf.length)
|
|
return FastBuffer.from(out)
|
|
}
|
|
}
|
|
}
|
|
|
|
function createCipheriv(algo, key, iv, opts) {
|
|
if (algo === 'chacha20' && JsChacha20Cipher) return chachaStreamFromIv(key, iv)
|
|
return nodeCreateCipheriv(algo, key, iv, opts)
|
|
}
|
|
|
|
function createDecipheriv(algo, key, iv, opts) {
|
|
if (algo === 'chacha20' && JsChacha20Cipher) return chachaStreamFromIv(key, iv)
|
|
return nodeCreateDecipheriv(algo, key, iv, opts)
|
|
}
|
|
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 {
|
|
'[email protected]': 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),
|
|
'[email protected]': info('aes-128-gcm', 16, 16, 12, 16, 0, CIPHER_STREAM),
|
|
'[email protected]': 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),
|
|
'[email protected]': 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),
|
|
'[email protected]': info('sha1', 20, 20, true),
|
|
'hmac-sha1-96': info('sha1', 20, 12, false),
|
|
'hmac-sha2-256': info('sha256', 32, 32, false),
|
|
'[email protected]': info('sha256', 32, 32, true),
|
|
'hmac-sha2-256-96': info('sha256', 32, 12, false),
|
|
'hmac-sha2-512': info('sha512', 64, 64, false),
|
|
'[email protected]': 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)
|
|
try {
|
|
if (typeof cipher.setAutoPadding === 'function') cipher.setAutoPadding(false)
|
|
} catch {
|
|
/* bare-crypto AEAD has no inner setAutoPadding; OpenSSL no-op for GCM */
|
|
}
|
|
|
|
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
|
|
)
|
|
try {
|
|
if (typeof this._decipherInstance.setAutoPadding === 'function')
|
|
this._decipherInstance.setAutoPadding(false)
|
|
} catch {
|
|
/* bare-crypto AEAD */
|
|
}
|
|
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
|
|
)
|
|
try {
|
|
if (typeof this._decipherInstance.setAutoPadding === 'function')
|
|
this._decipherInstance.setAutoPadding(false)
|
|
} catch {
|
|
/* bare-crypto AEAD */
|
|
}
|
|
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
|
|
}
|