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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ dependencies {

- Supports voice gateway v4, v5 and v8.
- Easily extendable for stuff such as support for codecs other than Opus or video sending, if Discord ever decides to support it on bots.
- Experimental video support.
- Basic RTCP support for measuring packet loss and other stuff.
- Support for most encryption modes used by Discord.
- Full support for Discord Audio & Video End-to-End Encryption (DAVE) Protocol.

#### Non-goals / won't do

Expand All @@ -49,3 +49,5 @@ Koe includes modified/stripped-down parts based on following open-source project

- [tweetnacl-java](https://github.com/InstantWebP2P/tweetnacl-java) (Poly1305, SecretBox)
- [nanojson](https://github.com/mmastrac/nanojson) (modified for bytebuf support, changed the API a bit and etc.)
- [BouncyCastle MLS](https://github.com/bcgit/bc-java/tree/1.79) (see README.md in mls module for more info)
- [libdave](https://github.com/discord/libdave) (Koe's DAVE implementation is heavily based on reference C++ implementation)
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ def getGitVersion() {
return [versionStr.toString().trim(), true]
}

ext {
nettyVersion = '4.1.112.Final'
slf4jVersion = '1.8.0-beta4'
tinkVersion = '1.15.0'
bouncyCastleVersion = '1.79'
jetbrainsAnnotationsVersion = '13.0'
}

subprojects {
apply plugin: 'maven-publish'
apply plugin: 'java-library'
Expand Down
12 changes: 6 additions & 6 deletions core/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
dependencies {
api group: 'io.netty', name: 'netty-transport', version: '4.1.112.Final'
implementation group: 'io.netty', name: 'netty-codec-http', version: '4.1.112.Final'
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.112.Final', classifier: 'linux-x86_64'
implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.8.0-beta4'
implementation group: 'com.google.crypto.tink', name: 'tink', version: '1.14.1'
compileOnly group: 'org.jetbrains', name: 'annotations', version: '13.0'
api group: 'io.netty', name: 'netty-transport', version: "$nettyVersion"
implementation group: 'io.netty', name: 'netty-codec-http', version: "$nettyVersion"
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: "$nettyVersion", classifier: 'linux-x86_64'
implementation group: 'org.slf4j', name: 'slf4j-api', version: "$slf4jVersion"
implementation group: 'com.google.crypto.tink', name: 'tink', version: "$tinkVersion"
compileOnly group: 'org.jetbrains', name: 'annotations', version: "$jetbrainsAnnotationsVersion"
}
4 changes: 4 additions & 0 deletions core/src/main/java/moe/kyokobot/koe/crypto/E2ESession.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package moe.kyokobot.koe.crypto;

public class E2ESession {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package moe.kyokobot.koe.crypto;

public interface KoeE2EKeyManager {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package moe.kyokobot.koe.gateway;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
Expand Down Expand Up @@ -122,6 +123,10 @@ public boolean isOpen() {

protected abstract void handlePayload(JsonObject object);

protected void handleBinaryPayload(ByteBuf buffer) {
// no-op
}

protected void onClose(int code, @Nullable String reason, boolean remote) {
if (!closed) {
closed = true;
Expand Down Expand Up @@ -166,14 +171,29 @@ public void sendInternalPayload(int op, Object d) {
sendRaw(new JsonObject().add("op", op).add("d", d));
}

public void sendBinaryInternalPayload(char op, ByteBuf buffer) {
var frame = channel.alloc().buffer(1 + buffer.readableBytes());
frame.writeByte(op);
frame.writeBytes(buffer);
sendRaw(frame);
buffer.release();
}

protected void sendRaw(JsonObject object) {
if (channel != null && channel.isOpen()) {
var data = object.toString();
logger.trace("<- {}", data);
logger.trace("<-T {}", data);
channel.writeAndFlush(new TextWebSocketFrame(data));
}
}

protected void sendRaw(ByteBuf buffer) {
if (channel != null && channel.isOpen()) {
logger.trace("<-B {}", buffer);
channel.writeAndFlush(new BinaryWebSocketFrame(buffer));
}
}

private class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {
private final WebSocketClientHandshaker handshaker;

Expand Down Expand Up @@ -228,9 +248,14 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Except
if (msg instanceof TextWebSocketFrame) {
var frame = (TextWebSocketFrame) msg;
var object = JsonParser.object().from(frame.content());
logger.trace("-> {}", object);
logger.trace("->T {}", object);
frame.release();
handlePayload(object);
} else if (msg instanceof BinaryWebSocketFrame) {
var frame = (BinaryWebSocketFrame) msg;
logger.trace("->B {}", frame.content());
handleBinaryPayload(frame.content());
frame.release();
} else if (msg instanceof CloseWebSocketFrame) {
var frame = (CloseWebSocketFrame) msg;
if (logger.isDebugEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package moe.kyokobot.koe.gateway;

import io.netty.buffer.ByteBuf;
import moe.kyokobot.koe.VoiceServerInfo;
import moe.kyokobot.koe.codec.Codec;
import moe.kyokobot.koe.codec.DefaultCodecs;
Expand Down Expand Up @@ -53,7 +54,9 @@ protected void identify() {
.addAsString("user_id", connection.getClient().getClientId())
.add("session_id", voiceServerInfo.getSessionId())
.add("token", voiceServerInfo.getToken())
.add("video", true));
.add("video", true)
.add("max_dave_protocol_version", 0)
.add("streams", new JsonArray()));
}

@Override
Expand Down Expand Up @@ -163,6 +166,13 @@ protected void handlePayload(JsonObject object) {
}
}

@Override
protected void handleBinaryPayload(ByteBuf buffer) {
sequence = buffer.readUnsignedShort();

var op = buffer.readUnsignedByte();
}

@Override
protected void onClose(int code, @Nullable String reason, boolean remote) {
super.onClose(code, reason, remote);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void handleSessionDescription(JsonObject object) {

if (encryptionMode == null) {
throw new IllegalStateException("Encryption mode selected by Discord is not supported by Koe or the " +
"protocol changed! Open an issue at https://github.com/KyokoBot/koe");
"protocol changed! Update to latest version or open an issue at https://github.com/KyokoBot/koe");
}

var keyArray = object.getArray("secret_key");
Expand Down
7 changes: 7 additions & 0 deletions dave/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dependencies {
api group: 'io.netty', name: 'netty-buffer', version: "$nettyVersion"
implementation project(':mls')
implementation group: 'org.slf4j', name: 'slf4j-api', version: "$slf4jVersion"
compileOnly group: 'org.jetbrains', name: 'annotations', version: "$jetbrainsAnnotationsVersion"
}

11 changes: 11 additions & 0 deletions dave/src/main/java/moe/kyokobot/koe/dave/DAVEException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package moe.kyokobot.koe.dave;

public class DAVEException extends Exception {
public DAVEException(String message) {
super(message);
}

public DAVEException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package moe.kyokobot.koe.dave;

import io.netty.buffer.ByteBuf;

public class InboundFrameProcessor {
private boolean isEncrypted;
private int originalSize;
private int truncatedNonce;
private ByteBuf buffer;
}
7 changes: 7 additions & 0 deletions dave/src/main/java/moe/kyokobot/koe/dave/KeyRatchet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package moe.kyokobot.koe.dave;

public interface KeyRatchet {
byte[] getKey(int keyGeneration);

void deleteKey(int keyGeneration);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package moe.kyokobot.koe.dave;

import org.bouncycastle.crypto.AsymmetricCipherKeyPair;

public interface PersistentKeyManager {
AsymmetricCipherKeyPair getKeyPair(String signingKeyId, int protocolVersion);
}
36 changes: 36 additions & 0 deletions dave/src/main/java/moe/kyokobot/koe/dave/mls/MLSKeyRatchet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package moe.kyokobot.koe.dave.mls;

import moe.kyokobot.koe.dave.KeyRatchet;
import moe.kyokobot.koe.mls.HashRatchet;
import moe.kyokobot.koe.mls.crypto.MlsCipherSuite;
import moe.kyokobot.koe.mls.crypto.Secret;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MLSKeyRatchet implements KeyRatchet {
private static final Logger logger = LoggerFactory.getLogger(MLSKeyRatchet.class);

private final HashRatchet hashRatchet;

public MLSKeyRatchet(MlsCipherSuite suite, byte[] baseSecret) {
hashRatchet = new HashRatchet(suite, new Secret(baseSecret));
}

@Override
public byte[] getKey(int keyGeneration) {
logger.info("Retrieving key for generation {} from HashRatchet", keyGeneration);

try {
var keyAndNonce = hashRatchet.get(keyGeneration);
return keyAndNonce.key;
} catch (Exception e) {
logger.error("Failed to retrieve key for generation {}: {}", keyGeneration, e.getMessage());
return null;
}
}

@Override
public void deleteKey(int keyGeneration) {
hashRatchet.erase(keyGeneration);
}
}
Loading