Files
bare-operating-system/packages/bare-os-openssh/vendor/bare-ssh2/lib/protocol/crypto.js
T
2026-04-08 22:21:22 -04:00

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
}