diff --git a/src/main/java/net/md_5/bungee/EncryptionUtil.java b/src/main/java/net/md_5/bungee/EncryptionUtil.java new file mode 100644 index 00000000..26fe790f --- /dev/null +++ b/src/main/java/net/md_5/bungee/EncryptionUtil.java @@ -0,0 +1,89 @@ +package net.md_5.bungee; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.net.URL; +import java.net.URLEncoder; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.util.Arrays; +import java.util.Random; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import net.md_5.bungee.packet.PacketFCEncryptionResponse; +import net.md_5.bungee.packet.PacketFDEncryptionRequest; +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.modes.CFBBlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +public class EncryptionUtil { + + private static final Random random = new Random(); + private static KeyPair keys; + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + public static PacketFDEncryptionRequest encryptRequest() throws NoSuchAlgorithmException { + if (keys == null) { + keys = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + } + + String hash = Long.toString(random.nextLong(), 16); + byte[] pubKey = keys.getPublic().getEncoded(); + byte[] verify = new byte[4]; + random.nextBytes(verify); + return new PacketFDEncryptionRequest(hash, pubKey, verify); + } + + public static SecretKey getSecret(PacketFCEncryptionResponse resp, PacketFDEncryptionRequest request) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, keys.getPrivate()); + byte[] decrypted = cipher.doFinal(resp.verifyToken); + + if (!Arrays.equals(request.verifyToken, decrypted)) { + throw new IllegalStateException("Key pairs do not match!"); + } + + cipher.init(Cipher.DECRYPT_MODE, keys.getPrivate()); + byte[] shared = resp.sharedSecret; + byte[] secret = cipher.doFinal(shared); + + return new SecretKeySpec(secret, "AES"); + } + + public static boolean isAuthenticated(String username, String connectionHash, SecretKey shared) throws NoSuchAlgorithmException, IOException { + String encName = URLEncoder.encode(username, "UTF-8"); + + MessageDigest sha = MessageDigest.getInstance("SHA-1"); + for (byte[] bit : new byte[][]{connectionHash.getBytes("ISO_8859_1"), shared.getEncoded(), keys.getPublic().getEncoded()}) { + sha.update(bit); + } + + String encodedHash = URLEncoder.encode(new BigInteger(sha.digest()).toString(16), "UTF-8"); + String authURL = "http://session.minecraft.net/game/checkserver.jsp?user=" + encName + "&serverId=" + encodedHash; + String reply; + try (BufferedReader in = new BufferedReader(new InputStreamReader(new URL(authURL).openStream()))) { + reply = in.readLine(); + } + + return "YES".equals(reply); + } + + public static BufferedBlockCipher getCipher(boolean forEncryption, Key shared) { + BufferedBlockCipher cip = new BufferedBlockCipher(new CFBBlockCipher(new AESFastEngine(), 8)); + cip.init(forEncryption, new ParametersWithIV(new KeyParameter(shared.getEncoded()), shared.getEncoded(), 0, 16)); + return cip; + } +} diff --git a/src/main/java/net/md_5/bungee/InitialHandler.java b/src/main/java/net/md_5/bungee/InitialHandler.java index 1690644b..7a39b4b7 100644 --- a/src/main/java/net/md_5/bungee/InitialHandler.java +++ b/src/main/java/net/md_5/bungee/InitialHandler.java @@ -1,17 +1,23 @@ package net.md_5.bungee; +import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; +import javax.crypto.SecretKey; import net.md_5.bungee.packet.Packet2Handshake; +import net.md_5.bungee.packet.PacketFCEncryptionResponse; +import net.md_5.bungee.packet.PacketFDEncryptionRequest; import net.md_5.bungee.packet.PacketFFKick; import net.md_5.bungee.packet.PacketInputStream; +import org.bouncycastle.crypto.io.CipherInputStream; +import org.bouncycastle.crypto.io.CipherOutputStream; public class InitialHandler implements Runnable { private final Socket socket; - private final PacketInputStream in; - private final OutputStream out; + private PacketInputStream in; + private OutputStream out; public InitialHandler(Socket socket) throws IOException { this.socket = socket; @@ -27,6 +33,24 @@ public class InitialHandler implements Runnable { switch (id) { case 0x02: Packet2Handshake handshake = new Packet2Handshake(packet); + PacketFDEncryptionRequest request = EncryptionUtil.encryptRequest(); + out.write(request.getPacket()); + PacketFCEncryptionResponse response = new PacketFCEncryptionResponse(in.readPacket()); + + SecretKey shared = EncryptionUtil.getSecret(response, request); + if (!EncryptionUtil.isAuthenticated(handshake.username, request.serverId, shared)) { + throw new KickException("Not authenticated with minecraft.net"); + } + + out.write(new PacketFCEncryptionResponse().getPacket()); + in = new PacketInputStream(new CipherInputStream(socket.getInputStream(), EncryptionUtil.getCipher(false, shared))); + out = new BufferedOutputStream(new CipherOutputStream(socket.getOutputStream(), EncryptionUtil.getCipher(true, shared)), 5120); + + int ciphId = Util.getId(in.readPacket()); + if (ciphId != 0xCD) { + throw new KickException("Unable to receive encrypted client status"); + } + break; case 0xFE: throw new KickException(BungeeCord.instance.config.motd + ChatColor.COLOR_CHAR + BungeeCord.instance.getOnlinePlayers() + ChatColor.COLOR_CHAR + BungeeCord.instance.config.maxPlayers); diff --git a/src/main/java/net/md_5/bungee/packet/DefinedPacket.java b/src/main/java/net/md_5/bungee/packet/DefinedPacket.java index 8e2f56a8..7d519e34 100644 --- a/src/main/java/net/md_5/bungee/packet/DefinedPacket.java +++ b/src/main/java/net/md_5/bungee/packet/DefinedPacket.java @@ -42,7 +42,6 @@ public abstract class DefinedPacket implements DataInput, DataOutput { out = ByteStreams.newDataOutput(); this.id = id; writeByte(id); - packet = null; } public byte[] getPacket() {