From fc5b0d35779541022760e2620d1cda84d5e9fec2 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Fri, 27 Jul 2018 00:10:09 -0400 Subject: [PATCH] Online-mode and encryption support --- .../velocitypowered/proxy/VelocityServer.java | 21 ++++- .../proxy/connection/MinecraftConnection.java | 20 ++++- .../connection/MinecraftSessionHandler.java | 2 +- .../connection/backend/ServerConnection.java | 7 +- .../connection/client/ConnectedPlayer.java | 17 ++-- .../client/LoginSessionHandler.java | 62 ++++++++++++-- .../client/StatusSessionHandler.java | 6 +- .../connection/http/NettyHttpClient.java | 63 ++++++++++++++ .../http/SimpleHttpResponseCollector.java | 41 +++++++++ .../proxy/data/GameProfile.java | 84 +++++++++++++++++++ .../encryption/JavaVelocityCipher.java | 43 ++++++++++ .../protocol/encryption/VelocityCipher.java | 10 +++ .../encryption/VelocityEncryptor.java | 8 -- .../netty/MinecraftCipherDecoder.java | 29 +++++++ .../netty/MinecraftCipherEncoder.java | 20 +++++ .../protocol/packets/EncryptionResponse.java | 28 ++++++- .../proxy/util/EncryptionUtils.java | 19 +++++ 17 files changed, 443 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/velocitypowered/proxy/connection/http/NettyHttpClient.java create mode 100644 src/main/java/com/velocitypowered/proxy/connection/http/SimpleHttpResponseCollector.java create mode 100644 src/main/java/com/velocitypowered/proxy/data/GameProfile.java create mode 100644 src/main/java/com/velocitypowered/proxy/protocol/encryption/JavaVelocityCipher.java create mode 100644 src/main/java/com/velocitypowered/proxy/protocol/encryption/VelocityCipher.java delete mode 100644 src/main/java/com/velocitypowered/proxy/protocol/encryption/VelocityEncryptor.java create mode 100644 src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherDecoder.java create mode 100644 src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherEncoder.java diff --git a/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 00226aef0..5d0ebd180 100644 --- a/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -1,6 +1,10 @@ package com.velocitypowered.proxy; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.http.NettyHttpClient; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftPipelineUtils; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; @@ -11,16 +15,22 @@ import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import net.kyori.text.Component; +import net.kyori.text.serializer.GsonComponentSerializer; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; public class VelocityServer { + public static final Gson GSON = new GsonBuilder() + .registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer()) + .create(); private static VelocityServer server; private EventLoopGroup bossGroup; private EventLoopGroup childGroup; + private NettyHttpClient httpClient; private KeyPair serverKeyPair; public VelocityServer() { @@ -46,12 +56,15 @@ public class VelocityServer { } // Start the listener - bossGroup = new NioEventLoopGroup(); - childGroup = new NioEventLoopGroup(); + bossGroup = new NioEventLoopGroup(0, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Netty Boss Thread").build()); + childGroup = new NioEventLoopGroup(0, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Netty I/O Thread #%d").build()); + httpClient = new NettyHttpClient(this); server = this; new ServerBootstrap() .channel(NioServerSocketChannel.class) .group(bossGroup, childGroup) + .childOption(ChannelOption.TCP_NODELAY, true) + .childOption(ChannelOption.IP_TOS, 0x18) .childHandler(new ChannelInitializer() { @Override protected void initChannel(Channel ch) throws Exception { @@ -82,4 +95,8 @@ public class VelocityServer { .channel(NioSocketChannel.class) .group(childGroup); } + + public NettyHttpClient getHttpClient() { + return httpClient; + } } diff --git a/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index 17c8f9c06..c54d27047 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -1,9 +1,12 @@ package com.velocitypowered.proxy.connection; import com.google.common.base.Preconditions; +import com.velocitypowered.proxy.Velocity; import com.velocitypowered.proxy.protocol.PacketWrapper; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.compression.JavaVelocityCompressor; +import com.velocitypowered.proxy.protocol.encryption.JavaVelocityCipher; +import com.velocitypowered.proxy.protocol.encryption.VelocityCipher; import com.velocitypowered.proxy.protocol.netty.*; import com.velocitypowered.proxy.protocol.packets.SetCompression; import io.netty.channel.Channel; @@ -12,8 +15,12 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.util.ReferenceCountUtil; -import static com.velocitypowered.proxy.protocol.netty.MinecraftPipelineUtils.MINECRAFT_DECODER; -import static com.velocitypowered.proxy.protocol.netty.MinecraftPipelineUtils.MINECRAFT_ENCODER; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import java.security.GeneralSecurityException; + +import static com.velocitypowered.proxy.protocol.netty.MinecraftPipelineUtils.*; /** * A utility class to make working with the pipeline a little less painful and transparently handles certain Minecraft @@ -156,4 +163,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { channel.pipeline().addBefore(MINECRAFT_DECODER, "compress-decoder", decoder); channel.pipeline().addBefore(MINECRAFT_ENCODER, "compress-encoder", encoder); } + + public void enableEncryption(byte[] secret) throws GeneralSecurityException { + SecretKey key = new SecretKeySpec(secret, "AES"); + + VelocityCipher decryptionCipher = new JavaVelocityCipher(false, key); + VelocityCipher encryptionCipher = new JavaVelocityCipher(true, key); + channel.pipeline().addBefore(FRAME_DECODER, "cipher-decoder", new MinecraftCipherDecoder(decryptionCipher)); + channel.pipeline().addBefore(FRAME_ENCODER, "cipher-encoder", new MinecraftCipherEncoder(encryptionCipher)); + } } diff --git a/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index b2eee3ff7..96d80b021 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -4,7 +4,7 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import io.netty.buffer.ByteBuf; public interface MinecraftSessionHandler { - void handle(MinecraftPacket packet); + void handle(MinecraftPacket packet) throws Exception; default void handleUnknown(ByteBuf buf) { // No-op: we'll release the buffer later. diff --git a/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java b/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java index e754416dd..3ca2f3c14 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java +++ b/src/main/java/com/velocitypowered/proxy/connection/backend/ServerConnection.java @@ -8,7 +8,6 @@ import com.velocitypowered.proxy.data.ServerInfo; import com.velocitypowered.proxy.protocol.netty.MinecraftPipelineUtils; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.util.UuidUtils; import io.netty.channel.*; public class ServerConnection { @@ -56,12 +55,10 @@ public class ServerConnection { // BungeeCord IP forwarding is simply a special injection after the "address" in the handshake, // separated by \0 (the null byte). In order, you send the original host, the player's IP, their // UUID (undashed), and if you are in online-mode, their login properties (retrieved from Mojang). - // - // Velocity doesn't yet support online-mode, unfortunately. That will come soon. return serverInfo.getAddress().getHostString() + "\0" + proxyPlayer.getRemoteAddress().getHostString() + "\0" + - UuidUtils.toUndashed(proxyPlayer.getUniqueId()) + "\0" + - "[]"; + proxyPlayer.getProfile().getId() + "\0" + + VelocityServer.GSON.toJson(proxyPlayer.getProfile().getProperties()); } private void startHandshake() { diff --git a/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 385206b2b..9a9c8e026 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -1,5 +1,6 @@ package com.velocitypowered.proxy.connection.client; +import com.velocitypowered.proxy.data.GameProfile; import com.velocitypowered.proxy.protocol.packets.Chat; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.backend.ServerConnection; @@ -14,23 +15,25 @@ import java.net.InetSocketAddress; import java.util.UUID; public class ConnectedPlayer { - private final String username; - private final UUID uniqueId; + private final GameProfile profile; private final MinecraftConnection connection; private ServerConnection connectedServer; - public ConnectedPlayer(String username, UUID uniqueId, MinecraftConnection connection) { - this.username = username; - this.uniqueId = uniqueId; + public ConnectedPlayer(GameProfile profile, MinecraftConnection connection) { + this.profile = profile; this.connection = connection; } public String getUsername() { - return username; + return profile.getName(); } public UUID getUniqueId() { - return uniqueId; + return profile.idAsUuid(); + } + + public GameProfile getProfile() { + return profile; } public MinecraftConnection getConnection() { diff --git a/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java b/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java index 3831b1a4f..e94d88fc3 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java +++ b/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java @@ -1,9 +1,11 @@ package com.velocitypowered.proxy.connection.client; import com.google.common.base.Preconditions; +import com.velocitypowered.proxy.data.GameProfile; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packets.EncryptionRequest; +import com.velocitypowered.proxy.protocol.packets.EncryptionResponse; import com.velocitypowered.proxy.protocol.packets.ServerLogin; import com.velocitypowered.proxy.protocol.packets.ServerLoginSuccess; import com.velocitypowered.proxy.connection.MinecraftConnection; @@ -11,25 +13,72 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.backend.ServerConnection; import com.velocitypowered.proxy.data.ServerInfo; +import com.velocitypowered.proxy.util.EncryptionUtils; import com.velocitypowered.proxy.util.UuidUtils; import java.net.InetSocketAddress; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.Arrays; import java.util.concurrent.ThreadLocalRandom; public class LoginSessionHandler implements MinecraftSessionHandler { + private static final String MOJANG_SERVER_AUTH_URL = + "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s"; + private final MinecraftConnection inbound; private ServerLogin login; + private byte[] verify; public LoginSessionHandler(MinecraftConnection inbound) { this.inbound = Preconditions.checkNotNull(inbound, "inbound"); } @Override - public void handle(MinecraftPacket packet) { + public void handle(MinecraftPacket packet) throws Exception { if (packet instanceof ServerLogin) { this.login = (ServerLogin) packet; + + // Request encryption. + EncryptionRequest request = generateRequest(); + this.verify = Arrays.copyOf(request.getVerifyToken(), 4); + inbound.write(request); + // TODO: Online-mode checks - handleSuccessfulLogin(); + //handleSuccessfulLogin(); + } + + if (packet instanceof EncryptionResponse) { + KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair(); + EncryptionResponse response = (EncryptionResponse) packet; + byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken()); + if (!Arrays.equals(verify, decryptedVerifyToken)) { + throw new IllegalStateException("Unable to successfully decrypt the verification token."); + } + + byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret()); + String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic()); + + String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString(); + VelocityServer.getServer().getHttpClient() + .get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp))) + .thenAccept(profileResponse -> { + try { + inbound.enableEncryption(decryptedSharedSecret); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + + GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class); + handleSuccessfulLogin(profile); + }) + .exceptionally(exception -> { + System.out.println("Can't enable encryption"); + exception.printStackTrace(); + inbound.close(); + return null; + }); } } @@ -43,17 +92,16 @@ public class LoginSessionHandler implements MinecraftSessionHandler { return request; } - private void handleSuccessfulLogin() { + private void handleSuccessfulLogin(GameProfile profile) { inbound.setCompressionThreshold(256); - String username = login.getUsername(); ServerLoginSuccess success = new ServerLoginSuccess(); - success.setUsername(username); - success.setUuid(UuidUtils.generateOfflinePlayerUuid(username)); + success.setUsername(profile.getName()); + success.setUuid(profile.idAsUuid()); inbound.write(success); // Initiate a regular connection and move over to it. - ConnectedPlayer player = new ConnectedPlayer(success.getUsername(), success.getUuid(), inbound); + ConnectedPlayer player = new ConnectedPlayer(profile, inbound); ServerInfo info = new ServerInfo("test", new InetSocketAddress("localhost", 25565)); ServerConnection connection = new ServerConnection(info, player, VelocityServer.getServer()); diff --git a/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index 825b6c5a7..8d688b1d2 100644 --- a/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -3,6 +3,7 @@ package com.velocitypowered.proxy.connection.client; import com.google.common.base.Preconditions; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packets.Ping; import com.velocitypowered.proxy.protocol.packets.StatusRequest; @@ -15,9 +16,6 @@ import net.kyori.text.TextComponent; import net.kyori.text.serializer.GsonComponentSerializer; public class StatusSessionHandler implements MinecraftSessionHandler { - private static final Gson GSON = new GsonBuilder() - .registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer()) - .create(); private final MinecraftConnection connection; public StatusSessionHandler(MinecraftConnection connection) { @@ -43,7 +41,7 @@ public class StatusSessionHandler implements MinecraftSessionHandler { null ); StatusResponse response = new StatusResponse(); - response.setStatus(GSON.toJson(ping)); + response.setStatus(VelocityServer.GSON.toJson(ping)); connection.write(response); } } diff --git a/src/main/java/com/velocitypowered/proxy/connection/http/NettyHttpClient.java b/src/main/java/com/velocitypowered/proxy/connection/http/NettyHttpClient.java new file mode 100644 index 000000000..f13e1931b --- /dev/null +++ b/src/main/java/com/velocitypowered/proxy/connection/http/NettyHttpClient.java @@ -0,0 +1,63 @@ +package com.velocitypowered.proxy.connection.http; + +import com.velocitypowered.proxy.VelocityServer; +import io.netty.channel.*; +import io.netty.handler.codec.http.*; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; + +import javax.net.ssl.SSLEngine; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +public class NettyHttpClient { + private final VelocityServer server; + + public NettyHttpClient(VelocityServer server) { + this.server = server; + } + + public CompletableFuture get(URL url) { + String host = url.getHost(); + int port = url.getPort(); + boolean ssl = url.getProtocol().equals("https"); + if (port == -1) { + port = ssl ? 443 : 80; + } + + CompletableFuture reply = new CompletableFuture<>(); + server.initializeGenericBootstrap() + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + if (ssl) { + SslContext context = SslContextBuilder.forClient().build(); + SSLEngine engine = context.newEngine(ch.alloc()); + ch.pipeline().addLast(new SslHandler(engine)); + } + ch.pipeline().addLast(new HttpClientCodec()); + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, url.getPath() + "?" + url.getQuery()); + request.headers().add(HttpHeaderNames.HOST, url.getHost()); + request.headers().add(HttpHeaderNames.USER_AGENT, "Velocity"); + ctx.writeAndFlush(request); + } + }); + ch.pipeline().addLast(new SimpleHttpResponseCollector(reply)); + } + }) + .connect(host, port) + .addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + reply.completeExceptionally(future.cause()); + } + } + }); + return reply; + } +} diff --git a/src/main/java/com/velocitypowered/proxy/connection/http/SimpleHttpResponseCollector.java b/src/main/java/com/velocitypowered/proxy/connection/http/SimpleHttpResponseCollector.java new file mode 100644 index 000000000..b39488827 --- /dev/null +++ b/src/main/java/com/velocitypowered/proxy/connection/http/SimpleHttpResponseCollector.java @@ -0,0 +1,41 @@ +package com.velocitypowered.proxy.connection.http; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.LastHttpContent; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; + +class SimpleHttpResponseCollector extends ChannelInboundHandlerAdapter { + private final StringBuilder buffer = new StringBuilder(1024); + private final CompletableFuture reply; + + SimpleHttpResponseCollector(CompletableFuture reply) { + this.reply = reply; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpResponse) { + HttpResponseStatus status = ((HttpResponse) msg).status(); + if (status != HttpResponseStatus.OK) { + ctx.close(); + reply.completeExceptionally(new RuntimeException("Unexpected status code " + status.code())); + } + } + + if (msg instanceof HttpContent) { + buffer.append(((HttpContent) msg).content().toString(StandardCharsets.UTF_8)); + ((HttpContent) msg).release(); + + if (msg instanceof LastHttpContent) { + ctx.close(); + reply.complete(buffer.toString()); + } + } + } +} diff --git a/src/main/java/com/velocitypowered/proxy/data/GameProfile.java b/src/main/java/com/velocitypowered/proxy/data/GameProfile.java new file mode 100644 index 000000000..6c6620c8e --- /dev/null +++ b/src/main/java/com/velocitypowered/proxy/data/GameProfile.java @@ -0,0 +1,84 @@ +package com.velocitypowered.proxy.data; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.velocitypowered.proxy.util.UuidUtils; + +import java.util.List; +import java.util.UUID; + +public class GameProfile { + private final String id; + private final String name; + private final List properties; + + public GameProfile(String id, String name, List properties) { + this.id = id; + this.name = name; + this.properties = ImmutableList.copyOf(properties); + } + + public String getId() { + return id; + } + + public UUID idAsUuid() { + return UuidUtils.fromUndashed(id); + } + + public String getName() { + return name; + } + + public List getProperties() { + return ImmutableList.copyOf(properties); + } + + public static GameProfile forOfflinePlayer(String username) { + Preconditions.checkNotNull(username, "username"); + String id = UuidUtils.toUndashed(UuidUtils.generateOfflinePlayerUuid(username)); + return new GameProfile(id, username, ImmutableList.of()); + } + + @Override + public String toString() { + return "GameProfile{" + + "id='" + id + '\'' + + ", name='" + name + '\'' + + ", properties=" + properties + + '}'; + } + + public class Property { + private final String name; + private final String value; + private final String signature; + + public Property(String name, String value, String signature) { + this.name = name; + this.value = value; + this.signature = signature; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public String getSignature() { + return signature; + } + + @Override + public String toString() { + return "Property{" + + "name='" + name + '\'' + + ", value='" + value + '\'' + + ", signature='" + signature + '\'' + + '}'; + } + } +} diff --git a/src/main/java/com/velocitypowered/proxy/protocol/encryption/JavaVelocityCipher.java b/src/main/java/com/velocitypowered/proxy/protocol/encryption/JavaVelocityCipher.java new file mode 100644 index 000000000..146b0fe02 --- /dev/null +++ b/src/main/java/com/velocitypowered/proxy/protocol/encryption/JavaVelocityCipher.java @@ -0,0 +1,43 @@ +package com.velocitypowered.proxy.protocol.encryption; + +import com.google.common.base.Preconditions; +import io.netty.buffer.ByteBuf; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import java.security.GeneralSecurityException; + +public class JavaVelocityCipher implements VelocityCipher { + private final Cipher cipher; + private boolean disposed = false; + + public JavaVelocityCipher(boolean encrypt, SecretKey key) throws GeneralSecurityException { + this.cipher = Cipher.getInstance("AES/CFB8/NoPadding"); + this.cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, new IvParameterSpec(key.getEncoded())); + } + + @Override + public void process(ByteBuf source, ByteBuf destination) throws ShortBufferException { + ensureNotDisposed(); + + byte[] sourceAsBytes = new byte[source.readableBytes()]; + source.readBytes(sourceAsBytes); + + int outputSize = cipher.getOutputSize(sourceAsBytes.length); + byte[] destinationBytes = new byte[outputSize]; + cipher.update(sourceAsBytes, 0, sourceAsBytes.length, destinationBytes); + destination.writeBytes(destinationBytes); + } + + @Override + public void dispose() { + ensureNotDisposed(); + disposed = true; + } + + private void ensureNotDisposed() { + Preconditions.checkState(!disposed, "Object already disposed"); + } +} diff --git a/src/main/java/com/velocitypowered/proxy/protocol/encryption/VelocityCipher.java b/src/main/java/com/velocitypowered/proxy/protocol/encryption/VelocityCipher.java new file mode 100644 index 000000000..d5f51eb67 --- /dev/null +++ b/src/main/java/com/velocitypowered/proxy/protocol/encryption/VelocityCipher.java @@ -0,0 +1,10 @@ +package com.velocitypowered.proxy.protocol.encryption; + +import com.velocitypowered.proxy.util.Disposable; +import io.netty.buffer.ByteBuf; + +import javax.crypto.ShortBufferException; + +public interface VelocityCipher extends Disposable { + void process(ByteBuf source, ByteBuf destination) throws ShortBufferException; +} diff --git a/src/main/java/com/velocitypowered/proxy/protocol/encryption/VelocityEncryptor.java b/src/main/java/com/velocitypowered/proxy/protocol/encryption/VelocityEncryptor.java deleted file mode 100644 index e05bc0e40..000000000 --- a/src/main/java/com/velocitypowered/proxy/protocol/encryption/VelocityEncryptor.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.velocitypowered.proxy.protocol.encryption; - -import com.velocitypowered.proxy.util.Disposable; -import io.netty.buffer.ByteBuf; - -public interface VelocityEncryptor extends Disposable { - void process(ByteBuf source, ByteBuf destination); -} diff --git a/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherDecoder.java b/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherDecoder.java new file mode 100644 index 000000000..ae5ea06a6 --- /dev/null +++ b/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherDecoder.java @@ -0,0 +1,29 @@ +package com.velocitypowered.proxy.protocol.netty; + +import com.google.common.base.Preconditions; +import com.velocitypowered.proxy.protocol.encryption.VelocityCipher; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +public class MinecraftCipherDecoder extends ByteToMessageDecoder { + private final VelocityCipher cipher; + + public MinecraftCipherDecoder(VelocityCipher cipher) { + this.cipher = Preconditions.checkNotNull(cipher, "cipher"); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + ByteBuf decrypted = ctx.alloc().buffer(); + try { + cipher.process(in, decrypted); + out.add(decrypted); + } catch (Exception e) { + decrypted.release(); + throw e; + } + } +} diff --git a/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherEncoder.java b/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherEncoder.java new file mode 100644 index 000000000..afe7cf502 --- /dev/null +++ b/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCipherEncoder.java @@ -0,0 +1,20 @@ +package com.velocitypowered.proxy.protocol.netty; + +import com.google.common.base.Preconditions; +import com.velocitypowered.proxy.protocol.encryption.VelocityCipher; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +public class MinecraftCipherEncoder extends MessageToByteEncoder { + private final VelocityCipher cipher; + + public MinecraftCipherEncoder(VelocityCipher cipher) { + this.cipher = Preconditions.checkNotNull(cipher, "cipher"); + } + + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { + cipher.process(msg, out); + } +} diff --git a/src/main/java/com/velocitypowered/proxy/protocol/packets/EncryptionResponse.java b/src/main/java/com/velocitypowered/proxy/protocol/packets/EncryptionResponse.java index a91157c8f..6c87fbb4e 100644 --- a/src/main/java/com/velocitypowered/proxy/protocol/packets/EncryptionResponse.java +++ b/src/main/java/com/velocitypowered/proxy/protocol/packets/EncryptionResponse.java @@ -5,14 +5,40 @@ import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; +import java.util.Arrays; + public class EncryptionResponse implements MinecraftPacket { private byte[] sharedSecret; private byte[] verifyToken; + public byte[] getSharedSecret() { + return sharedSecret; + } + + public void setSharedSecret(byte[] sharedSecret) { + this.sharedSecret = sharedSecret; + } + + public byte[] getVerifyToken() { + return verifyToken; + } + + public void setVerifyToken(byte[] verifyToken) { + this.verifyToken = verifyToken; + } + + @Override + public String toString() { + return "EncryptionResponse{" + + "sharedSecret=" + Arrays.toString(sharedSecret) + + ", verifyToken=" + Arrays.toString(verifyToken) + + '}'; + } + @Override public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { this.sharedSecret = ProtocolUtils.readByteArray(buf, 256); - this.verifyToken = ProtocolUtils.readByteArray(buf, 4); + this.verifyToken = ProtocolUtils.readByteArray(buf, 128); } @Override diff --git a/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java b/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java index a98a58bf9..fff09131b 100644 --- a/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java +++ b/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java @@ -1,9 +1,28 @@ package com.velocitypowered.proxy.util; +import javax.crypto.Cipher; import java.math.BigInteger; +import java.security.*; public enum EncryptionUtils { ; public static String twosComplementSha1Digest(byte[] digest) { return new BigInteger(digest).toString(16); } + + public static byte[] decryptRsa(KeyPair keyPair, byte[] bytes) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); + return cipher.doFinal(bytes); + } + + public static String generateServerId(byte[] sharedSecret, PublicKey key) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.update(sharedSecret); + digest.update(key.getEncoded()); + return twosComplementSha1Digest(digest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } }