diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index cc5f389..36588d3 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -22,7 +22,15 @@ class SingleSilence extends Silence { } } -const SUPPORTED_MODES = ['aead_aes256_gcm_rtpsize', 'aead_xchacha20_poly1305_rtpsize']; +const SUPPORTED_MODES = [ + // Avalible (but only voice working) + // 'aead_aes256_gcm_rtpsize', + // 'aead_xchacha20_poly1305_rtpsize', + // Deprecated + 'xsalsa20_poly1305_lite', + 'xsalsa20_poly1305_suffix', + 'xsalsa20_poly1305', +]; const SUPPORTED_CODECS = ['VP8', 'H264']; /** diff --git a/src/client/voice/dispatcher/BaseDispatcher.js b/src/client/voice/dispatcher/BaseDispatcher.js index 2622ec8..65e638a 100644 --- a/src/client/voice/dispatcher/BaseDispatcher.js +++ b/src/client/voice/dispatcher/BaseDispatcher.js @@ -9,9 +9,7 @@ const kill = require('tree-kill'); const secretbox = require('../util/Secretbox'); const CHANNELS = 2; - const MAX_NONCE_SIZE = 2 ** 32 - 1; -const nonce = Buffer.alloc(24); /** * @external WritableStream @@ -36,10 +34,7 @@ class BaseDispatcher extends Writable { this.extensionEnabled = extensionEnabled; this._nonce = 0; - this._nonceBuffer = - this.player.voiceConnection.authentication.mode === 'aead_aes256_gcm_rtpsize' - ? Buffer.alloc(12) - : Buffer.alloc(24); + this._nonceBuffer = null; /** * The time that the stream was paused at (null if not paused) @@ -89,6 +84,13 @@ class BaseDispatcher extends Writable { }); } + resetNonceBuffer() { + this._nonceBuffer = + this.player.voiceConnection.authentication.mode === 'aead_aes256_gcm_rtpsize' + ? Buffer.alloc(12) + : Buffer.alloc(24); + } + get TIMESTAMP_INC() { return this.extensionEnabled ? 90000 / this.fps : 480 * CHANNELS; } @@ -111,7 +113,7 @@ class BaseDispatcher extends Writable { getNewSequence() { const currentSeq = this.sequence; this.sequence++; - if (this.sequence >= 2 ** 16) this.sequence = 0; + if (this.sequence > 2 ** 16 - 1) this.sequence = 0; return currentSeq; } @@ -307,6 +309,9 @@ class BaseDispatcher extends Writable { // Both supported encryption methods want the nonce to be an incremental integer this._nonce++; if (this._nonce > MAX_NONCE_SIZE) this._nonce = 0; + if (!this._nonceBuffer) { + this.resetNonceBuffer(); + } this._nonceBuffer.writeUInt32BE(this._nonce, 0); // 4 extra bytes of padding on the end of the encrypted packet @@ -333,6 +338,16 @@ class BaseDispatcher extends Writable { return [encrypted, noncePadding]; } + case 'xsalsa20_poly1305_lite': { + return [secretbox.methods.close(buffer, this._nonceBuffer, secret_key), noncePadding]; + } + case 'xsalsa20_poly1305_suffix': { + const random = secretbox.methods.random(24); + return [secretbox.methods.close(buffer, random, secret_key), random]; + } + case 'xsalsa20_poly1305': { + return [secretbox.methods.close(buffer, Buffer.concat([additionalData, Buffer.alloc(12)]), secret_key)]; + } default: { // This should never happen. Our encryption mode is chosen from a list given to us by the gateway and checked with the ones we support. throw new RangeError(`Unsupported encryption method: ${mode}`); @@ -342,9 +357,9 @@ class BaseDispatcher extends Writable { _createPacket(buffer, isLastPacket = false) { // Header - const rtpHeader = Buffer.alloc(12); - rtpHeader[0] = this.extensionEnabled ? 0x90 : 0x80; // 0b10000000 | ((this.extensionEnabled ? 1 : 0) << 4); - rtpHeader[1] = this.payloadType; + const rtpHeader = Buffer.alloc(12); // RTP_HEADER_SIZE + rtpHeader[0] = this.extensionEnabled ? 0x90 : 0x80; // Version + Flags (1 byte) + rtpHeader[1] = this.payloadType; // Payload Type (1 byte) if (this.extensionEnabled) { if (isLastPacket) { @@ -356,8 +371,6 @@ class BaseDispatcher extends Writable { rtpHeader.writeUIntBE(this.timestamp, 4, 4); rtpHeader.writeUIntBE(this.player.voiceConnection.authentication.ssrc + this.extensionEnabled, 8, 4); - rtpHeader.copy(nonce, 0, 0, 12); - return Buffer.concat([rtpHeader, ...this._encrypt(buffer, rtpHeader)]); } diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 6d0420e..289bdb6 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -56,6 +56,14 @@ class PacketHandler extends EventEmitter { return stream; } + makeTestVideoStream(user) { + if (this.videoStreams.has(user)) return this.videoStreams.get(user); + const stream = new Readable(); + stream.on('end', () => this.videoStreams.delete(user)); + this.videoStreams.set(user, stream); + return stream; + } + makeVideoStream(user) { if (this.videoStreams.has(user)) return this.videoStreams.get(user); const stream = new IvfJoinner('VP8'); // Test VP8 ok @@ -66,10 +74,11 @@ class PacketHandler extends EventEmitter { parseBuffer(buffer) { const { secret_key, mode } = this.receiver.connection.authentication; + // Open packet + if (!secret_key) return new Error('secret_key cannot be null or undefined'); if (!this.nonce) { this.resetNonce(); } - // Copy the last 4 bytes of unpadded nonce to the padding of (12 - 4) or (24 - 4) bytes buffer.copy(this.nonce, 0, buffer.length - UNPADDED_NONCE_LENGTH); @@ -109,13 +118,31 @@ class PacketHandler extends EventEmitter { packet = Buffer.from(packet); break; } + case 'xsalsa20_poly1305_lite': { + packet = secretbox.methods.open(buffer.slice(12, buffer.length - 4), this.nonce, secret_key); + break; + } + + case 'xsalsa20_poly1305_suffix': { + packet = secretbox.methods.open(buffer.slice(12, buffer.length - 24), this.nonce, secret_key); + break; + } + + case 'xsalsa20_poly1305': { + packet = secretbox.methods.open(buffer.slice(12), this.nonce, secret_key); + break; + } default: { throw new RangeError(`Unsupported decryption method: ${mode}`); } } - // Strip decrypted RTP Header Extension if present - if (buffer.slice(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) { + // Strip RTP Header Extensions (one-byte only) + if (packet[0] === 0xbe && packet[1] === 0xde) { + const headerExtensionLength = packet.readUInt16BE(2); + packet = packet.subarray(4 + 4 * headerExtensionLength); + } else if (buffer.slice(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) { + // Strip decrypted RTP Header Extension if present const headerExtensionLength = buffer.slice(14).readUInt16BE(); packet = packet.subarray(4 * headerExtensionLength); } diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index bd7e9c2..e359f77 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -77,6 +77,13 @@ class VoiceReceiver extends EventEmitter { const stream = this.packets.makeVideoStream(user.id); return stream; } + + createTestVideoStream(user) { + user = this.connection.client.users.resolve(user); + if (!user) throw new Error('VOICE_USER_MISSING'); + const stream = this.packets.makeTestVideoStream(user.id); + return stream; + } } module.exports = VoiceReceiver;