diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java index d68936cec..fd264e26b 100644 --- a/src/main/java/emu/grasscutter/config/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java @@ -33,9 +33,11 @@ public class ConfigContainer { * Lua script require system if performance is a concern. * Version 12 - 'http.startImmediately' was added to control whether the * HTTP server should start immediately. + * Version 13 - 'game.useUniquePacketKey' was added to control whether the + * encryption key used for packets is a constant or randomly generated. */ private static int version() { - return 12; + return 13; } /** @@ -169,6 +171,9 @@ public class ConfigContainer { /* This is the port used in the default region. */ public int accessPort = 0; + /* Enabling this will generate a unique packet encryption key for each player. */ + public boolean useUniquePacketKey = true; + /* Entities within a certain range will be loaded for the player */ public int loadEntitiesForPlayerRange = 300; /* Start in 'unstable-quests', Lua scripts will be enabled by default. */ diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index f9c629031..481bcbe8f 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -39,7 +39,7 @@ import emu.grasscutter.server.event.entity.EntityCreationEvent; import emu.grasscutter.server.event.player.PlayerTeleportEvent; import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.scheduler.ServerTaskScheduler; -import emu.grasscutter.utils.objects.KahnsSort; +import emu.grasscutter.utils.algorithms.KahnsSort; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import java.util.*; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/emu/grasscutter/net/packet/BasePacket.java b/src/main/java/emu/grasscutter/net/packet/BasePacket.java index ce605f637..5918d93aa 100644 --- a/src/main/java/emu/grasscutter/net/packet/BasePacket.java +++ b/src/main/java/emu/grasscutter/net/packet/BasePacket.java @@ -108,13 +108,7 @@ public class BasePacket { this.writeBytes(baos, data); this.writeUint16(baos, const2); - byte[] packet = baos.toByteArray(); - - if (this.shouldEncrypt) { - Crypto.xor(packet, this.useDispatchKey() ? Crypto.DISPATCH_KEY : Crypto.ENCRYPT_KEY); - } - - return packet; + return baos.toByteArray(); } public void writeUint16(ByteArrayOutputStream baos, int i) { diff --git a/src/main/java/emu/grasscutter/server/game/GameSession.java b/src/main/java/emu/grasscutter/server/game/GameSession.java index 0cff31cd1..fa0d1a357 100644 --- a/src/main/java/emu/grasscutter/server/game/GameSession.java +++ b/src/main/java/emu/grasscutter/server/game/GameSession.java @@ -23,6 +23,9 @@ public class GameSession implements GameSessionManager.KcpChannel { @Getter @Setter private Account account; @Getter private Player player; + @Getter private long encryptSeed = Crypto.ENCRYPT_SEED; + private byte[] encryptKey = Crypto.ENCRYPT_KEY; + @Setter private boolean useSecretKey; @Getter @Setter private SessionState state; @@ -34,6 +37,11 @@ public class GameSession implements GameSessionManager.KcpChannel { this.server = server; this.state = SessionState.WAITING_FOR_TOKEN; this.lastPingTime = System.currentTimeMillis(); + + if (GAME_INFO.useUniquePacketKey) { + this.encryptKey = new byte[4096]; + this.encryptSeed = Crypto.generateEncryptKeyAndSeed(this.encryptKey); + } } public GameServer getServer() { @@ -133,7 +141,12 @@ public class GameSession implements GameSessionManager.KcpChannel { event.call(); if (!event.isCanceled()) { // If event is not cancelled, continue. try { - tunnel.writeData(event.getPacket().build()); + packet = event.getPacket(); + var bytes = packet.build(); + if (packet.shouldEncrypt) { + Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey); + } + tunnel.writeData(bytes); } catch (Exception ignored) { Grasscutter.getLogger().debug("Unable to send packet to client."); } @@ -149,7 +162,7 @@ public class GameSession implements GameSessionManager.KcpChannel { @Override public void handleReceive(byte[] bytes) { // Decrypt and turn back into a packet - Crypto.xor(bytes, useSecretKey() ? Crypto.ENCRYPT_KEY : Crypto.DISPATCH_KEY); + Crypto.xor(bytes, useSecretKey() ? this.encryptKey : Crypto.DISPATCH_KEY); ByteBuf packet = Unpooled.wrappedBuffer(bytes); // Log diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java index 5f25512fc..edcc18a00 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java @@ -111,32 +111,33 @@ public class HandlerGetPlayerTokenReq extends PacketHandler { // Only >= 2.7.50 has this if (req.getKeyId() > 0) { + var encryptSeed = session.getEncryptSeed(); try { var cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, Crypto.CUR_SIGNING_KEY); - var client_seed_encrypted = Utils.base64Decode(req.getClientRandKey()); - var client_seed = ByteBuffer.wrap(cipher.doFinal(client_seed_encrypted)).getLong(); + var clientSeedEncrypted = Utils.base64Decode(req.getClientRandKey()); + var clientSeed = ByteBuffer.wrap(cipher.doFinal(clientSeedEncrypted)).getLong(); - var seed_bytes = - ByteBuffer.wrap(new byte[8]).putLong(Crypto.ENCRYPT_SEED ^ client_seed).array(); + var seedBytes = + ByteBuffer.wrap(new byte[8]).putLong(encryptSeed ^ clientSeed).array(); cipher.init(Cipher.ENCRYPT_MODE, Crypto.EncryptionKeys.get(req.getKeyId())); - var seed_encrypted = cipher.doFinal(seed_bytes); + var seedEncrypted = cipher.doFinal(seedBytes); var privateSignature = Signature.getInstance("SHA256withRSA"); privateSignature.initSign(Crypto.CUR_SIGNING_KEY); - privateSignature.update(seed_bytes); + privateSignature.update(seedBytes); session.send( new PacketGetPlayerTokenRsp( session, - Utils.base64Encode(seed_encrypted), + Utils.base64Encode(seedEncrypted), Utils.base64Encode(privateSignature.sign()))); } catch (Exception ignored) { // Only UA Patch users will have exception var clientBytes = Utils.base64Decode(req.getClientRandKey()); - var seed = ByteHelper.longToBytes(Crypto.ENCRYPT_SEED); + var seed = ByteHelper.longToBytes(encryptSeed); Crypto.xor(clientBytes, seed); var base64str = Utils.base64Encode(clientBytes); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java index 29d855651..6a1ab63f0 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java @@ -20,7 +20,7 @@ public class PacketGetPlayerTokenRsp extends BasePacket { .setAccountType(1) .setIsProficientPlayer( session.getPlayer().getAvatars().getAvatarCount() > 0) // Not sure where this goes - .setSecretKeySeed(Crypto.ENCRYPT_SEED) + .setSecretKeySeed(session.getEncryptSeed()) .setSecurityCmdBuffer(ByteString.copyFrom(Crypto.ENCRYPT_SEED_BUFFER)) .setPlatformType(3) .setChannelId(1) @@ -66,7 +66,7 @@ public class PacketGetPlayerTokenRsp extends BasePacket { .setAccountType(1) .setIsProficientPlayer( session.getPlayer().getAvatars().getAvatarCount() > 0) // Not sure where this goes - .setSecretKeySeed(Crypto.ENCRYPT_SEED) + .setSecretKeySeed(session.getEncryptSeed()) .setSecurityCmdBuffer(ByteString.copyFrom(Crypto.ENCRYPT_SEED_BUFFER)) .setPlatformType(3) .setChannelId(1) diff --git a/src/main/java/emu/grasscutter/utils/Crypto.java b/src/main/java/emu/grasscutter/utils/Crypto.java index d9f4a8403..03f2644ac 100644 --- a/src/main/java/emu/grasscutter/utils/Crypto.java +++ b/src/main/java/emu/grasscutter/utils/Crypto.java @@ -2,6 +2,8 @@ package emu.grasscutter.utils; import emu.grasscutter.Grasscutter; import emu.grasscutter.server.http.objects.QueryCurRegionRspJson; +import emu.grasscutter.utils.algorithms.MersenneTwister64; + import java.io.ByteArrayOutputStream; import java.nio.file.Path; import java.security.*; @@ -34,9 +36,9 @@ public final class Crypto { try { CUR_SIGNING_KEY = - KeyFactory.getInstance("RSA") - .generatePrivate( - new PKCS8EncodedKeySpec(FileUtils.readResource("/keys/SigningKey.der"))); + 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")) { @@ -46,8 +48,8 @@ public final class Crypto { if (m.matches()) { var key = - KeyFactory.getInstance("RSA") - .generatePublic(new X509EncodedKeySpec(FileUtils.read(path))); + KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(FileUtils.read(path))); EncryptionKeys.put(Integer.valueOf(m.group(1)), key); } @@ -74,8 +76,28 @@ public final class Crypto { return bytes; } + public static long generateEncryptKeyAndSeed(byte[] encryptKey) { + var encryptSeed = secureRandom.nextLong(); + var mt = new MersenneTwister64(); + mt.setSeed(encryptSeed); + mt.setSeed(mt.nextLong()); + mt.nextLong(); + for (int i = 0; i < 4096 >> 3; i++) { + var rand = mt.nextLong(); + encryptKey[i << 3] = (byte) (rand >> 56); + encryptKey[(i << 3) + 1] = (byte) (rand >> 48); + encryptKey[(i << 3) + 2] = (byte) (rand >> 40); + encryptKey[(i << 3) + 3] = (byte) (rand >> 32); + encryptKey[(i << 3) + 4] = (byte) (rand >> 24); + encryptKey[(i << 3) + 5] = (byte) (rand >> 16); + encryptKey[(i << 3) + 6] = (byte) (rand >> 8); + encryptKey[(i << 3) + 7] = (byte) rand; + } + return encryptSeed; + } + public static QueryCurRegionRspJson encryptAndSignRegionData(byte[] regionInfo, String key_id) - throws Exception { + throws Exception { if (key_id == null) { throw new Exception("Key ID was not set"); } @@ -93,8 +115,8 @@ public final class Crypto { for (int i = 0; i < numChunks; i++) { byte[] chunk = - Arrays.copyOfRange( - regionInfo, i * chunkSize, Math.min((i + 1) * chunkSize, regionInfoLength)); + Arrays.copyOfRange( + regionInfo, i * chunkSize, Math.min((i + 1) * chunkSize, regionInfoLength)); byte[] encryptedChunk = cipher.doFinal(chunk); encryptedRegionInfoStream.write(encryptedChunk); } diff --git a/src/main/java/emu/grasscutter/utils/objects/KahnsSort.java b/src/main/java/emu/grasscutter/utils/algorithms/KahnsSort.java similarity index 97% rename from src/main/java/emu/grasscutter/utils/objects/KahnsSort.java rename to src/main/java/emu/grasscutter/utils/algorithms/KahnsSort.java index 57c22d7b0..38dee3c59 100644 --- a/src/main/java/emu/grasscutter/utils/objects/KahnsSort.java +++ b/src/main/java/emu/grasscutter/utils/algorithms/KahnsSort.java @@ -1,4 +1,4 @@ -package emu.grasscutter.utils.objects; +package emu.grasscutter.utils.algorithms; import java.util.*; diff --git a/src/main/java/emu/grasscutter/utils/algorithms/MersenneTwister64.java b/src/main/java/emu/grasscutter/utils/algorithms/MersenneTwister64.java new file mode 100644 index 000000000..8709a2276 --- /dev/null +++ b/src/main/java/emu/grasscutter/utils/algorithms/MersenneTwister64.java @@ -0,0 +1,53 @@ +package emu.grasscutter.utils.algorithms; + +public final class MersenneTwister64 { + // Period parameters + private static final int N = 312; + private static final int M = 156; + private static final long MATRIX_A = 0xB5026F5AA96619E9L; // private static final * constant vector a + private static final long UPPER_MASK = 0xFFFFFFFF80000000L; // most significant w-r bits + private static final int LOWER_MASK = 0x7FFFFFFF; // least significant r bits + + private final long[] mt = new long[N]; // the array for the state vector + private int mti; // mti == N+1 means mt[N] is not initialized + + public synchronized void setSeed(long seed) { + mt[0] = seed; + for (mti = 1; mti < N; mti++) { + mt[mti] = (0x5851F42D4C957F2DL * (mt[mti - 1] ^ (mt[mti - 1] >>> 62)) + mti); + } + } + + public synchronized long nextLong() { + int i; + long x; + final long[] mag01 = {0x0L, MATRIX_A}; + + if (mti >= N) { // generate N words at one time + if (mti == N + 1) { + setSeed(5489L); + } + + for (i = 0; i < N - M; i++) { + x = (mt[i] & UPPER_MASK) | (mt[i + 1] & LOWER_MASK); + mt[i] = mt[i + M] ^ (x >>> 1) ^ mag01[(int) (x & 0x1)]; + } + for (; i < N - 1; i++) { + x = (mt[i] & UPPER_MASK) | (mt[i + 1] & LOWER_MASK); + mt[i] = mt[i + (M - N)] ^ (x >>> 1) ^ mag01[(int) (x & 0x1)]; + } + x = (mt[N - 1] & UPPER_MASK) | (mt[0] & LOWER_MASK); + mt[N - 1] = mt[M - 1] ^ (x >>> 1) ^ mag01[(int) (x & 0x1)]; + + mti = 0; + } + + x = mt[mti++]; + x ^= (x >>> 29) & 0x5555555555555555L; + x ^= (x << 17) & 0x71D67FFFEDA60000L; + x ^= (x << 37) & 0xFFF7EEE000000000L; + x ^= (x >>> 43); + + return x; + } +}