Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 89 additions & 35 deletions lib/voice/VoiceConnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ try {

/** @type {import("@snazzah/davey")?} */
let Davey = null;
/** @type {import("@stablelib/xchacha20poly1305")?} */
let StableLib = null;
/** @type {import("@projectdysnomia/libsodium")?} */
let Sodium = null;
/** @type {import("node:crypto")?} */
let crypto = null;
let aes256Available = false;

Expand Down Expand Up @@ -97,15 +100,15 @@ class VoiceConnection extends EventEmitter {
ready = false;
reconnecting = false;
samplingRate = 48_000;
sendBuffer = Buffer.allocUnsafe(16 + 32 + MAX_FRAME_SIZE);
sendHeader = Buffer.alloc(12);

#davePendingTransitions = new Map();
#daveDowngraded = false;
#wsSendBuffer = null;
#clientsConnected = new Set();
#nonce = 0;
/** @type {import("@projectdysnomia/libsodium").BufferPointer?} */
#nonceBuffer = null;
/** @type {import("@stablelib/xchacha20poly1305").XChaCha20Poly1305?} */
#stablelib = null;
sequence = 0;
speaking = false;
Expand All @@ -130,7 +133,7 @@ class VoiceConnection extends EventEmitter {

if(!Sodium && !StableLib) {
try {
Sodium = require("sodium-native");
Sodium = require("@projectdysnomia/libsodium");
} catch{
try {
StableLib = require("@stablelib/xchacha20poly1305");
Expand Down Expand Up @@ -174,6 +177,9 @@ class VoiceConnection extends EventEmitter {
this.opus = {};
}

this.sendBuffer = this.#alloc(16 + 32 + MAX_FRAME_SIZE, false);
this.sendHeader = this.#alloc(12);

this.sendHeader[0] = 0x80;
this.sendHeader[1] = 0x78;

Expand Down Expand Up @@ -431,10 +437,10 @@ class VoiceConnection extends EventEmitter {
}
case VoiceOPCodes.SESSION_DESCRIPTION: {
this.mode = packet.d.mode;
this.secret = Buffer.from(packet.d.secret_key);
this.#nonceBuffer = Buffer.alloc(this.mode === "aead_aes256_gcm_rtpsize" ? 12 : 24);
this.secret = this.#transfer(Buffer.from(packet.d.secret_key));
this.#nonceBuffer = this.#alloc(this.mode === "aead_aes256_gcm_rtpsize" ? 12 : 24);
if(this.mode === "aead_xchacha20_poly1305_rtpsize" && StableLib) {
this.#stablelib = new StableLib.XChaCha20Poly1305(this.secret);
this.#stablelib = new StableLib.XChaCha20Poly1305(this.secret.buffer);
}
this.daveProtocolVersion = packet.d.dave_protocol_version;
this.connecting = false;
Expand Down Expand Up @@ -733,6 +739,10 @@ class VoiceConnection extends EventEmitter {
this.sessionID = null;
this.token = null;
this.wsSequence = -1;
this.secret?.free();
this.#nonceBuffer?.free();
this.secret = null;
this.#nonceBuffer = null;
this.#stablelib = null;
this.updateVoiceState();
/**
Expand Down Expand Up @@ -895,8 +905,8 @@ class VoiceConnection extends EventEmitter {
const hasExtension = !!(msg[0] & 0b10000);
const hasPadding = !!(msg[0] & 0b100000);
const cc = msg[0] & 0b1111;
const nonce = Buffer.alloc(this.mode === "aead_aes256_gcm_rtpsize" ? 12 : 24);
msg.copy(nonce, 0, msg.length - 4, msg.length);
const nonce = this.#alloc(this.mode === "aead_aes256_gcm_rtpsize" ? 12 : 24);
msg.copy(nonce.buffer, 0, msg.length - 4, msg.length);

// Header Size = Fixed Header Length + (CSRC Identifier Length * CSRC count) + Extension
let headerSize = 12 + (cc * 4);
Expand All @@ -906,23 +916,30 @@ class VoiceConnection extends EventEmitter {

let data;
if(this.mode === "aead_aes256_gcm_rtpsize") {
const decipher = crypto.createDecipheriv("aes-256-gcm", this.secret, nonce);
const decipher = crypto.createDecipheriv("aes-256-gcm", this.secret.buffer, nonce.buffer);
decipher.setAAD(msg.subarray(0, headerSize));
decipher.setAuthTag(msg.subarray(msg.length - 20, msg.length - 4));
data = Buffer.concat([decipher.update(msg.subarray(headerSize, msg.length - 20)), decipher.final()]);
} else if(Sodium) {
data = Buffer.alloc(msg.length - (headerSize + 4) - Sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES);
const msgPtr = this.#transfer(msg);
const d = this.#alloc(msg.length - (headerSize + 4) - Sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES);
Sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
data,
d,
null,
msg.subarray(headerSize, msg.length - 4),
msg.subarray(0, headerSize),
msgPtr.subarray(headerSize, msg.length - 4),
msgPtr.subarray(0, headerSize),
nonce,
this.secret
);

// SAFE: the native backend has no concept of freeing memory, so the buffer can be used directly
data = Sodium.native ? d.buffer : Buffer.from(d.buffer);

msgPtr.free();
d.free();
} else if(this.#stablelib) {
data = this.#stablelib.open(
nonce,
nonce.buffer,
msg.subarray(headerSize, msg.length - 4),
msg.subarray(0, headerSize)
);
Expand All @@ -933,6 +950,8 @@ class VoiceConnection extends EventEmitter {
data = Buffer.from(data.buffer);
}

nonce.free();

// RFC3550 5.1: Padding (may need testing)
if(hasPadding) {
const paddingAmount = data.subarray(0, data.length - 1);
Expand Down Expand Up @@ -1143,6 +1162,8 @@ class VoiceConnection extends EventEmitter {
}

_destroy() {
this.sendBuffer.free();
this.sendHeader.free();
if(this.opus) {
for(const key in this.opus) {
this.opus[key].delete?.();
Expand Down Expand Up @@ -1191,28 +1212,28 @@ class VoiceConnection extends EventEmitter {
}

_sendAudioFrame(unencryptedFrame) {
const headerSize = this.sendHeader.length;
this.sendHeader.writeUInt16BE(this.sequence, 2);
this.sendHeader.writeUInt32BE(this.timestamp, 4);
const headerSize = this.sendHeader.buffer.length;
this.sendHeader.buffer.writeUInt16BE(this.sequence, 2);
this.sendHeader.buffer.writeUInt32BE(this.timestamp, 4);

this.#nonce = (this.#nonce + 1) >>> 0;
this.#nonceBuffer.writeUInt32BE(this.#nonce, 0);
this.#nonceBuffer.buffer.writeUInt32BE(this.#nonce, 0);

const frame = this.#daveReady && !unencryptedFrame.equals(SILENCE_FRAME) ? this.daveSession.encryptOpus(unencryptedFrame) : unencryptedFrame;

if(this.mode === "aead_aes256_gcm_rtpsize") {
const cipher = crypto.createCipheriv("aes-256-gcm", this.secret, this.#nonceBuffer);
cipher.setAAD(this.sendHeader);
const cipher = crypto.createCipheriv("aes-256-gcm", this.secret.buffer, this.#nonceBuffer.buffer);
cipher.setAAD(this.sendHeader.buffer);
const result = Buffer.concat([cipher.update(frame), cipher.final(), cipher.getAuthTag()]);
this.sendHeader.copy(this.sendBuffer, 0, 0, headerSize);
result.copy(this.sendBuffer, headerSize);
this.#nonceBuffer.copy(this.sendBuffer, headerSize + result.length, 0, 4); // nonce padding
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + result.length + 4));
this.sendHeader.buffer.copy(this.sendBuffer.buffer, 0, 0, headerSize);
result.copy(this.sendBuffer.buffer, headerSize);
this.#nonceBuffer.buffer.copy(this.sendBuffer.buffer, headerSize + result.length, 0, 4); // nonce padding
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + result.length + 4).buffer);
} else if(Sodium) {
const ABYTES = Sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES;
const length = frame.length + ABYTES;
this.sendBuffer.fill(0, headerSize + frame.length, headerSize + frame.length + ABYTES);
frame.copy(this.sendBuffer, headerSize);
this.sendBuffer.buffer.fill(0, headerSize + frame.length, headerSize + frame.length + ABYTES);
frame.copy(this.sendBuffer.buffer, headerSize);
Sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
this.sendBuffer.subarray(headerSize, headerSize + length),
this.sendBuffer.subarray(headerSize, headerSize + frame.length),
Expand All @@ -1221,24 +1242,57 @@ class VoiceConnection extends EventEmitter {
this.#nonceBuffer,
this.secret
);
this.sendHeader.copy(this.sendBuffer, 0, 0, headerSize);
this.#nonceBuffer.copy(this.sendBuffer, headerSize + length, 0, 4); // nonce padding
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + length + 4));
this.sendHeader.buffer.copy(this.sendBuffer, 0, 0, headerSize);
this.#nonceBuffer.buffer.copy(this.sendBuffer, headerSize + length, 0, 4); // nonce padding
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + length + 4).buffer);
} else if(this.#stablelib) {
const length = frame.length + StableLib.TAG_LENGTH;
this.sendBuffer.fill(0, headerSize + frame.length, headerSize + length);
this.#stablelib.seal(
this.#nonceBuffer,
this.#nonceBuffer.buffer,
frame,
this.sendHeader,
this.sendBuffer.subarray(headerSize, headerSize + length)
this.sendHeader.buffer,
this.sendBuffer.subarray(headerSize, headerSize + length).buffer
);
this.sendHeader.copy(this.sendBuffer, 0, 0, headerSize);
this.#nonceBuffer.copy(this.sendBuffer, headerSize + length, 0, 4); // nonce padding
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + length + 4));
this.sendHeader.buffer.copy(this.sendBuffer.buffer, 0, 0, headerSize);
this.#nonceBuffer.buffer.copy(this.sendBuffer.buffer, headerSize + length, 0, 4); // nonce padding
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + length + 4).buffer);
}
}

/** @param {Buffer} buf */
#transfer(buf) {
if(Sodium) {
return Sodium.transfer(buf);
} else {
return this.#makeBufferPointer(buf);
}
}

/**
* @param {Number} size
* @param {Boolean} zero
*/
#alloc(size, zero = true) {
if(Sodium) {
return Sodium.alloc(size, zero);
} else {
return this.#makeBufferPointer(zero ? Buffer.alloc(size) : Buffer.allocUnsafe(size));
}
}

/** @param {Buffer} buf */
#makeBufferPointer(buf) {
// Keep in sync with BufferPointer in @projectdysnomia/libsodium to maintain compatibility
return {
buffer: buf,
free: () => {},
subarray: (start, end) => {
return this.#makeBufferPointer(buf.subarray(start, end));
}
};
}

[util.inspect.custom]() {
return Base.prototype[util.inspect.custom].call(this);
}
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"optionalDependencies": {
"@snazzah/davey": "^0.1.6",
"@stablelib/xchacha20poly1305": "~1.0.1",
"@projectdysnomia/libsodium": "github:projectdysnomia/libsodium#acefd1b44526953eed4f0a87e31149963d052d21",
"opusscript": "^0.1.1"
},
"browser": {
Expand All @@ -70,6 +71,7 @@
},
"peerDependencies": {
"@discordjs/opus": "^0.9.0",
"@stablelib/xchacha20poly1305": "~1.0.1",
"erlpack": "github:discord/erlpack",
"eventemitter3": "^5.0.1",
"pako": "^2.1.0",
Expand All @@ -80,6 +82,9 @@
"@discordjs/opus": {
"optional": true
},
"@stablelib/xchacha20poly1305": {
"optional": true
},
"eventemitter3": {
"optional": true
},
Expand Down