Skip to content

Commit 7727c1a

Browse files
committed
refactor: use @projectdysnomia/libsodium to perform packet encryption
Not ready for production use yet. Memory WILL leak, especially when under the WASM backend. StableLib is kept as a peer dependency for those who have WASM disabled.
1 parent eb4f958 commit 7727c1a

File tree

2 files changed

+85
-35
lines changed

2 files changed

+85
-35
lines changed

lib/voice/VoiceConnection.js

Lines changed: 84 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ try {
1919
EventEmitter = require("node:events").EventEmitter;
2020
}
2121

22+
/** @type {import("@stablelib/xchacha20poly1305")?} */
2223
let StableLib = null;
24+
/** @type {import("@projectdysnomia/libsodium")?} */
2325
let Sodium = null;
26+
/** @type {import("node:crypto")?} */
2427
let crypto = null;
2528
let aes256Available = false;
2629

@@ -88,11 +91,11 @@ class VoiceConnection extends EventEmitter {
8891
ready = false;
8992
reconnecting = false;
9093
samplingRate = 48_000;
91-
sendBuffer = Buffer.allocUnsafe(16 + 32 + MAX_FRAME_SIZE);
92-
sendHeader = Buffer.alloc(12);
9394

9495
#nonce = 0;
96+
/** @type {import("@projectdysnomia/libsodium").BufferPointer?} */
9597
#nonceBuffer = null;
98+
/** @type {import("@stablelib/xchacha20poly1305").XChaCha20Poly1305?} */
9699
#stablelib = null;
97100
sequence = 0;
98101
speaking = false;
@@ -116,7 +119,7 @@ class VoiceConnection extends EventEmitter {
116119

117120
if(!Sodium && !StableLib) {
118121
try {
119-
Sodium = require("sodium-native");
122+
Sodium = require("@projectdysnomia/libsodium");
120123
} catch{
121124
try {
122125
StableLib = require("@stablelib/xchacha20poly1305");
@@ -148,6 +151,9 @@ class VoiceConnection extends EventEmitter {
148151
this.opus = {};
149152
}
150153

154+
this.sendBuffer = this.#alloc(16 + 32 + MAX_FRAME_SIZE, false);
155+
this.sendHeader = this.#alloc(12);
156+
151157
this.sendHeader[0] = 0x80;
152158
this.sendHeader[1] = 0x78;
153159

@@ -334,8 +340,8 @@ class VoiceConnection extends EventEmitter {
334340
}
335341
case VoiceOPCodes.SESSION_DESCRIPTION: {
336342
this.mode = packet.d.mode;
337-
this.secret = Buffer.from(packet.d.secret_key);
338-
this.#nonceBuffer = Buffer.alloc(this.mode === "aead_aes256_gcm_rtpsize" ? 12 : 24);
343+
this.secret = this.#transfer(Buffer.from(packet.d.secret_key));
344+
this.#nonceBuffer = this.#alloc(this.mode === "aead_aes256_gcm_rtpsize" ? 12 : 24);
339345
if(this.mode === "aead_xchacha20_poly1305_rtpsize" && StableLib) {
340346
this.#stablelib = new StableLib.XChaCha20Poly1305(this.secret);
341347
}
@@ -682,8 +688,8 @@ class VoiceConnection extends EventEmitter {
682688
const hasExtension = !!(msg[0] & 0b10000);
683689
const hasPadding = !!(msg[0] & 0b100000);
684690
const cc = msg[0] & 0b1111;
685-
const nonce = Buffer.alloc(this.mode === "aead_aes256_gcm_rtpsize" ? 12 : 24);
686-
msg.copy(nonce, 0, msg.length - 4, msg.length);
691+
const nonce = this.#alloc(this.mode === "aead_aes256_gcm_rtpsize" ? 12 : 24);
692+
msg.copy(nonce.buffer, 0, msg.length - 4, msg.length);
687693

688694
// Header Size = Fixed Header Length + (CSRC Identifier Length * CSRC count) + Extension
689695
let headerSize = 12 + (cc * 4);
@@ -693,23 +699,34 @@ class VoiceConnection extends EventEmitter {
693699

694700
let data;
695701
if(this.mode === "aead_aes256_gcm_rtpsize") {
696-
const decipher = crypto.createDecipheriv("aes-256-gcm", this.secret, nonce);
702+
const decipher = crypto.createDecipheriv("aes-256-gcm", this.secret.buffer, nonce.buffer);
697703
decipher.setAAD(msg.subarray(0, headerSize));
698704
decipher.setAuthTag(msg.subarray(msg.length - 20, msg.length - 4));
699705
data = Buffer.concat([decipher.update(msg.subarray(headerSize, msg.length - 20)), decipher.final()]);
700706
} else if(Sodium) {
701-
data = Buffer.alloc(msg.length - (headerSize + 4) - Sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES);
707+
const msgPtr = this.#transfer(msg);
708+
const d = this.#alloc(msg.length - (headerSize + 4) - Sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES);
702709
Sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
703-
data,
710+
d,
704711
null,
705-
msg.subarray(headerSize, msg.length - 4),
706-
msg.subarray(0, headerSize),
712+
msgPtr.subarray(headerSize, msg.length - 4),
713+
msgPtr.subarray(0, headerSize),
707714
nonce,
708715
this.secret
709716
);
717+
718+
if(Sodium.native) {
719+
data = d.buffer;
720+
} else {
721+
// The Buffer instance isn't guaranteed to be stable when in WASM, therefore a full copy is needed
722+
data = Buffer.from(d.buffer);
723+
}
724+
725+
msgPtr.free();
726+
d.free();
710727
} else if(this.#stablelib) {
711728
data = this.#stablelib.open(
712-
nonce,
729+
nonce.buffer,
713730
msg.subarray(headerSize, msg.length - 4),
714731
msg.subarray(0, headerSize)
715732
);
@@ -945,26 +962,26 @@ class VoiceConnection extends EventEmitter {
945962
}
946963

947964
_sendAudioFrame(frame) {
948-
const headerSize = this.sendHeader.length;
949-
this.sendHeader.writeUInt16BE(this.sequence, 2);
950-
this.sendHeader.writeUInt32BE(this.timestamp, 4);
965+
const headerSize = this.sendHeader.buffer;
966+
this.sendHeader.buffer.writeUInt16BE(this.sequence, 2);
967+
this.sendHeader.buffer.writeUInt32BE(this.timestamp, 4);
951968

952969
this.#nonce = (this.#nonce + 1) >>> 0;
953-
this.#nonceBuffer.writeUInt32BE(this.#nonce, 0);
970+
this.#nonceBuffer.buffer.writeUInt32BE(this.#nonce, 0);
954971

955972
if(this.mode === "aead_aes256_gcm_rtpsize") {
956-
const cipher = crypto.createCipheriv("aes-256-gcm", this.secret, this.#nonceBuffer);
957-
cipher.setAAD(this.sendHeader);
973+
const cipher = crypto.createCipheriv("aes-256-gcm", this.secret.buffer, this.#nonceBuffer.buffer);
974+
cipher.setAAD(this.sendHeader.buffer);
958975
const result = Buffer.concat([cipher.update(frame), cipher.final(), cipher.getAuthTag()]);
959-
this.sendHeader.copy(this.sendBuffer, 0, 0, headerSize);
960-
result.copy(this.sendBuffer, headerSize);
961-
this.#nonceBuffer.copy(this.sendBuffer, headerSize + result.length, 0, 4); // nonce padding
962-
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + result.length + 4));
976+
this.sendHeader.buffer.copy(this.sendBuffer.buffer, 0, 0, headerSize);
977+
result.copy(this.sendBuffer.buffer, headerSize);
978+
this.#nonceBuffer.buffer.copy(this.sendBuffer.buffer, headerSize + result.length, 0, 4); // nonce padding
979+
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + result.length + 4).buffer);
963980
} else if(Sodium) {
964981
const ABYTES = Sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES;
965982
const length = frame.length + ABYTES;
966-
this.sendBuffer.fill(0, headerSize + frame.length, headerSize + frame.length + ABYTES);
967-
frame.copy(this.sendBuffer, headerSize);
983+
this.sendBuffer.buffer.fill(0, headerSize + frame.length, headerSize + frame.length + ABYTES);
984+
frame.copy(this.sendBuffer.buffer, headerSize);
968985
Sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
969986
this.sendBuffer.subarray(headerSize, headerSize + length),
970987
this.sendBuffer.subarray(headerSize, headerSize + frame.length),
@@ -973,24 +990,57 @@ class VoiceConnection extends EventEmitter {
973990
this.#nonceBuffer,
974991
this.secret
975992
);
976-
this.sendHeader.copy(this.sendBuffer, 0, 0, headerSize);
977-
this.#nonceBuffer.copy(this.sendBuffer, headerSize + length, 0, 4); // nonce padding
978-
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + length + 4));
993+
this.sendHeader.buffer.copy(this.sendBuffer, 0, 0, headerSize);
994+
this.#nonceBuffer.buffer.copy(this.sendBuffer, headerSize + length, 0, 4); // nonce padding
995+
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + length + 4).buffer);
979996
} else if(this.#stablelib) {
980997
const length = frame.length + StableLib.TAG_LENGTH;
981998
this.sendBuffer.fill(0, headerSize + frame.length, headerSize + length);
982999
this.#stablelib.seal(
983-
this.#nonceBuffer,
1000+
this.#nonceBuffer.buffer,
9841001
frame,
985-
this.sendHeader,
986-
this.sendBuffer.subarray(headerSize, headerSize + length)
1002+
this.sendHeader.buffer,
1003+
this.sendBuffer.subarray(headerSize, headerSize + length).buffer
9871004
);
988-
this.sendHeader.copy(this.sendBuffer, 0, 0, headerSize);
989-
this.#nonceBuffer.copy(this.sendBuffer, headerSize + length, 0, 4); // nonce padding
990-
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + length + 4));
1005+
this.sendHeader.buffer.copy(this.sendBuffer.buffer, 0, 0, headerSize);
1006+
this.#nonceBuffer.buffer.copy(this.sendBuffer.buffer, headerSize + length, 0, 4); // nonce padding
1007+
return this.sendUDPPacket(this.sendBuffer.subarray(0, headerSize + length + 4).buffer);
9911008
}
9921009
}
9931010

1011+
/** @param {Buffer} buf */
1012+
#transfer(buf) {
1013+
if(Sodium) {
1014+
return Sodium.transfer(buf);
1015+
} else {
1016+
return this.#makeBufferPointer(buf);
1017+
}
1018+
}
1019+
1020+
/**
1021+
* @param {Number} size
1022+
* @param {Boolean} safe
1023+
*/
1024+
#alloc(size, safe = true) {
1025+
if(Sodium) {
1026+
return Sodium.alloc(size, safe);
1027+
} else {
1028+
return this.#makeBufferPointer(Buffer[safe ? "alloc" : "allocUnsafe"](size));
1029+
}
1030+
}
1031+
1032+
/** @param {Buffer} buf */
1033+
#makeBufferPointer(buf) {
1034+
// Keep in sync with BufferPointer in @projectdysnomia/libsodium to maintain compatibility
1035+
return {
1036+
buffer: buf,
1037+
free: () => {},
1038+
subarray: (start, end) => {
1039+
return this.#makeBufferPointer(buf.subarray(start, end));
1040+
}
1041+
};
1042+
}
1043+
9941044
[util.inspect.custom]() {
9951045
return Base.prototype[util.inspect.custom].call(this);
9961046
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"typescript-eslint": "^8.6.0"
5656
},
5757
"optionalDependencies": {
58-
"@stablelib/xchacha20poly1305": "~1.0.1",
58+
"@projectdysnomia/libsodium": "github:projectdysnomia/libsodium#acefd1b44526953eed4f0a87e31149963d052d21",
5959
"opusscript": "^0.1.1"
6060
},
6161
"browser": {

0 commit comments

Comments
 (0)