Compare commits

...

8 Commits
1.0 ... main

Author SHA1 Message Date
Jill ef0f7f13f8 formatting fixes 2022-08-30 05:55:57 +03:00
Jill 1328da587d more information on packets 2022-08-30 05:52:28 +03:00
Jill 182ee0023a update blog post link 2022-05-13 12:51:14 +00:00
Jill b50964fe65 tweaks 2022-05-13 05:20:14 +03:00
Jill 85692e6949 decode some json data 2022-05-13 05:17:59 +03:00
Jill c69bd953bb typo fix 2022-05-13 05:11:23 +03:00
Jill 5b9b23f260 slight phrasing fix 2022-05-13 05:10:47 +03:00
Jill 944d68e707 init commit 2022-05-13 05:06:11 +03:00
2 changed files with 188 additions and 2 deletions

View File

@ -1,3 +1,11 @@
# essential-re
# Essential RE Findings
Reverse-engineered information about the Essential Minecraft mod
This is a repository containing information about the [Essential](https://essential.gg/) Minecraft mod, found through reverse engineering the mod and its server.
You're most likely looking for the [converted cosmetic downloads](https://git.oat.zone/oat/essential-re/releases). If you want a nicer, less formal write-up of what's going on, specifically about cosmetics, you're looking for the [blog post](https://blog.oat.zone/reverse-engineering-minecraft-cosmetics/).
Otherwise, here's what I've found, categorized nicely:
## Findings
- [packets.md](./packets.md), describing how packets are structured

178
packets.md Normal file
View File

@ -0,0 +1,178 @@
# Essential Packets
In Essential, packets are sent over WebSockets to `wss://connect.essential.gg/v1`. Both the server and client share the same simple structure:
1. A single byte, representing the packet type sent as an integer.
The type ID is a number that is unique to the client and server and established by the sending party.
For example, if a client wants to send a `ClientConnectionLoginPacket`, they will have to send to the server a packet of ID `0`, establishing that `ClientConnectionLoginPacket` is ID `1` (which looks something like `{"a":"connection.ClientConnectionLoginPacket","b":1}`), and then send a packet with the newly established ID `1`. This packet will then be understood by the server as a `ClientConnectionLoginPacket` consequentially, and will not need to be redefined.
Both parties establish that a packet of ID `0` is a `RegisterPacketTypeId` packet upon connection.
2. A single byte, representing the length of the packet UUID this packet is in reply to. Set to NUL and ignore next spot if this packet is not in reply to anything.
3. The UUIDv4 of the packet this packet is in reply to. Usually either 0 or 36 bytes long, seperated by dashes.
It's unknown to me how the UUID of a packet is obtained. This UUID is not sent by the sending party, but it wasn't relevant to what I was doing so I discarded it.
4. The length of the payload.
5. The JSON-encoded payload.
The payload will be encoded using a custom GSON config which encodes simple JSON keys into single characters. For instance, a payload of `{"className":"connection.ClientConnectionLoginPacket","packetId":1}` will be converted into `{"a":"connection.ClientConnectionLoginPacket","b":1}`. This seems to be a networking optimization and obfuscation tactic. The keys will always stay consistent and seem to be based on the `@SerializedName` annotation, however commonly they're properly ordered in the constructor; if a constructor of a packet lists the properties `id` and `type`, then you can always know that `a` will mean `id` and `b` will mean `type`. It seems that both parties always maintain packet definitions for both sides to decode recieved packets.
## Packets
Here are some note-worthy packets I've discovered:
### connection.RegisterPacketTypeId
This is a packet that's always registered internally as `0` for both sides, which lets a party define the ID of a packet.
### connection.ClientConnectionLoginPacket
_Client-exclusive_
This packet is sent by the client and is used for authentication. A `username` and `sharedSecret` are sent which are then processed by the server to authenticate the user. Afterwards, the server will consider the user authenticated, and proceed to send them various packets. All packets sent upon authentication from now on will be marked with "Sent on authentication".
### response.ResponseActionPacket
This packet contains a single boolean and is used in responses to other packets to indicate whether something succeeded or failed.
### cosmetic.ServerCosmeticsPopulatePacket
_Sent on authentication_
_Server-exclusive_
This packet sends every cosmetic registered on the server, along with the necessary assets and metadata. This is the only packet where information about a cosmetic is sent.
### profile.trustedhosts.ServerProfileTrustedHostsPopulatePacket
_Sent on authentication_
_Server-exclusive_
This packet seems to provide the client with "trusted hosts". As of writing, this is what this packet looks like:
```json
{
"trustedHosts": [
{
"id": "60cb24a50dc956941869312d",
"name": "Discord",
"domains": [ "cdn.discordapp.com", "media.discordapp.net" ]
},
{
"id": "60cb24a50dc956941869312e",
"name": "BadLion",
"domains": [ "i.badlion.net" ]
},
{
"id": "60cb24a50dc956941869312f",
"name": "Imgur",
"domains": [ "i.imgur.com", "imgur.com" ]
},
{
"id": "60cb24a50dc9569418693130",
"name": "Reddit",
"domains": [ "i.redd.it" ]
},
{
"id": "60cb24a50dc9569418693131",
"name": "Twitter",
"domains": [ "pbs.twimg.com" ]
}
]
}
```
It is unknown what purpose these links serve. It may be a list of links that you are allowed to embed in private messages.
### telemetry.ClientTelemetryPacket
_Client-exclusive_
This packet is used for various purposes. It stores a single `String key` and a `Map<String, Object> metadata`. The `metadata` is optional in the constructor. It's used only three times:
1. Each time a new screen is opened in `GuiUtil` it sends a `ClientTelemetryPacket`:
```java
Essential.getInstance().getConnectionManager().send(new ClientTelemetryPacket("GUI_OPEN", MapsKt.mapOf(new Pair("name", screen.getClass().getName()))));
```
Which looks like this:
```json
{"key": "GUI_OPEN", "metadata": {"name": "[screen.getClass().getName()]"}}
```
2. Each time you connect, a blank `ClientTelemetryPacket` will be sent:
```java
this.send(new ClientTelemetryPacket("SESSION_START"));
```
3. Each time you connect, a `"MINI_STATE"` `ClientTelemetryPacket` will be sent, containing if a specific config setting is on:
```java
this.send(new ClientTelemetryPacket("MINI_STATE", new HashMap() {
{
this.put("mini", !EssentialConfig.INSTANCE.getEssentialFull());
}
}));
```
```java
public boolean getEssentialFull() {
return essentialFull;
}
```
```java
@Property(
type = PropertyType.SWITCH,
name = "Essential Full",
category = "General",
subcategory = "Experience",
description = "Enables all features available in Essential"
)
private static boolean essentialFull = true;
```
This is not to be confused with the `OnboardingData.hasAcceptedTos()` function. I'm not sure what exactly the Essential Full switch does, but disabling it seems to remove certain networking features.
### mod.ClientModsAnnouncePacket
_Client-exclusive_
_Sent on authentication_
This is a packet that contains:
- The client's Minecraft version
- The MD5 checksum of every single mod the client has installed
It's worth noting that the mod with the ID `"feather"` has a fake checksum in both the Fabric and Forge version, regardless of Minecraft version:
```java
modId != "feather" ? checksum : "e3d04e686b28b34b5a98ce078e4f9da8"
```
Since this is hardcoded, chances are the developers can find out exactly who uses a mod with this ID, where with a normal mod, you're able to spoof the signature by editing the .jar, tampering with the checksum. This is likely the [Feather client](https://feathermc.com/), which Essential devs have [a bit of history](https://twitter.com/Sk1er_/status/1499812679007064066) with.
- The client's mod loader:
```java
public static enum Platform {
FORGE,
FABRIC;
}
```
- The loader's version
In the Fabric version, it does this by finding the Fabric API, and then condensing its' ID and version to a string:
```java
String platformVersion = mod.getId() + ":" + mod.getVersion().getFriendlyString();
```
In Forge, it's as simple as:
```java
String.valueOf(ForgeVersion.getBuildVersion());
```