From 4d6cca72f44f80235a97664554b47d3b5195fc9d Mon Sep 17 00:00:00 2001 From: qeled Date: Fri, 18 Mar 2016 06:32:35 +0300 Subject: [PATCH] Implement voice encryption support --- lib/Voice/VoiceConnection.js | 25 ++++++++++++++++++-- lib/Voice/VoicePacket.js | 46 +++++++++++++++++++++++++----------- package.json | 1 + src/Voice/VoiceConnection.js | 25 ++++++++++++++++++-- src/Voice/VoicePacket.js | 23 ++++++++++++++---- 5 files changed, 97 insertions(+), 23 deletions(-) diff --git a/lib/Voice/VoiceConnection.js b/lib/Voice/VoiceConnection.js index f9f642fda..6ca2b2573 100644 --- a/lib/Voice/VoiceConnection.js +++ b/lib/Voice/VoiceConnection.js @@ -51,6 +51,9 @@ var _unpipe = require("unpipe"); var _unpipe2 = _interopRequireDefault(_unpipe); +var MODE_xsalsa20_poly1305 = "xsalsa20_poly1305"; +var MODE_plain = "plain"; + var VoiceConnection = (function (_EventEmitter) { _inherits(VoiceConnection, _EventEmitter); @@ -77,6 +80,10 @@ var VoiceConnection = (function (_EventEmitter) { this.KAI = null; this.timestamp = 0; this.sequence = 0; + + this.mode = null; + this.secret = null; + this.volume = new _VolumeTransformer2["default"](); this.init(); } @@ -232,7 +239,7 @@ var VoiceConnection = (function (_EventEmitter) { } var buffer = self.encoder.opusBuffer(rawbuffer); - var packet = new _VoicePacket2["default"](buffer, sequence, timestamp, self.vWSData.ssrc); + var packet = new _VoicePacket2["default"](buffer, sequence, timestamp, self.vWSData.ssrc, self.secret); return self.sendPacket(packet, callback); } catch (e) { self.playing = false; @@ -361,6 +368,13 @@ var VoiceConnection = (function (_EventEmitter) { } discordPort = msg.readUIntLE(msg.length - 2, 2).toString(10); + var modes = self.vWSData.modes; + var mode = MODE_xsalsa20_poly1305; + if (modes.indexOf(MODE_xsalsa20_poly1305) < 0) { + mode = MODE_plain; + self.client.emit("debug", "Encrypted mode not reported as supported by the server, using 'plain'"); + } + vWS.send(JSON.stringify({ "op": 1, "d": { @@ -368,7 +382,7 @@ var VoiceConnection = (function (_EventEmitter) { "data": { "address": discordIP, "port": Number(discordPort), - "mode": self.vWSData.modes[0] //Plain + "mode": mode } } })); @@ -410,6 +424,13 @@ var VoiceConnection = (function (_EventEmitter) { }); break; case 4: + if (data.d.secret_key && data.d.secret_key.length > 0) { + var buffer = new ArrayBuffer(data.d.secret_key.length); + self.secret = new Uint8Array(buffer); + for (var i = 0; i < _this4.secret.length; i++) { + self.secret[i] = data.d.secret_key[i]; + } + } self.ready = true; self.mode = data.d.mode; diff --git a/lib/Voice/VoicePacket.js b/lib/Voice/VoicePacket.js index 445f9d6c4..3de6f05e9 100644 --- a/lib/Voice/VoicePacket.js +++ b/lib/Voice/VoicePacket.js @@ -2,27 +2,45 @@ exports.__esModule = true; +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } -var VoicePacket = function VoicePacket(data, sequence, time, ssrc) { - _classCallCheck(this, VoicePacket); +var _tweetnacl = require("tweetnacl"); - var audioBuffer = data, - returnBuffer = new Buffer(audioBuffer.length + 12); +var _tweetnacl2 = _interopRequireDefault(_tweetnacl); - returnBuffer.fill(0); - returnBuffer[0] = 0x80; - returnBuffer[1] = 0x78; +var nonce = new Buffer(24); +nonce.fill(0); - returnBuffer.writeUIntBE(sequence, 2, 2); - returnBuffer.writeUIntBE(time, 4, 4); - returnBuffer.writeUIntBE(ssrc, 8, 4); +var VoicePacket = function VoicePacket(data, sequence, time, ssrc, secret) { + _classCallCheck(this, VoicePacket); - for (var i = 0; i < audioBuffer.length; i++) { - returnBuffer[i + 12] = audioBuffer[i]; - } + var mac = secret ? 16 : 0; + var packetLength = data.length + 12 + mac; - return returnBuffer; + var audioBuffer = data; + var returnBuffer = new Buffer(packetLength); + + returnBuffer.fill(0); + returnBuffer[0] = 0x80; + returnBuffer[1] = 0x78; + + returnBuffer.writeUIntBE(sequence, 2, 2); + returnBuffer.writeUIntBE(time, 4, 4); + returnBuffer.writeUIntBE(ssrc, 8, 4); + + if (secret) { + // copy first 12 bytes + returnBuffer.copy(nonce, 0, 0, 12); + audioBuffer = _tweetnacl2["default"].secretbox(data, nonce, secret); + } + + for (var i = 0; i < audioBuffer.length; i++) { + returnBuffer[i + 12] = audioBuffer[i]; + } + + return returnBuffer; }; exports["default"] = VoicePacket; diff --git a/package.json b/package.json index 00ba71e7c..e2e45d0d2 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "homepage": "https://github.com/hydrabolt/discord.js#readme", "dependencies": { "superagent": "^1.8.0", + "tweetnacl": "^0.14.1", "unpipe": "^1.0.0", "ws": "^1.0.1" }, diff --git a/src/Voice/VoiceConnection.js b/src/Voice/VoiceConnection.js index 16f39cf70..89498e8b9 100644 --- a/src/Voice/VoiceConnection.js +++ b/src/Voice/VoiceConnection.js @@ -17,6 +17,9 @@ import StreamIntent from "./StreamIntent"; import EventEmitter from "events"; import unpipe from "unpipe"; +const MODE_xsalsa20_poly1305 = "xsalsa20_poly1305"; +const MODE_plain = "plain"; + export default class VoiceConnection extends EventEmitter { constructor(channel, client, session, token, server, endpoint) { super(); @@ -39,6 +42,10 @@ export default class VoiceConnection extends EventEmitter { this.KAI = null; this.timestamp = 0; this.sequence = 0; + + this.mode = null; + this.secret = null; + this.volume = new VolumeTransformer(); this.init(); } @@ -196,7 +203,7 @@ export default class VoiceConnection extends EventEmitter { } var buffer = self.encoder.opusBuffer(rawbuffer); - var packet = new VoicePacket(buffer, sequence, timestamp, self.vWSData.ssrc); + var packet = new VoicePacket(buffer, sequence, timestamp, self.vWSData.ssrc, self.secret); return self.sendPacket(packet, callback); } catch (e) { @@ -314,6 +321,13 @@ export default class VoiceConnection extends EventEmitter { } discordPort = msg.readUIntLE(msg.length - 2, 2).toString(10); + var modes = self.vWSData.modes; + var mode = MODE_xsalsa20_poly1305; + if (modes.indexOf(MODE_xsalsa20_poly1305) < 0) { + mode = MODE_plain; + self.client.emit("debug", "Encrypted mode not reported as supported by the server, using 'plain'"); + } + vWS.send(JSON.stringify({ "op": 1, "d": { @@ -321,7 +335,7 @@ export default class VoiceConnection extends EventEmitter { "data": { "address": discordIP, "port": Number(discordPort), - "mode": self.vWSData.modes[0] //Plain + "mode": mode } } })); @@ -365,6 +379,13 @@ export default class VoiceConnection extends EventEmitter { }); break; case 4: + if (data.d.secret_key && data.d.secret_key.length > 0) { + const buffer = new ArrayBuffer(data.d.secret_key.length); + self.secret = new Uint8Array(buffer); + for (let i = 0; i < this.secret.length; i++) { + self.secret[i] = data.d.secret_key[i]; + } + } self.ready = true; self.mode = data.d.mode; diff --git a/src/Voice/VoicePacket.js b/src/Voice/VoicePacket.js index 7e76095d3..d3bed4b16 100644 --- a/src/Voice/VoicePacket.js +++ b/src/Voice/VoicePacket.js @@ -1,10 +1,17 @@ "use strict"; -export default class VoicePacket { - constructor(data, sequence, time, ssrc){ +import nacl from "tweetnacl"; - var audioBuffer = data, - returnBuffer = new Buffer(audioBuffer.length + 12); +const nonce = new Buffer(24); +nonce.fill(0); + +export default class VoicePacket { + constructor(data, sequence, time, ssrc, secret){ + var mac = secret ? 16 : 0; + var packetLength = data.length + 12 + mac; + + var audioBuffer = data; + var returnBuffer = new Buffer(packetLength); returnBuffer.fill(0); returnBuffer[0] = 0x80; @@ -14,7 +21,13 @@ export default class VoicePacket { returnBuffer.writeUIntBE(time, 4, 4); returnBuffer.writeUIntBE(ssrc, 8, 4); - for (var i=0; i