Skip to content

Commit

Permalink
Add seed exchange support with modified global-metadata (Grasscutters#9)
Browse files Browse the repository at this point in the history
## Description

Please carefully read the [Contributing note](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md) and [Code of conduct](https://github.com/Grasscutters/Grasscutter/blob/development/CODE_OF_CONDUCT.md) before making any pull requests.
And, **Do not make a pull request to merge into stable unless it is a hotfix. Use the development branch instead.**
## Issues fixed by this PR

<!--- Put the links of issues that may be fixed by this PR here (if any). -->
## Type of changes

<!--- Put an `x` in all the boxes that apply your changes. -->

- [ ] Bug fix
- [x] New feature
- [x] Enhancement
- [ ] Documentation

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] My pull request is unique and no other pull requests have been opened for these changes
- [x] I have read the [Contributing note](https://github.com/Grasscutters/Grasscutter/blob/stable/CONTRIBUTING.md) and [Code of conduct](https://github.com/Grasscutters/Grasscutter/blob/development/CODE_OF_CONDUCT.md)
- [x] I am responsible for any copyright issues with my code if it occurs in the future.

Co-authored-by: ayy lmao <ridvan-nuri@windowslive.com>
Co-authored-by: Benj <benjamin7006@gmail.com>
Reviewed-on: https://git.4benj.com/Grasscutter-Backrooms/Grasscutter/pulls/9
  • Loading branch information
3 people committed Jul 12, 2022
1 parent 818d449 commit fe4be04
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 125 deletions.
3 changes: 3 additions & 0 deletions proto/GetPlayerTokenReq.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@ message GetPlayerTokenReq {
string psn_id = 5;
string client_ip_str = 6;
string birthday = 966;
uint32 unk1 = 1883;
string client_seed = 924;
uint32 key_id = 550;
}
6 changes: 6 additions & 0 deletions proto/GetPlayerTokenRsp.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,10 @@ message GetPlayerTokenRsp {
uint32 reg_platform = 633;
string client_ip_str = 1238;
string birthday = 1109;
uint32 unk1 = 1728;
bool unk2 = 1679;
repeated uint32 unk3 = 2012;
string encrypted_seed = 1596;
string seed_signature = 1501;
uint32 unk6 = 1447;
}
73 changes: 34 additions & 39 deletions src/main/java/emu/grasscutter/net/packet/BasePacket.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,131 +10,126 @@
public class BasePacket {
private static final int const1 = 17767; // 0x4567
private static final int const2 = -30293; // 0x89ab

private int opcode;
private boolean shouldBuildHeader = false;

private byte[] header;
private byte[] data;

// Encryption
private boolean useDispatchKey;
public boolean shouldEncrypt = true;

public BasePacket(int opcode) {
this.opcode = opcode;
}

public BasePacket(int opcode, int clientSequence) {
this.opcode = opcode;
this.buildHeader(clientSequence);
}

public BasePacket(int opcode, boolean buildHeader) {
this.opcode = opcode;
this.shouldBuildHeader = buildHeader;
}

public int getOpcode() {
return opcode;
return this.opcode;
}

public void setOpcode(int opcode) {
this.opcode = opcode;
}

public boolean useDispatchKey() {
return useDispatchKey;
return this.useDispatchKey;
}

public void setUseDispatchKey(boolean useDispatchKey) {
this.useDispatchKey = useDispatchKey;
}

public byte[] getHeader() {
return header;
return this.header;
}

public void setHeader(byte[] header) {
this.header = header;
}

public boolean shouldBuildHeader() {
return shouldBuildHeader;
return this.shouldBuildHeader;
}

public byte[] getData() {
return data;
return this.data;
}

public void setData(byte[] data) {
this.data = data;
}

public void setData(GeneratedMessageV3 proto) {
this.data = proto.toByteArray();
}

@SuppressWarnings("rawtypes")
public void setData(GeneratedMessageV3.Builder proto) {
this.data = proto.build().toByteArray();
}

public BasePacket buildHeader(int clientSequence) {
if (this.getHeader() != null && clientSequence == 0) {
return this;
}
setHeader(PacketHead.newBuilder().setClientSequenceId(clientSequence).setTimestamp(System.currentTimeMillis()).build().toByteArray());
this.setHeader(PacketHead.newBuilder().setClientSequenceId(clientSequence).setTimestamp(System.currentTimeMillis()).build().toByteArray());
return this;
}

public byte[] build() {
if (getHeader() == null) {
if (this.getHeader() == null) {
this.header = new byte[0];
}
if (getData() == null) {

if (this.getData() == null) {
this.data = new byte[0];
}
ByteArrayOutputStream baos = new ByteArrayOutputStream(2 + 2 + 2 + 4 + getHeader().length + getData().length + 2);

ByteArrayOutputStream baos = new ByteArrayOutputStream(2 + 2 + 2 + 4 + this.getHeader().length + this.getData().length + 2);

this.writeUint16(baos, const1);
this.writeUint16(baos, opcode);
this.writeUint16(baos, header.length);
this.writeUint32(baos, data.length);
this.writeBytes(baos, header);
this.writeBytes(baos, data);
this.writeUint16(baos, this.opcode);
this.writeUint16(baos, this.header.length);
this.writeUint32(baos, this.data.length);
this.writeBytes(baos, this.header);
this.writeBytes(baos, this.data);
this.writeUint16(baos, const2);

byte[] packet = baos.toByteArray();

if (this.shouldEncrypt) {
if(this.useDispatchKey()) {
Crypto.xor(packet, Crypto.DISPATCH_KEY, true);
}
else {
Crypto.xor(packet, Crypto.ENCRYPT_KEY, false);
}
}
if (this.shouldEncrypt) {
Crypto.xor(packet, this.useDispatchKey() ? Crypto.DISPATCH_KEY : Crypto.ENCRYPT_KEY);
}

return packet;
}

public void writeUint16(ByteArrayOutputStream baos, int i) {
// Unsigned short
baos.write((byte) ((i >>> 8) & 0xFF));
baos.write((byte) (i & 0xFF));
}

public void writeUint32(ByteArrayOutputStream baos, int i) {
// Unsigned int (long)
baos.write((byte) ((i >>> 24) & 0xFF));
baos.write((byte) ((i >>> 16) & 0xFF));
baos.write((byte) ((i >>> 8) & 0xFF));
baos.write((byte) (i & 0xFF));
}

public void writeBytes(ByteArrayOutputStream baos, byte[] bytes) {
try {
baos.write(bytes);
Expand All @@ -143,4 +138,4 @@ public void writeBytes(ByteArrayOutputStream baos, byte[] bytes) {
e.printStackTrace();
}
}
}
}
6 changes: 3 additions & 3 deletions src/main/java/emu/grasscutter/server/game/GameSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ public void onConnected(GameSessionManager.KcpTunnel tunnel) {

@Override
public void handleReceive(byte[] bytes) {
// Decrypt and turn back into a packet
Crypto.xor(bytes, useSecretKey() ? Crypto.ENCRYPT_KEY : Crypto.DISPATCH_KEY);
ByteBuf packet = Unpooled.wrappedBuffer(bytes);
// Decrypt and turn back into a packet
Crypto.xor(bytes, useSecretKey() ? Crypto.ENCRYPT_KEY : Crypto.DISPATCH_KEY);
ByteBuf packet = Unpooled.wrappedBuffer(bytes);

// Log
//logPacket(packet);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package emu.grasscutter.server.http.dispatch;

import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.*;
import emu.grasscutter.net.proto.RegionInfoOuterClass;
import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp;
import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo;
import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo;
import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent;
Expand All @@ -21,17 +19,19 @@
import io.javalin.Javalin;

import javax.crypto.Cipher;
import java.io.File;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.security.*;
import java.io.ByteArrayOutputStream;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.security.Signature;


import static emu.grasscutter.Configuration.*;
import static emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.*;
import static emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp;

/**
* Handles requests related to region queries.
Expand Down Expand Up @@ -85,8 +85,7 @@ private void initialize() {
// Create a region info object.
var regionInfo = RegionInfo.newBuilder()
.setGateserverIp(region.Ip).setGateserverPort(region.Port)
//.setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.setSecretKey(ByteString.copyFrom(new byte[]{0}))
.setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.build();
// Create an updated region query.
var updatedQuery = QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(regionInfo).build();
Expand All @@ -95,7 +94,7 @@ private void initialize() {

// 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, false); // XOR the config with the key.
Crypto.xor(customConfig, Crypto.DISPATCH_KEY); // XOR the config with the key.

// Create an updated region list.
QueryRegionListHttpRsp updatedRegionList = QueryRegionListHttpRsp.newBuilder()
Expand Down Expand Up @@ -144,28 +143,39 @@ private static void queryCurrentRegion(Request request, Response response) {

if( versionName.contains("2.7.5") || versionName.contains("2.8.")) {
try {
var key = FileUtils.readResource("/keys/" + (versionName.contains("OSCB") ? "OSCB_Pub.der" : "OSCN_Pub.der") );

KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(key);
PublicKey pub_key = keyFactory.generatePublic(keySpec);

Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, pub_key);
cipher.init(Cipher.ENCRYPT_MODE, versionName.contains("OSCB") ? Crypto.CUR_OSCB_ENCRYPT_KEY : Crypto.CUR_OSCN_ENCRYPT_KEY);

QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call();
var regionInfo = event.getRegionInfo();
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);

String encodedRsp = Utils.base64Encode(cipher.doFinal(Utils.base64Decode(regionInfo)));
var rsp = new QueryCurRegionRspJson();

rsp.content = encodedRsp;
rsp.sign = "ZnVja195b3VfbWh5";
rsp.content = Utils.base64Encode(encryptedRegionInfoStream.toByteArray());
rsp.sign = Utils.base64Encode(privateSignature.sign());

response.send(rsp);
}
catch(Exception e) {
e.printStackTrace();
catch (Exception e) {
Grasscutter.getLogger().error("An error occurred while handling query_cur_region.", e);
}
}
else {
Expand Down Expand Up @@ -206,4 +216,4 @@ public String getBase64() {
public static QueryCurrRegionHttpRsp getCurrentRegion() {
return SERVER.runMode == ServerRunMode.HYBRID ? regions.get("os_usa").getRegionQuery() : null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.game.GameSession.SessionState;
import emu.grasscutter.server.packet.send.PacketGetPlayerTokenRsp;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.Utils;

import javax.crypto.Cipher;

import java.nio.ByteBuffer;
import java.security.Signature;

@Opcodes(PacketOpcodes.GetPlayerTokenReq)
public class HandlerGetPlayerTokenReq extends PacketHandler {
Expand Down Expand Up @@ -90,8 +97,32 @@ public void handle(GameSession session, byte[] header, byte[] payload) throws Ex
session.setUseSecretKey(true);
session.setState(SessionState.WAITING_FOR_LOGIN);

// Send packet
session.send(new PacketGetPlayerTokenRsp(session));
}

// Only >= 2.7.50 has this
if (req.getKeyId() > 0) {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, Crypto.CUR_SIGNING_KEY);

var client_seed_encrypted = Utils.base64Decode(req.getClientSeed());
var client_seed = ByteBuffer.wrap(cipher.doFinal(client_seed_encrypted))
.getLong();

byte[] seed_bytes = ByteBuffer.wrap(new byte[8])
.putLong(Crypto.ENCRYPT_SEED ^ client_seed)
.array();

//Kind of a hack, but whatever
cipher.init(Cipher.ENCRYPT_MODE, req.getKeyId() == 3 ? Crypto.CUR_OSCB_ENCRYPT_KEY : Crypto.CUR_OSCN_ENCRYPT_KEY);
var seed_encrypted = cipher.doFinal(seed_bytes);

Signature privateSignature = Signature.getInstance("SHA256withRSA");
privateSignature.initSign(Crypto.CUR_SIGNING_KEY);
privateSignature.update(seed_bytes);

session.send(new PacketGetPlayerTokenRsp(session, Utils.base64Encode(seed_encrypted), Utils.base64Encode(privateSignature.sign())));
}
else {
// Send packet
session.send(new PacketGetPlayerTokenRsp(session));
}
}
}
Loading

0 comments on commit fe4be04

Please sign in to comment.