From 3c60f792cae082ecab3a7d5f875d25e3939343f1 Mon Sep 17 00:00:00 2001 From: tamilpp25 Date: Tue, 11 Apr 2023 05:35:11 +0530 Subject: [PATCH] reject clients on version mismatch (#2106) --- .../server/http/dispatch/RegionHandler.java | 62 +++++++++---------- .../java/emu/grasscutter/utils/Crypto.java | 46 ++++++++++++-- 2 files changed, 73 insertions(+), 35 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java index 3b55c2f2d..60517aded 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java @@ -1,12 +1,15 @@ package emu.grasscutter.server.http.dispatch; import com.google.protobuf.ByteString; +import emu.grasscutter.GameConstants; import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp; import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp; import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; +import emu.grasscutter.net.proto.StopServerInfoOuterClass.StopServerInfo; import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; import emu.grasscutter.server.http.Router; @@ -16,6 +19,7 @@ import emu.grasscutter.utils.Utils; import io.javalin.Javalin; import io.javalin.http.Context; +import java.time.Instant; import javax.crypto.Cipher; import java.io.ByteArrayOutputStream; import java.util.*; @@ -202,7 +206,8 @@ public final class RegionHandler implements Router { regionData = region.getBase64(); } - String[] versionCode = versionName.replaceAll(Pattern.compile("[a-zA-Z]").pattern(), "").split("\\."); + String clientVersion = versionName.replaceAll(Pattern.compile("[a-zA-Z]").pattern(), ""); + String[] versionCode = clientVersion.split("\\."); int versionMajor = Integer.parseInt(versionCode[0]); int versionMinor = Integer.parseInt(versionCode[1]); int versionFix = Integer.parseInt(versionCode[2]); @@ -211,6 +216,30 @@ public final class RegionHandler implements Router { try { QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call(); + String key_id = ctx.queryParam("key_id"); + + if (!clientVersion.equals(GameConstants.VERSION)) { // Reject clients when there is a version mismatch + + boolean updateClient = GameConstants.VERSION.compareTo(clientVersion) > 0; + + QueryCurrRegionHttpRsp rsp = QueryCurrRegionHttpRsp.newBuilder() + .setRetcode(Retcode.RET_STOP_SERVER_VALUE) + .setMsg("Connection Failed!") + .setRegionInfo(RegionInfo.newBuilder()) + .setStopServer(StopServerInfo.newBuilder() + .setUrl("https://discord.gg/grasscutters") + .setStopBeginTime((int) Instant.now().getEpochSecond()) + .setStopEndTime((int) Instant.now().getEpochSecond()*2) + .setContentMsg(updateClient ? "\nVersion mismatch outdated client! \n\nServer version: %s\nClient version: %s".formatted(GameConstants.VERSION, clientVersion) : "\nVersion mismatch outdated server! \n\nServer version: %s\nClient version: %s".formatted(GameConstants.VERSION, clientVersion)) + .build()) + .buildPartial(); + + Grasscutter.getLogger().info(String.format("Connection denied for %s due to %s", ctx.ip(), updateClient ? "outdated client!" : "outdated server!")); + + ctx.json(Crypto.encryptAndSignRegionData(rsp.toByteArray(), key_id)); + return; + } + if (ctx.queryParam("dispatchSeed") == null) { // More love for UA Patch players var rsp = new QueryCurRegionRspJson(); @@ -222,39 +251,10 @@ public final class RegionHandler implements Router { return; } - String key_id = ctx.queryParam("key_id"); - if (key_id == null) - throw new Exception("Key ID was not set"); - - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - cipher.init(Cipher.ENCRYPT_MODE, Crypto.EncryptionKeys.get(Integer.valueOf(key_id))); var regionInfo = Utils.base64Decode(event.getRegionInfo()); - //Encrypt regionInfo in chunks - ByteArrayOutputStream encryptedRegionInfoStream = new ByteArrayOutputStream(); - - //Thank you so much GH Copilot - int chunkSize = 256 - 11; - int regionInfoLength = regionInfo.length; - int numChunks = (int) Math.ceil(regionInfoLength / (double) chunkSize); - - for (int i = 0; i < numChunks; i++) { - byte[] chunk = Arrays.copyOfRange(regionInfo, i * chunkSize, Math.min((i + 1) * chunkSize, regionInfoLength)); - byte[] encryptedChunk = cipher.doFinal(chunk); - encryptedRegionInfoStream.write(encryptedChunk); - } - - Signature privateSignature = Signature.getInstance("SHA256withRSA"); - privateSignature.initSign(Crypto.CUR_SIGNING_KEY); - privateSignature.update(regionInfo); - - var rsp = new QueryCurRegionRspJson(); - - rsp.content = Utils.base64Encode(encryptedRegionInfoStream.toByteArray()); - rsp.sign = Utils.base64Encode(privateSignature.sign()); - - ctx.json(rsp); + ctx.json(Crypto.encryptAndSignRegionData(regionInfo, key_id)); } catch (Exception e) { Grasscutter.getLogger().error("An error occurred while handling query_cur_region.", e); diff --git a/src/main/java/emu/grasscutter/utils/Crypto.java b/src/main/java/emu/grasscutter/utils/Crypto.java index f0da36ea6..da25073ad 100644 --- a/src/main/java/emu/grasscutter/utils/Crypto.java +++ b/src/main/java/emu/grasscutter/utils/Crypto.java @@ -1,20 +1,26 @@ package emu.grasscutter.utils; +import emu.grasscutter.server.http.objects.QueryCurRegionRspJson; +import java.io.ByteArrayOutputStream; import java.io.File; import java.nio.file.Path; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; +import java.security.Signature; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; import java.util.Map; import java.util.HashMap; import java.util.regex.Pattern; import emu.grasscutter.Grasscutter; +import javax.crypto.Cipher; public final class Crypto { + private static final SecureRandom secureRandom = new SecureRandom(); public static byte[] DISPATCH_KEY; @@ -45,8 +51,7 @@ public final class Crypto { var m = pattern.matcher(path.getFileName().toString()); - if (m.matches()) - { + if (m.matches()) { var key = KeyFactory.getInstance("RSA") .generatePublic(new X509EncodedKeySpec(FileUtils.read(path))); @@ -54,8 +59,7 @@ public final class Crypto { } } } - } - catch (Exception e) { + } catch (Exception e) { Grasscutter.getLogger().error("An error occurred while loading keys.", e); } } @@ -75,4 +79,38 @@ public final class Crypto { secureRandom.nextBytes(bytes); return bytes; } + + public static QueryCurRegionRspJson encryptAndSignRegionData(byte[] regionInfo, String key_id) throws Exception { + if (key_id == null) { + throw new Exception("Key ID was not set"); + } + + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, EncryptionKeys.get(Integer.valueOf(key_id))); + + //Encrypt regionInfo in chunks + ByteArrayOutputStream encryptedRegionInfoStream = new ByteArrayOutputStream(); + + //Thank you so much GH Copilot + int chunkSize = 256 - 11; + int regionInfoLength = regionInfo.length; + int numChunks = (int) Math.ceil(regionInfoLength / (double) chunkSize); + + for (int i = 0; i < numChunks; i++) { + byte[] chunk = Arrays.copyOfRange(regionInfo, i * chunkSize, + Math.min((i + 1) * chunkSize, regionInfoLength)); + byte[] encryptedChunk = cipher.doFinal(chunk); + encryptedRegionInfoStream.write(encryptedChunk); + } + + Signature privateSignature = Signature.getInstance("SHA256withRSA"); + privateSignature.initSign(CUR_SIGNING_KEY); + privateSignature.update(regionInfo); + + var rsp = new QueryCurRegionRspJson(); + + rsp.content = Utils.base64Encode(encryptedRegionInfoStream.toByteArray()); + rsp.sign = Utils.base64Encode(privateSignature.sign()); + return rsp; + } }