diff --git a/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/MinecraftDataHandler.java b/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/MinecraftDataHandler.java new file mode 100644 index 0000000..0d5f951 --- /dev/null +++ b/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/MinecraftDataHandler.java @@ -0,0 +1,193 @@ +package com.rtm516.mcxboxbroadcast.core.webrtc; + +import com.rtm516.mcxboxbroadcast.core.webrtc.encryption.BedrockEncryptionEncoder; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import javax.crypto.SecretKey; +import org.bouncycastle.util.encoders.Hex; +import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; +import org.cloudburstmc.protocol.bedrock.codec.BedrockCodecHelper; +import org.cloudburstmc.protocol.bedrock.data.DisconnectFailReason; +import org.cloudburstmc.protocol.bedrock.data.PacketCompressionAlgorithm; +import org.cloudburstmc.protocol.bedrock.netty.BedrockPacketWrapper; +import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec; +import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec_v3; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; +import org.cloudburstmc.protocol.bedrock.packet.DisconnectPacket; +import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; +import org.cloudburstmc.protocol.bedrock.packet.NetworkSettingsPacket; +import org.cloudburstmc.protocol.bedrock.packet.RequestNetworkSettingsPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackClientResponsePacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; +import org.cloudburstmc.protocol.bedrock.packet.TransferPacket; +import org.cloudburstmc.protocol.bedrock.util.EncryptionUtils; +import org.cloudburstmc.protocol.common.util.VarInts; +import pe.pi.sctp4j.sctp.SCTPByteStreamListener; +import pe.pi.sctp4j.sctp.SCTPStream; + +public class MinecraftDataHandler implements SCTPByteStreamListener { + private final BedrockPacketCodec packetCodec = new BedrockPacketCodec_v3(); + private final SCTPStream sctpStream; + private final BedrockCodec codec; + private final BedrockCodecHelper helper; + + private BedrockEncryptionEncoder encryptionEncoder; + + private ByteBuf concat; + private int expectedLength; + + public MinecraftDataHandler(SCTPStream sctpStream, BedrockCodec codec) { + this.sctpStream = sctpStream; + this.codec = codec; + this.helper = codec.createHelper(); + } + + @Override + public void onMessage(SCTPStream sctpStream, byte[] bytes) { + try { + System.out.println("binary message (" + sctpStream.getLabel() + "): " + Hex.toHexString(bytes)); + if (bytes.length == 0) { + throw new IllegalStateException("Expected at least 2 bytes"); + } + //todo only do this if segmentcount > 0 + var buf = Unpooled.buffer(bytes.length); + buf.writeBytes(bytes); + + byte remainingSegments = buf.readByte(); + if (concat == null) { + expectedLength = VarInts.readUnsignedInt(buf); + } + + if (remainingSegments > 0) { + if (concat == null) { + concat = buf; + } else { + concat.writeBytes(buf); + } + return; + } + + if (concat != null) { + concat.writeBytes(buf); + buf = concat; + concat = null; + } + +// if (buf.readableBytes() != expectedLength) { +// System.out.println("expected " + expectedLength + " bytes but got " + buf.readableBytes()); +// var disconnect = new DisconnectPacket(); +// disconnect.setReason(DisconnectFailReason.BAD_PACKET); +// disconnect.setKickMessage(""); +// sendPacket(disconnect, sctpStream); +// return; +// } + + var packet = readPacket(buf); + + if (!(packet instanceof LoginPacket)) { + System.out.println(packet); + } + + if (packet instanceof RequestNetworkSettingsPacket) { + var networkSettings = new NetworkSettingsPacket(); + networkSettings.setCompressionAlgorithm(PacketCompressionAlgorithm.ZLIB); + networkSettings.setCompressionThreshold(0); + sendPacket(networkSettings); + } else if (packet instanceof LoginPacket login) { +// Utils.validateAndEncryptConnection(this, login.getChain(), login.getExtra()); + + var disconnect = new DisconnectPacket(); + disconnect.setReason(DisconnectFailReason.ZOMBIE_CONNECTION); + disconnect.setKickMessage("hhhhehehe"); + sendPacket(disconnect); + +// var status = new PlayStatusPacket(); +// status.setStatus(PlayStatusPacket.Status.LOGIN_SUCCESS); +// sendPacket(status); +// +// var info = new ResourcePacksInfoPacket(); +// sendPacket(info); + } else if (packet instanceof ResourcePackClientResponsePacket status) { + switch (status.getStatus()) { + case COMPLETED -> { + var transfer = new TransferPacket(); + transfer.setAddress("test.geysermc.org"); + transfer.setPort(19132); + sendPacket(transfer); + } + case HAVE_ALL_PACKS -> { + var stack = new ResourcePackStackPacket(); + stack.setExperimentsPreviouslyToggled(false); + stack.setForcedToAccept(false); + stack.setGameVersion("*"); + sendPacket(stack); + } + default -> { + var disconnect = new DisconnectPacket(); + disconnect.setKickMessage("disconnectionScreen.resourcePack"); + sendPacket(disconnect); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void onMessage(SCTPStream sctpStream, String s) { + System.out.println("string message (" + sctpStream.getLabel() + "): " + s); + } + + @Override + public void close(SCTPStream sctpStream) { + System.out.println("stream closed: " + sctpStream.getLabel()); + } + + public void sendPacket(BedrockPacket packet) { + try { + ByteBuf dataBuf = Unpooled.buffer(128); + int packetId = codec.getPacketDefinition(packet.getClass()).getId(); + System.out.println("packet id: " + packetId); + packetCodec.encodeHeader( + dataBuf, + BedrockPacketWrapper.create(packetId, 0, 0, null, null) + ); + codec.tryEncode(helper, dataBuf, packet); + + if (encryptionEncoder != null) { + dataBuf = encryptionEncoder.encode(dataBuf); + } + + int segmentCount = (int) Math.ceil(dataBuf.readableBytes() / 10_000f); + for (int remainingSegements = segmentCount - 1; remainingSegements >= 0; remainingSegements--) { + int segmentLength = (remainingSegements == 0 ? dataBuf.readableBytes() : 10_000); + var sendBuf = Unpooled.buffer(segmentLength + 1 + 5); + sendBuf.writeByte(remainingSegements); + VarInts.writeUnsignedInt(sendBuf, segmentLength); + sendBuf.writeBytes(dataBuf, segmentLength); + + byte[] send = new byte[sendBuf.readableBytes()]; + sendBuf.readBytes(send); + System.out.println("sending: " + Hex.toHexString(send)); + sctpStream.send(send); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private BedrockPacket readPacket(ByteBuf buf) { + BedrockPacketWrapper wrapper = BedrockPacketWrapper.create(); + packetCodec.decodeHeader(buf, wrapper); + System.out.println("sender/target: " + wrapper.getSenderSubClientId() + " " + wrapper.getTargetSubClientId()); + var packet = codec.tryDecode(helper, buf.slice(), wrapper.getPacketId()); + // release it + wrapper.getHandle().recycle(wrapper); + return packet; + } + + public void enableEncryption(SecretKey secretKey) { + encryptionEncoder = new BedrockEncryptionEncoder(secretKey, EncryptionUtils.createCipher(true, true, secretKey)); + } +} diff --git a/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/SctpAssociationListener.java b/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/SctpAssociationListener.java index e6c4e7b..7e5c9b1 100644 --- a/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/SctpAssociationListener.java +++ b/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/SctpAssociationListener.java @@ -1,35 +1,11 @@ package com.rtm516.mcxboxbroadcast.core.webrtc; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import org.bouncycastle.util.encoders.Hex; -import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; -import org.cloudburstmc.protocol.bedrock.codec.BedrockCodecHelper; import org.cloudburstmc.protocol.bedrock.codec.v712.Bedrock_v712; -import org.cloudburstmc.protocol.bedrock.data.PacketCompressionAlgorithm; -import org.cloudburstmc.protocol.bedrock.netty.BedrockPacketWrapper; -import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec; -import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec_v3; -import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; -import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; -import org.cloudburstmc.protocol.bedrock.packet.NetworkSettingsPacket; -import org.cloudburstmc.protocol.bedrock.packet.RequestNetworkSettingsPacket; -import org.cloudburstmc.protocol.bedrock.util.EncryptionUtils; -import org.cloudburstmc.protocol.common.util.VarInts; import pe.pi.sctp4j.sctp.Association; import pe.pi.sctp4j.sctp.AssociationListener; -import pe.pi.sctp4j.sctp.SCTPByteStreamListener; import pe.pi.sctp4j.sctp.SCTPStream; -import java.util.HashMap; -import java.util.Map; - public class SctpAssociationListener implements AssociationListener { - private final Map streams = new HashMap<>(); - private final BedrockCodec codec = Bedrock_v712.CODEC; - private final BedrockPacketCodec packetCodec = new BedrockPacketCodec_v3(); - private final BedrockCodecHelper helper = codec.createHelper(); - @Override public void onAssociated(Association association) { System.out.println("Association associated: " + association.toString()); @@ -46,126 +22,14 @@ public void onDCEPStream(SCTPStream sctpStream, String label, int i) throws Exce return; } System.out.println("Received DCEP SCTP stream: " + sctpStream.toString()); - streams.put(sctpStream.getLabel(), sctpStream); - - sctpStream.setSCTPStreamListener(new SCTPByteStreamListener() { - private ByteBuf concat; - private int expectedLength; - - @Override - public void onMessage(SCTPStream sctpStream, byte[] bytes) { - try { - System.out.println("binary message (" + sctpStream.getLabel() + "): " + Hex.toHexString(bytes)); - if (bytes.length == 0) { - throw new IllegalStateException("Expected at least 2 bytes"); - } - //todo only do this if segmentcount > 0 - var buf = Unpooled.buffer(bytes.length); - buf.writeBytes(bytes); - - byte remainingSegments = buf.readByte(); - if (concat == null) { - expectedLength = VarInts.readUnsignedInt(buf); - } - - if (remainingSegments > 0) { - if (concat == null) { - concat = buf; - } else { - concat.writeBytes(buf); - } - return; - } - - if (concat != null) { - concat.writeBytes(buf); - buf = concat; - concat = null; - } - -// if (buf.readableBytes() != expectedLength) { -// System.out.println("expected " + expectedLength + " bytes but got " + buf.readableBytes()); -// var disconnect = new DisconnectPacket(); -// disconnect.setReason(DisconnectFailReason.BAD_PACKET); -// disconnect.setKickMessage(""); -// sendPacket(disconnect, sctpStream); -// return; -// } - - var packet = readPacket(buf); - - System.out.println(packet); - if (packet instanceof RequestNetworkSettingsPacket) { - var networkSettings = new NetworkSettingsPacket(); - networkSettings.setCompressionAlgorithm(PacketCompressionAlgorithm.ZLIB); - networkSettings.setCompressionThreshold(0); - sendPacket(networkSettings, sctpStream); - } else if (packet instanceof LoginPacket login) { - var result = EncryptionUtils.validateChain(login.getChain()); - System.out.println("signed? " + result.signed()); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Override - public void onMessage(SCTPStream sctpStream, String s) { - System.out.println("string message (" + sctpStream.getLabel() + "): " + s); - } - @Override - public void close(SCTPStream sctpStream) { - System.out.println("stream closed: " + sctpStream.getLabel()); - } - }); + if ("ReliableDataChannel".equals(label)) { + sctpStream.setSCTPStreamListener(new MinecraftDataHandler(sctpStream, Bedrock_v712.CODEC)); + } } @Override public void onRawStream(SCTPStream sctpStream) { System.out.println("Received raw SCTP stream: " + sctpStream.toString()); } - - private void sendPacket(BedrockPacket packet, String streamLabel) { - sendPacket(packet, streams.get(streamLabel)); - } - - private void sendPacket(BedrockPacket packet, SCTPStream stream) { - try { - ByteBuf dataBuf = Unpooled.buffer(128); - int packetId = codec.getPacketDefinition(packet.getClass()).getId(); - System.out.println("packet id: " + packetId); - packetCodec.encodeHeader( - dataBuf, - BedrockPacketWrapper.create(packetId, 0, 0, null, null) - ); - codec.tryEncode(helper, dataBuf, packet); - - int segmentCount = (int) Math.ceil(dataBuf.readableBytes() / 10_000f); - for (int remainingSegements = segmentCount - 1; remainingSegements >= 0; remainingSegements--) { - int segmentLength = (remainingSegements == 0 ? dataBuf.readableBytes() : 10_000); - var sendBuf = Unpooled.buffer(segmentLength + 1 + 5); - sendBuf.writeByte(remainingSegements); - VarInts.writeUnsignedInt(sendBuf, segmentLength); - sendBuf.writeBytes(dataBuf, segmentLength); - - byte[] send = new byte[sendBuf.readableBytes()]; - sendBuf.readBytes(send); - System.out.println("sending: " + Hex.toHexString(send)); - stream.send(send); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - private BedrockPacket readPacket(ByteBuf buf) { - BedrockPacketWrapper wrapper = BedrockPacketWrapper.create(); - packetCodec.decodeHeader(buf, wrapper); - System.out.println("sender/target: " + wrapper.getSenderSubClientId() + " " + wrapper.getTargetSubClientId()); - var packet = codec.tryDecode(helper, buf.slice(), wrapper.getPacketId()); - // release it - wrapper.getHandle().recycle(wrapper); - return packet; - } } \ No newline at end of file diff --git a/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/Utils.java b/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/Utils.java new file mode 100644 index 0000000..b3f5179 --- /dev/null +++ b/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/Utils.java @@ -0,0 +1,41 @@ +package com.rtm516.mcxboxbroadcast.core.webrtc; + +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.List; +import javax.crypto.SecretKey; +import org.cloudburstmc.protocol.bedrock.packet.ServerToClientHandshakePacket; +import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult; +import org.cloudburstmc.protocol.bedrock.util.EncryptionUtils; + +// https://github.com/GeyserMC/GlobalLinkServer/blob/master/src/main/java/org/geysermc/globallinkserver/util/Utils.java +public class Utils { + public static ChainValidationResult.IdentityData validateAndEncryptConnection(MinecraftDataHandler session, List certChainData, String clientDataJwt) throws Exception { + ChainValidationResult result = EncryptionUtils.validateChain(certChainData); + if (!result.signed()) { + throw new IllegalArgumentException("Chain is not signed"); + } + PublicKey identityPublicKey = result.identityClaims().parsedIdentityPublicKey(); + + byte[] clientDataPayload = EncryptionUtils.verifyClientData(clientDataJwt, identityPublicKey); + if (clientDataPayload == null) { + throw new IllegalStateException("Client data isn't signed by the given chain data"); + } + + startEncryptionHandshake(session, identityPublicKey); + + return result.identityClaims().extraData; + } + + private static void startEncryptionHandshake(MinecraftDataHandler session, PublicKey key) throws Exception { + KeyPair serverKeyPair = EncryptionUtils.createKeyPair(); + byte[] token = EncryptionUtils.generateRandomToken(); + + ServerToClientHandshakePacket packet = new ServerToClientHandshakePacket(); + packet.setJwt(EncryptionUtils.createHandshakeJwt(serverKeyPair, token)); + session.sendPacket(packet); + + SecretKey encryptionKey = EncryptionUtils.getSecretKey(serverKeyPair.getPrivate(), key, token); + session.enableEncryption(encryptionKey); + } +} diff --git a/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/encryption/BedrockEncryptionEncoder.java b/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/encryption/BedrockEncryptionEncoder.java new file mode 100644 index 0000000..64ed17e --- /dev/null +++ b/core/src/main/java/com/rtm516/mcxboxbroadcast/core/webrtc/encryption/BedrockEncryptionEncoder.java @@ -0,0 +1,71 @@ +package com.rtm516.mcxboxbroadcast.core.webrtc.encryption; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.concurrent.FastThreadLocal; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +// https://github.com/CloudburstMC/Protocol/blob/3.0/bedrock-connection/src/main/java/org/cloudburstmc/protocol/bedrock/netty/codec/encryption/BedrockEncryptionEncoder.java +public class BedrockEncryptionEncoder { + private static final FastThreadLocal DIGEST = new FastThreadLocal() { + protected MessageDigest initialValue() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (Exception var2) { + throw new AssertionError(var2); + } + } + }; + private final AtomicLong packetCounter = new AtomicLong(); + private final SecretKey key; + private final Cipher cipher; + + public ByteBuf encode(ByteBuf data) throws Exception { + ByteBuf buf = Unpooled.buffer(data.readableBytes() + 8); + + try { + ByteBuffer trailer = ByteBuffer.wrap(generateTrailer(data, this.key, this.packetCounter)); + ByteBuffer inBuffer = data.nioBuffer(); + ByteBuffer outBuffer = buf.nioBuffer(0, data.readableBytes() + 8); + int index = this.cipher.update(inBuffer, outBuffer); + index += this.cipher.update(trailer, outBuffer); + buf.writerIndex(index); + return buf.retain(); + } finally { + buf.release(); + } + + } + + static byte[] generateTrailer(ByteBuf buf, SecretKey key, AtomicLong counter) { + MessageDigest digest = DIGEST.get(); + ByteBuf counterBuf = ByteBufAllocator.DEFAULT.directBuffer(8); + + byte[] var7; + try { + counterBuf.writeLongLE(counter.getAndIncrement()); + ByteBuffer keyBuffer = ByteBuffer.wrap(key.getEncoded()); + digest.update(counterBuf.nioBuffer(0, 8)); + digest.update(buf.nioBuffer(buf.readerIndex(), buf.readableBytes())); + digest.update(keyBuffer); + byte[] hash = digest.digest(); + var7 = Arrays.copyOf(hash, 8); + } finally { + counterBuf.release(); + digest.reset(); + } + + return var7; + } + + public BedrockEncryptionEncoder(SecretKey key, Cipher cipher) { + this.key = key; + this.cipher = cipher; + } +}