Difference between revisions of "Bedrock Login Sequence"

From wiki.vg
Jump to navigation Jump to search
(Initial documentation over Bedrock login sequence (still wip))
 
m (Nitpicks)
 
Line 9: Line 9:
 
   S->C : PlayStatusPacket(status=LOGIN_SUCCESS)
 
   S->C : PlayStatusPacket(status=LOGIN_SUCCESS)
 
   (Resource Pack Handling)
 
   (Resource Pack Handling)
   S->C: ResourcePacks Info
+
   S->C: ResourcePacksInfoPacket
   C->S: ResourcePackClientResponse
+
   C->S: ResourcePackClientResponsePacket(status=SEND_PACKS) (if applicable)
   S->C: ResourcePackStack
+
  C->S: ResourcePackClientResponsePacket(status=HAVE_ALL_PACKS)
   C->S: ResourcePackClientResponse
+
   S->C: ResourcePackStackPacket
 +
   C->S: ResourcePackClientResponsePacket(status=COMPLETED)
 
   (Resource Pack Handling Complete)
 
   (Resource Pack Handling Complete)
 
   S->C: Start Game
 
   S->C: Start Game

Latest revision as of 03:27, 31 August 2021

This page documents the current Bedrock Login Sequence and the method in which to validate Bedrock users with Xbox Live. This also covers other details such as encryption which are part of this process.

Overview

  C->S : LoginPacket
  (Server Auth; server enables encryption)
  S->C : ServerToClientHandshakePacket
  (Client Auth; client enables encryption)
  C->S : ClientToServerHandshakePacket
  S->C : PlayStatusPacket(status=LOGIN_SUCCESS)
  (Resource Pack Handling)
  S->C: ResourcePacksInfoPacket
  C->S: ResourcePackClientResponsePacket(status=SEND_PACKS) (if applicable)
  C->S: ResourcePackClientResponsePacket(status=HAVE_ALL_PACKS)
  S->C: ResourcePackStackPacket
  C->S: ResourcePackClientResponsePacket(status=COMPLETED)
 (Resource Pack Handling Complete)
  S->C: Start Game
  1. see Login Process to get information about what happens next.

Client Login

After establishing the RakNet connection and after the client sends the NEW_INCOMING_CONNECTION RakNet socket event, the client will send over the Login packet. This should be expected as the first packet once this connection is created.

Within the Login packet is the client's protocol version, the JWT chain and the client's "extra" data. The first step in this process is to validate this chain. Typically this is sent over the protocol as a json array with a length of 3. If a client sends over a response smaller or larger than this number, you can safely close the connection as the data they sent over is likely corrupt or an attempt to send over malicious data.

Validating the Chain of Authority

This part is extremely important to get right, so make sure you read this carefully!

In the login packet, the client sends over json to complete this process. For validating the chain of authority, it is sent as a json array in the chain object from the Ascii string inside the LoginPacket. This contains the 3 JWT claims needed for authentication.

Within each of the 3 JWT claims sent over, each new claim is validated with the key specified in the previous one, with the first key being a self signed key. The header of each claim will always be an X509 certificate encoded in Base64. These keys should be generated using the EC (Elliptic Curve) algorithm with an X509 key spec, taking in the "identityPublicKey" json object from the previous key. The first key, being self-signed, should be constructed from the Signed JWT from the claim header's certificate URL. Of these 3 claims, 1 of them will always be Mojang's public key. This key's Base64 will always be a constant value and should always be 1 of the 3 claims. This key pair is generated should be constant and will also be used during the client to server and server to client handshake processes. For generating Mojang's public key, the algorithm should be EC (Elliptic Curve) using the "secp384r1" standard. Here is an example in Java of how to construct Mojang's public key:

private static final ECPublicKey MOJANG_PUBLIC_KEY;
private static final String MOJANG_PUBLIC_KEY_BASE64 =
            "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8ELkixyLcwlZryUQcu1TvPOmI2B7vX83ndnWRUaXm74wFfa5f/lwQNTfrLVHa2PmenpGI6JhIMUJaWZrjmMj90NoKNFSNBuKdm8rYiXsfaz3K36x/1U26HpG0ZxK/V1V";
private static final KeyPairGenerator KEY_PAIR_GEN;

static {
    try {
        KEY_PAIR_GEN = KeyPairGenerator.getInstance("EC");
        KEY_PAIR_GEN.initialize(new ECGenParameterSpec("secp384r1"));
        MOJANG_PUBLIC_KEY = generateKey(MOJANG_PUBLIC_KEY_BASE64);
    } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeySpecException e) {
        throw new AssertionError("Unable to initialize required encryption", e);
    }
}

public static ECPublicKey generateKey(String b64) throws NoSuchAlgorithmException, InvalidKeySpecException {
    return (ECPublicKey) KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(b64)));
}

It is extremely important that each claim is validated with the previous key in order to keep your server secure. Here is an example of the full login validation in Java (using nimbus-jose-jwt):

public boolean validateChainData(List<String> data) throws Exception {
    if (data.size() != 3) {
        return false;
    }

    ECPublicKey lastKey = null;
    boolean validChain = false;
    for (String claim : data) {
        JWSObject jwt = JWSObject.parse(claim);

        // x509 cert is expected in every claim
        URI x5u = jwt.getHeader().getX509CertURL();
        if (x5u == null) {
            return false;
        }

        ECPublicKey expectedKey = generateKey(jwt.getHeader().getX509CertURL().toString());
        // First key is self-signed
        if (lastKey == null) {
            lastKey = expectedKey;
        } else if (!lastKey.equals(expectedKey)) {
            return false;
        }

        if (!jws.verify(new ECDSAVerifier(lastKey))) {
            return false;
        }

        if (lastKey.equals(MOJANG_PUBLIC_KEY)) {
            validChain = true;
        }

        Object payload = JSONValue.parse(jwt.getPayload().toString());
        // Payload is not a json object
        if (!(payload instanceof JSONObject) {
            return false;
        }

        Object identityPublicKey = ((JSONObject) payload).get("identityPublicKey");
        // Public key is missing or invalid in the chain data; it's invalid
        if (!(identityPublicKey instanceof String)) {
            return false;
        }

        lastKey = generateKey((String) identityPublicKey);
    }

    return validChain;
}

Once the chain data has deemed valid, you can move onto the next part which is extracting the user's identity from the chain data. This is typically the last claim in the chain data. This can be extracted as a JWT object with the payload mapped to a string containing the json data. Within this json data is the displayName (the username), the identity (Xbox UUID), and the XUID (the user's XUID).

Constructing the User Profile

After completing the client chain of authority, the next step is extracting the user profile. With the last claim in the chain data extracted from the previous process, the key from that claim should be used to validate the JWT data sent over for the skin too. This public key will too, be used in the client & server encryption process. After checking that the extra data JWT is valid, the next step is reading the json payload from there, which contains a tonne of information relating to the client, such as their skin data, device OS, UI profile, input mode, etc.

.. More to come, still a WIP :)