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 87e81e60d..f75d06556 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java @@ -1,330 +1,330 @@ -package emu.grasscutter.server.http.dispatch; - -import static emu.grasscutter.config.Configuration.*; - -import com.google.protobuf.ByteString; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.Grasscutter.ServerRunMode; -import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp; -import emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp; -import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; -import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; -import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; -import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; -import emu.grasscutter.server.http.Router; -import emu.grasscutter.server.http.objects.QueryCurRegionRspJson; -import emu.grasscutter.utils.Crypto; -import emu.grasscutter.utils.Utils; -import io.javalin.Javalin; -import io.javalin.http.Context; -import java.io.ByteArrayOutputStream; -import java.security.Signature; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; -import javax.crypto.Cipher; -import org.slf4j.Logger; - -/** Handles requests related to region queries. */ -public final class RegionHandler implements Router { - private static final Map regions = new ConcurrentHashMap<>(); - private static String regionListResponse; - private static String regionListResponsecn; - - public RegionHandler() { - try { // Read & initialize region data. - this.initialize(); - } catch (Exception exception) { - Grasscutter.getLogger().error("Failed to initialize region data.", exception); - } - } - - /** - * Handle query region list request. - * - * @param ctx The context object for handling the request. - * @route /query_region_list - */ - private static void queryRegionList(Context ctx) { - // Get logger and query parameters. - Logger logger = Grasscutter.getLogger(); - if (ctx.queryParamMap().containsKey("version") && ctx.queryParamMap().containsKey("platform")) { - String versionName = ctx.queryParam("version"); - String versionCode = versionName.replaceAll("[/.0-9]*", ""); - String platformName = ctx.queryParam("platform"); - - // Determine the region list to use based on the version and platform. - if ("CNRELiOS".equals(versionCode) - || "CNRELWin".equals(versionCode) - || "CNRELAndroid".equals(versionCode)) { - // Use the CN region list. - QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponsecn); - event.call(); - logger.debug("Connect to Chinese version"); - - // Respond with the event result. - ctx.result(event.getRegionList()); - } else if ("OSRELiOS".equals(versionCode) - || "OSRELWin".equals(versionCode) - || "OSRELAndroid".equals(versionCode)) { - // Use the OS region list. - QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); - event.call(); - logger.debug("Connect to global version"); - - // Respond with the event result. - ctx.result(event.getRegionList()); - } else { - /* - * String regionListResponse = "CP///////////wE="; - * QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); - * event.call(); - * ctx.result(event.getRegionList()); - * return; - */ - // Use the default region list. - QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); - event.call(); - logger.debug("Connect to global version"); - - // Respond with the event result. - ctx.result(event.getRegionList()); - } - } else { - // Use the default region list. - QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); - event.call(); - logger.debug("Connect to global version"); - - // Respond with the event result. - ctx.result(event.getRegionList()); - } - // Log the request to the console. - Grasscutter.getLogger() - .info(String.format("[Dispatch] Client %s request: query_region_list", ctx.ip())); - } - - /** - * @route /query_cur_region/{region} - */ - private static void queryCurrentRegion(Context ctx) { - // Get region to query. - String regionName = ctx.pathParam("region"); - String versionName = ctx.queryParam("version"); - var region = regions.get(regionName); - - // Get region data. - String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; - if (ctx.queryParamMap().values().size() > 0) { - if (region != null) regionData = region.getBase64(); - } - - String[] versionCode = - versionName.replaceAll(Pattern.compile("[a-zA-Z]").pattern(), "").split("\\."); - int versionMajor = Integer.parseInt(versionCode[0]); - int versionMinor = Integer.parseInt(versionCode[1]); - int versionFix = Integer.parseInt(versionCode[2]); - - if (versionMajor >= 3 - || (versionMajor == 2 && versionMinor == 7 && versionFix >= 50) - || (versionMajor == 2 && versionMinor == 8)) { - try { - QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); - event.call(); - - if (ctx.queryParam("dispatchSeed") == null) { - // More love for UA Patch players - var rsp = new QueryCurRegionRspJson(); - - rsp.content = event.getRegionInfo(); - rsp.sign = "TW9yZSBsb3ZlIGZvciBVQSBQYXRjaCBwbGF5ZXJz"; - - ctx.json(rsp); - 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); - } catch (Exception e) { - Grasscutter.getLogger().error("An error occurred while handling query_cur_region.", e); - } - } else { - // Invoke event. - QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); - event.call(); - // Respond with event result. - ctx.result(event.getRegionInfo()); - } - // Log to console. - Grasscutter.getLogger() - .info(String.format("Client %s request: query_cur_region/%s", ctx.ip(), regionName)); - } - - /** - * Gets the current region query. - * - * @return A {@link QueryCurrRegionHttpRsp} object. - */ - public static QueryCurrRegionHttpRsp getCurrentRegion() { - return SERVER.runMode == ServerRunMode.HYBRID ? regions.get("os_usa").getRegionQuery() : null; - } - - /** Configures region data according to configuration. */ - private void initialize() { - String dispatchDomain = - "http" - + (HTTP_ENCRYPTION.useInRouting ? "s" : "") - + "://" - + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) - + ":" - + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort); - - // Create regions. - List servers = new ArrayList<>(); - List usedNames = new ArrayList<>(); // List to check for potential naming conflicts. - - var configuredRegions = new ArrayList<>(List.of(DISPATCH_INFO.regions)); - if (SERVER.runMode != ServerRunMode.HYBRID && configuredRegions.size() == 0) { - Grasscutter.getLogger() - .error( - "[Dispatch] There are no game servers available. Exiting due to unplayable state."); - System.exit(1); - } else if (configuredRegions.size() == 0) - configuredRegions.add( - new Region( - "os_usa", - DISPATCH_INFO.defaultName, - lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress), - lr(GAME_INFO.accessPort, GAME_INFO.bindPort))); - - configuredRegions.forEach( - region -> { - if (usedNames.contains(region.Name)) { - Grasscutter.getLogger().error("Region name already in use."); - return; - } - - // Create a region identifier. - var identifier = - RegionSimpleInfo.newBuilder() - .setName(region.Name) - .setTitle(region.Title) - .setType("DEV_PUBLIC") - .setDispatchUrl(dispatchDomain + "/query_cur_region/" + region.Name) - .build(); - usedNames.add(region.Name); - servers.add(identifier); - - // Create a region info object. - var regionInfo = - RegionInfo.newBuilder() - .setGateserverIp(region.Ip) - .setGateserverPort(region.Port) - .setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED)) - .build(); - // Create an updated region query. - var updatedQuery = QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(regionInfo).build(); - regions.put( - region.Name, - new RegionData( - updatedQuery, Utils.base64Encode(updatedQuery.toByteString().toByteArray()))); - }); - - // Create a config object. - byte[] customConfig = - "{\"sdkenv\":\"2\",\"checkdevice\":\"false\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}" - .getBytes(); - Crypto.xor(customConfig, Crypto.DISPATCH_KEY); // XOR the config with the key. - - // Create an updated region list. - QueryRegionListHttpRsp updatedRegionList = - QueryRegionListHttpRsp.newBuilder() - .addAllRegionList(servers) - .setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED)) - .setClientCustomConfigEncrypted(ByteString.copyFrom(customConfig)) - .setEnableLoginPc(true) - .build(); - - // Set the region list response. - regionListResponse = Utils.base64Encode(updatedRegionList.toByteString().toByteArray()); - - // CN - // Create a config object. - byte[] customConfigcn = - "{\"sdkenv\":\"0\",\"checkdevice\":\"true\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}" - .getBytes(); - Crypto.xor(customConfigcn, Crypto.DISPATCH_KEY); // XOR the config with the key. - - // Create an updated region list. - QueryRegionListHttpRsp updatedRegionListcn = - QueryRegionListHttpRsp.newBuilder() - .addAllRegionList(servers) - .setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED)) - .setClientCustomConfigEncrypted(ByteString.copyFrom(customConfigcn)) - .setEnableLoginPc(true) - .build(); - - // Set the region list response. - regionListResponsecn = Utils.base64Encode(updatedRegionListcn.toByteString().toByteArray()); - } - - @Override - public void applyRoutes(Javalin javalin) { - javalin.get("/query_region_list", RegionHandler::queryRegionList); - javalin.get("/query_cur_region/{region}", RegionHandler::queryCurrentRegion); - } - - /** Region data container. */ - public static class RegionData { - private final QueryCurrRegionHttpRsp regionQuery; - private final String base64; - - public RegionData(QueryCurrRegionHttpRsp prq, String b64) { - this.regionQuery = prq; - this.base64 = b64; - } - - public QueryCurrRegionHttpRsp getRegionQuery() { - return this.regionQuery; - } - - public String getBase64() { - return this.base64; - } - } -} +package emu.grasscutter.server.http.dispatch; + +import static emu.grasscutter.config.Configuration.*; + +import com.google.protobuf.ByteString; +import emu.grasscutter.GameConstants; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerRunMode; +import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp; +import emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp; +import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; +import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; +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; +import emu.grasscutter.server.http.objects.QueryCurRegionRspJson; +import emu.grasscutter.utils.Crypto; +import emu.grasscutter.utils.Utils; +import io.javalin.Javalin; +import io.javalin.http.Context; + +import javax.crypto.Cipher; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; +import org.slf4j.Logger; + +/** Handles requests related to region queries. */ +public final class RegionHandler implements Router { + private static final Map regions = new ConcurrentHashMap<>(); + private static String regionListResponse; + private static String regionListResponsecn; + + public RegionHandler() { + try { // Read & initialize region data. + this.initialize(); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to initialize region data.", exception); + } + } + + /** + * Handle query region list request. + * + * @param ctx The context object for handling the request. + * @route /query_region_list + */ + private static void queryRegionList(Context ctx) { + // Get logger and query parameters. + Logger logger = Grasscutter.getLogger(); + if (ctx.queryParamMap().containsKey("version") && ctx.queryParamMap().containsKey("platform")) { + String versionName = ctx.queryParam("version"); + String versionCode = versionName.replaceAll("[/.0-9]*", ""); + String platformName = ctx.queryParam("platform"); + + // Determine the region list to use based on the version and platform. + if ("CNRELiOS".equals(versionCode) + || "CNRELWin".equals(versionCode) + || "CNRELAndroid".equals(versionCode)) { + // Use the CN region list. + QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponsecn); + event.call(); + logger.debug("Connect to Chinese version"); + + // Respond with the event result. + ctx.result(event.getRegionList()); + } else if ("OSRELiOS".equals(versionCode) + || "OSRELWin".equals(versionCode) + || "OSRELAndroid".equals(versionCode)) { + // Use the OS region list. + QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); + event.call(); + logger.debug("Connect to global version"); + + // Respond with the event result. + ctx.result(event.getRegionList()); + } else { + /* + * String regionListResponse = "CP///////////wE="; + * QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); + * event.call(); + * ctx.result(event.getRegionList()); + * return; + */ + // Use the default region list. + QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); + event.call(); + logger.debug("Connect to global version"); + + // Respond with the event result. + ctx.result(event.getRegionList()); + } + } else { + // Use the default region list. + QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); + event.call(); + logger.debug("Connect to global version"); + + // Respond with the event result. + ctx.result(event.getRegionList()); + } + // Log the request to the console. + Grasscutter.getLogger() + .info(String.format("[Dispatch] Client %s request: query_region_list", ctx.ip())); + } + + /** + * @route /query_cur_region/{region} + */ + private static void queryCurrentRegion(Context ctx) { + // Get region to query. + String regionName = ctx.pathParam("region"); + String versionName = ctx.queryParam("version"); + var region = regions.get(regionName); + + // Get region data. + String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; + if (ctx.queryParamMap().values().size() > 0) { + if (region != null) regionData = region.getBase64(); + } + + String[] versionCode = + versionName.replaceAll(Pattern.compile("[a-zA-Z]").pattern(), "").split("\\."); + String clientVersion = versionName.replaceAll(Pattern.compile("[a-zA-Z]").pattern(), ""); + int versionMajor = Integer.parseInt(versionCode[0]); + int versionMinor = Integer.parseInt(versionCode[1]); + int versionFix = Integer.parseInt(versionCode[2]); + + if (versionMajor >= 3 + || (versionMajor == 2 && versionMinor == 7 && versionFix >= 50) + || (versionMajor == 2 && versionMinor == 8)) { + 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(); + + rsp.content = event.getRegionInfo(); + rsp.sign = "TW9yZSBsb3ZlIGZvciBVQSBQYXRjaCBwbGF5ZXJz"; + + ctx.json(rsp); + return; + } + + 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()); + + ctx.json(Crypto.encryptAndSignRegionData(regionInfo, key_id)); + } catch (Exception e) { + Grasscutter.getLogger().error("An error occurred while handling query_cur_region.", e); + } + } else { + // Invoke event. + QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); + event.call(); + // Respond with event result. + ctx.result(event.getRegionInfo()); + } + // Log to console. + Grasscutter.getLogger() + .info(String.format("Client %s request: query_cur_region/%s", ctx.ip(), regionName)); + } + + /** + * Gets the current region query. + * + * @return A {@link QueryCurrRegionHttpRsp} object. + */ + public static QueryCurrRegionHttpRsp getCurrentRegion() { + return SERVER.runMode == ServerRunMode.HYBRID ? regions.get("os_usa").getRegionQuery() : null; + } + + /** Configures region data according to configuration. */ + private void initialize() { + String dispatchDomain = + "http" + + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + + "://" + + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + + ":" + + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort); + + // Create regions. + List servers = new ArrayList<>(); + List usedNames = new ArrayList<>(); // List to check for potential naming conflicts. + + var configuredRegions = new ArrayList<>(List.of(DISPATCH_INFO.regions)); + if (SERVER.runMode != ServerRunMode.HYBRID && configuredRegions.size() == 0) { + Grasscutter.getLogger() + .error( + "[Dispatch] There are no game servers available. Exiting due to unplayable state."); + System.exit(1); + } else if (configuredRegions.size() == 0) + configuredRegions.add( + new Region( + "os_usa", + DISPATCH_INFO.defaultName, + lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress), + lr(GAME_INFO.accessPort, GAME_INFO.bindPort))); + + configuredRegions.forEach( + region -> { + if (usedNames.contains(region.Name)) { + Grasscutter.getLogger().error("Region name already in use."); + return; + } + + // Create a region identifier. + var identifier = + RegionSimpleInfo.newBuilder() + .setName(region.Name) + .setTitle(region.Title) + .setType("DEV_PUBLIC") + .setDispatchUrl(dispatchDomain + "/query_cur_region/" + region.Name) + .build(); + usedNames.add(region.Name); + servers.add(identifier); + + // Create a region info object. + var regionInfo = + RegionInfo.newBuilder() + .setGateserverIp(region.Ip) + .setGateserverPort(region.Port) + .setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED)) + .build(); + // Create an updated region query. + var updatedQuery = QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(regionInfo).build(); + regions.put( + region.Name, + new RegionData( + updatedQuery, Utils.base64Encode(updatedQuery.toByteString().toByteArray()))); + }); + + // Create a config object. + byte[] customConfig = + "{\"sdkenv\":\"2\",\"checkdevice\":\"false\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}" + .getBytes(); + Crypto.xor(customConfig, Crypto.DISPATCH_KEY); // XOR the config with the key. + + // Create an updated region list. + QueryRegionListHttpRsp updatedRegionList = + QueryRegionListHttpRsp.newBuilder() + .addAllRegionList(servers) + .setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED)) + .setClientCustomConfigEncrypted(ByteString.copyFrom(customConfig)) + .setEnableLoginPc(true) + .build(); + + // Set the region list response. + regionListResponse = Utils.base64Encode(updatedRegionList.toByteString().toByteArray()); + + // CN + // Create a config object. + byte[] customConfigcn = + "{\"sdkenv\":\"0\",\"checkdevice\":\"true\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}" + .getBytes(); + Crypto.xor(customConfigcn, Crypto.DISPATCH_KEY); // XOR the config with the key. + + // Create an updated region list. + QueryRegionListHttpRsp updatedRegionListcn = + QueryRegionListHttpRsp.newBuilder() + .addAllRegionList(servers) + .setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED)) + .setClientCustomConfigEncrypted(ByteString.copyFrom(customConfigcn)) + .setEnableLoginPc(true) + .build(); + + // Set the region list response. + regionListResponsecn = Utils.base64Encode(updatedRegionListcn.toByteString().toByteArray()); + } + + @Override + public void applyRoutes(Javalin javalin) { + javalin.get("/query_region_list", RegionHandler::queryRegionList); + javalin.get("/query_cur_region/{region}", RegionHandler::queryCurrentRegion); + } + + /** Region data container. */ + public static class RegionData { + private final QueryCurrRegionHttpRsp regionQuery; + private final String base64; + + public RegionData(QueryCurrRegionHttpRsp prq, String b64) { + this.regionQuery = prq; + this.base64 = b64; + } + + public QueryCurrRegionHttpRsp getRegionQuery() { + return this.regionQuery; + } + + public String getBase64() { + return this.base64; + } + } +} diff --git a/src/main/java/emu/grasscutter/utils/Crypto.java b/src/main/java/emu/grasscutter/utils/Crypto.java index d57f78433..2dd854544 100644 --- a/src/main/java/emu/grasscutter/utils/Crypto.java +++ b/src/main/java/emu/grasscutter/utils/Crypto.java @@ -1,77 +1,117 @@ -package emu.grasscutter.utils; - -import emu.grasscutter.Grasscutter; -import java.nio.file.Path; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Pattern; - -public final class Crypto { - private static final SecureRandom secureRandom = new SecureRandom(); - - public static byte[] DISPATCH_KEY; - public static byte[] DISPATCH_SEED; - - public static byte[] ENCRYPT_KEY; - public static long ENCRYPT_SEED = Long.parseUnsignedLong("11468049314633205968"); - public static byte[] ENCRYPT_SEED_BUFFER = new byte[0]; - - public static PrivateKey CUR_SIGNING_KEY; - - public static Map EncryptionKeys = new HashMap<>(); - - public static void loadKeys() { - DISPATCH_KEY = FileUtils.readResource("/keys/dispatchKey.bin"); - DISPATCH_SEED = FileUtils.readResource("/keys/dispatchSeed.bin"); - - ENCRYPT_KEY = FileUtils.readResource("/keys/secretKey.bin"); - ENCRYPT_SEED_BUFFER = FileUtils.readResource("/keys/secretKeyBuffer.bin"); - - try { - CUR_SIGNING_KEY = - KeyFactory.getInstance("RSA") - .generatePrivate( - new PKCS8EncodedKeySpec(FileUtils.readResource("/keys/SigningKey.der"))); - - Pattern pattern = Pattern.compile("([0-9]*)_Pub\\.der"); - for (Path path : FileUtils.getPathsFromResource("/keys/game_keys")) { - if (path.toString().endsWith("_Pub.der")) { - - var m = pattern.matcher(path.getFileName().toString()); - - if (m.matches()) { - var key = - KeyFactory.getInstance("RSA") - .generatePublic(new X509EncodedKeySpec(FileUtils.read(path))); - - EncryptionKeys.put(Integer.valueOf(m.group(1)), key); - } - } - } - } catch (Exception e) { - Grasscutter.getLogger().error("An error occurred while loading keys.", e); - } - } - - public static void xor(byte[] packet, byte[] key) { - try { - for (int i = 0; i < packet.length; i++) { - packet[i] ^= key[i % key.length]; - } - } catch (Exception e) { - Grasscutter.getLogger().error("Crypto error.", e); - } - } - - public static byte[] createSessionKey(int length) { - byte[] bytes = new byte[length]; - secureRandom.nextBytes(bytes); - return bytes; - } -} +package emu.grasscutter.utils; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.server.http.objects.QueryCurRegionRspJson; + +import java.io.ByteArrayOutputStream; +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 javax.crypto.Cipher; + +public final class Crypto { + private static final SecureRandom secureRandom = new SecureRandom(); + + public static byte[] DISPATCH_KEY; + public static byte[] DISPATCH_SEED; + + public static byte[] ENCRYPT_KEY; + public static long ENCRYPT_SEED = Long.parseUnsignedLong("11468049314633205968"); + public static byte[] ENCRYPT_SEED_BUFFER = new byte[0]; + + public static PrivateKey CUR_SIGNING_KEY; + + public static Map EncryptionKeys = new HashMap<>(); + + public static void loadKeys() { + DISPATCH_KEY = FileUtils.readResource("/keys/dispatchKey.bin"); + DISPATCH_SEED = FileUtils.readResource("/keys/dispatchSeed.bin"); + + ENCRYPT_KEY = FileUtils.readResource("/keys/secretKey.bin"); + ENCRYPT_SEED_BUFFER = FileUtils.readResource("/keys/secretKeyBuffer.bin"); + + try { + CUR_SIGNING_KEY = + KeyFactory.getInstance("RSA") + .generatePrivate( + new PKCS8EncodedKeySpec(FileUtils.readResource("/keys/SigningKey.der"))); + + Pattern pattern = Pattern.compile("([0-9]*)_Pub\\.der"); + for (Path path : FileUtils.getPathsFromResource("/keys/game_keys")) { + if (path.toString().endsWith("_Pub.der")) { + + var m = pattern.matcher(path.getFileName().toString()); + + if (m.matches()) { + var key = + KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(FileUtils.read(path))); + + EncryptionKeys.put(Integer.valueOf(m.group(1)), key); + } + } + } + } catch (Exception e) { + Grasscutter.getLogger().error("An error occurred while loading keys.", e); + } + } + + public static void xor(byte[] packet, byte[] key) { + try { + for (int i = 0; i < packet.length; i++) { + packet[i] ^= key[i % key.length]; + } + } catch (Exception e) { + Grasscutter.getLogger().error("Crypto error.", e); + } + } + + public static byte[] createSessionKey(int length) { + byte[] bytes = new byte[length]; + 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 + var 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; + } +}