Difference between revisions of "Protocol Encryption"

From wiki.vg
Jump to navigation Jump to search
(emphasize need for correct content-type; add extra possible error i got while writing a client; remove "HTTP/1.1" as server supports other http versions)
(102 intermediate revisions by 40 users not shown)
Line 1: Line 1:
As of 12w17a, minecraft implements SSL-like encryption.
+
As of 12w17a, Minecraft uses encrypted connections for online-mode servers.
  
 
== Overview ==
 
== Overview ==
  
#Client connects to server
+
# '''C'''→'''S''': [[Protocol#Handshake|Handshake]] State=2
#'''C->S''' 0x02 handshake
+
# '''C'''→'''S''': [[Protocol#Login Start|Login Start]]
#'''S->C''' 0xFD encryption request - server sends its public key
+
# '''S'''→'''C''': [[Protocol#Encryption Request|Encryption Request]]
#Client generates symmetric key (shared secret)
+
# ''Client authentication''
#Client authenticates via session.minecraft.net
+
# '''C'''→'''S''': [[Protocol#Encryption Response|Encryption Response]]
#'''C->S''' 0xFC encryption response - client encrypts shared secret with server's public key and sends
+
# ''Server authentication, both enable encryption''
#Server decrypts shared secret with its private key
+
# '''S'''→'''C''': [[Protocol#Login Success|Login Success]]
#Server checks player authenticity via session.minecraft.net
+
 
#'''S->C''' 0xFC encryption response - empty payload
+
See [[Protocol FAQ]] for a full list of packets exchanged after encryption.
#Server enables RC4 stream encryption
+
 
#Client enables RC4 stream encryption
+
==Server ID String==
#'''C->S''' 0x01 login
+
'''Update (1.7.x):''' The server ID is now sent as an empty string. Hashes also utilize the public key, so they will still be correct.
#'''S->C''' 0x01 login
+
 
#see [[Protocol FAQ]] to get information about what happens next.
+
'''Pre-1.7.x:'''
 +
The server ID string is a randomly-generated string of characters with a maximum length of 20 code points (the client disconnects with an exception if the length is longer than 20).
 +
 
 +
The client appears to arrive at incorrect hashes if the server ID string contains certain unprintable characters, so for consistent results only characters with code points in the range U+0021-U+007E (inclusive) should be sent.  This range corresponds to all of ASCII with the exception of the space character (U+0020) and all control characters (U+0000-U+001F, U+007F).
 +
 
 +
The client appears to arrive at incorrect hashes if the server ID string is too short.  15 to 20 (inclusive) length strings have been observed from the Notchian server and confirmed to work as of 1.5.2.
  
 
== Key Exchange ==
 
== Key Exchange ==
  
The server generates a 1024-bit RSA keypair on startup. The key, when packed into a 0xFD packet, is in ASN.1 format as defined by x.509.
+
The server generates a 1024-bit RSA keypair on startup. The public key sent in the Encryption Request packet is encoded in [https://en.wikipedia.org/wiki/ASN.1 ASN.1] [https://en.wikipedia.org/wiki/X.690#DER_encoding DER] format. This is a general-purpose binary format common in cryptography, conceptually similar to [[NBT]]. The schema is the same as the <code>SubjectPublicKeyInfo</code> structure defined by [https://en.wikipedia.org/wiki/X.509 X.509] (not a full-blown X.509 certificate!):
The ASN.1 structure looks as follows
 
  
 
  SubjectPublicKeyInfo ::= SEQUENCE {
 
  SubjectPublicKeyInfo ::= SEQUENCE {
Line 28: Line 32:
 
     parameters        ANY OPTIONAL
 
     parameters        ANY OPTIONAL
 
   }
 
   }
   subjectPublicKey  BITSTRING
+
   subjectPublicKey  BIT STRING
 
  }
 
  }
 
   
 
   
Line 36: Line 40:
 
  }
 
  }
  
If you're struggling to import this using a crypto library, you can convert it to common PEM by base64-encoding and wrapping in '-----BEGIN PUBLIC KEY-----' and '-----END PUBLIC KEY-----'.
+
(See the [[#Additional Links]] section of this article for further information.)
 +
 
 +
If you're struggling to import this using a crypto library, try to find a function that loads a DER encoded public key. If you can't find one, you can convert it to the more common PEM encoding by base64-encoding the raw bytes and wrapping the base64 text in '-----BEGIN PUBLIC KEY-----' and '-----END PUBLIC KEY-----'. See this example of a PEM encoded key: https://git.io/v7Ol9
 +
 
 +
It is also possible for a modified or custom server to use a longer RSA key, without breaking official clients.
  
 
== Symmetric Encryption ==
 
== Symmetric Encryption ==
  
On receipt of a 0xFD from the server, the client will generate a 16-byte shared secret, to be used with the RC4 stream cipher. It then encrypts it with the server's public key (PKCS#1 padded), and sends it to the server with a 0xFC.
+
On receipt of a Encryption Request from the server, the client will generate a random 16-byte shared secret, to be used with the AES/CFB8 stream cipher. It then encrypts it with the server's public key (PKCS#1 v1.5 padded), and also encrypts the verify token received in the Encryption Request packet in the same way, then sends both to the server in a Encryption Response packet. Both byte arrays in the Encryption Response packet will be 128 bytes long because of the padding. This is the only time the client uses the server's public key.
 +
 
 +
[[File:Icon_exclaim.gif|:!:]] In your crypto library, ensure that you set up your "feedback/segment size" to 8 bits or 1 byte, as indicated in the name AES/CFB'''8'''. Any other feedback size will result in encryption mismatch.  
  
The server decrypts the shared secret using its private key. It then sends a 0xFC to the client with an empty payload, and enables RC4 encryption. Similarly, the client will also enable encryption upon receipt of the empty 0xFC. From this point forward, everything is encrypted.
+
The server decrypts the shared secret and token using its private key, and checks if the token is the same. It then sends a Login Success, and enables AES/CFB8 encryption. For the Initial Vector (IV) and AES setup, both sides use the shared secret as both the IV and the key. Similarly, the client will also enable encryption upon sending Encryption Response. From this point forward, everything is encrypted. Note: the entire packet is encrypted, including the length fields and the packet's data.
 +
 
 +
The Login Success packet is sent encrypted.
 +
 
 +
[[File:Icon_exclaim.gif|:!:]] Note that the AES cipher is updated continuously, not finished and restarted every packet.
  
 
== Authentication ==
 
== Authentication ==
  
Both server and client need to make a request to session.minecraft.net if the server is in online-mode.
+
Both server and client need to make a request to sessionserver.mojang.com if the server is in online-mode.
  
 
=== Client ===
 
=== Client ===
  
 
After generating the shared secret, the client generates the following hash:
 
After generating the shared secret, the client generates the following hash:
<code>
 
    hash := sha1(concat(a, b, c))
 
    where:
 
      a = serverId from 0xFC
 
      b = server's public key
 
      c = shared secret
 
</code>
 
  
And sends an HTTP request to
+
sha1 := Sha1()
  <nowiki>http://session.minecraft.net/game/joinserver.jsp?user=</nowiki>''username''&sessionId=''[[Session#Login|user_session]]''&serverId=''hash''
+
sha1.update(ASCII encoding of the server id string from Encryption Request)
If it returns '''OK''' then continue, otherwise stop
+
sha1.update(shared secret)
 +
sha1.update(server's encoded public key from Encryption Request)
 +
hash := sha1.hexdigest()  # String of hex characters
 +
 
 +
[[File:Icon_exclaim.gif|:!:]] Note that the Sha1.hexdigest() method used by minecraft is non standard. It doesn't match the digest method found in most programming languages and libraries. It works by treating the sha1 output bytes as one large integer in two's complement and then printing the integer in base 16, placing a minus sign if the interpreted number is negative. Some examples of the minecraft digest are found below:
 +
 
 +
sha1(Notch) :  4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48
 +
sha1(jeb_)  : -7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1
 +
sha1(simon) :  88e16a1019277b15d58faf0541e11910eb756f6
 +
 
 +
The resulting hash is then sent via an HTTP POST request to
 +
  <nowiki>https://sessionserver.mojang.com/session/minecraft/join</nowiki>
 +
With the following sent as post data. You ''must'' have the Content-Type header set to application/json or you will get a 415 Unsupported Media Type or 403 Forbidden response.
 +
<syntaxhighlight lang="javascript">
 +
  {
 +
    "accessToken": "<accessToken>",
 +
    "selectedProfile": "<player's uuid without dashes>",
 +
    "serverId": "<serverHash>"
 +
  }
 +
</syntaxhighlight>
 +
 
 +
The fields <accessToken> and the player's uuid were received by the client during [[Authentication#Authenticate|authentication]].
 +
 
 +
If everything goes well, the client will receive a "204 No Content" response.
 +
 
 +
The server will respond with "403 Forbidden" if the player's Xbox profile has multiplayer disabled, with the following response:
 +
<syntaxhighlight lang="javascript">
 +
{
 +
    "error": "InsufficientPrivilegesException",
 +
    "path": "/session/minecraft/join"
 +
}
 +
</syntaxhighlight>
 +
 
 +
Similarly, if the player was banned from Multiplayer then the server will respond with the following error:
 +
<syntaxhighlight lang="javascript">
 +
{
 +
    "error": "UserBannedException",
 +
    "path": "/session/minecraft/join"
 +
}
 +
</syntaxhighlight>
 +
 
 +
If you forget to include a body with your request and just send an empty POST request, or if you use a malformed Content-Type header, you'll get this non-descriptive error:
 +
<syntaxhighlight lang="javascript">
 +
{
 +
    "error": "Forbidden",
 +
    "path": "/session/minecraft/join"
 +
}
 +
</syntaxhighlight>
  
 
=== Server ===
 
=== Server ===
  
After decrypting the shared secret in 0xFD, the sever generates the login hash as above and sends it to
+
After decrypting the shared secret in the second Encryption Response, the server generates the login hash as above and sends a HTTP GET to
  <nowiki>http://session.minecraft.net/game/checkserver?user=</nowiki>''username''&serverId=''hash''
+
  <nowiki>https://sessionserver.mojang.com/session/minecraft/hasJoined?username=</nowiki>''username''&serverId=''hash''&ip=''ip''
 +
 
 +
The username is case insensitive and must match the client's username (which was received in the Login Start packet). Note that this is the in-game nickname of the selected profile, not the Mojang account name (which is never sent to the server).
 +
Servers should use the name sent in the "name" field.
 +
 
 +
The ip field is optional and when present should be the IP address of the connecting player; it is the one that originally initiated the session request. The notchian server includes this only when <code>prevent-proxy-connections</code> is set to true in server.properties.
 +
 
 +
The response is a JSON object containing the user's UUID and skin blob
 +
<syntaxhighlight lang="javascript">
 +
{
 +
    "id": "<profile identifier>",
 +
    "name": "<player name>",
 +
    "properties": [
 +
        {
 +
            "name": "textures",
 +
            "value": "<base64 string>",
 +
            "signature": "<base64 string; signed data using Yggdrasil's private key>"
 +
        }
 +
    ]
 +
}
 +
</syntaxhighlight>
 +
 
 +
The "id" and "name" fields are then sent back to the client using a Login Success packet. The profile id in the json response has format
 +
"11111111222233334444555555555555" which needs to be changed into format "11111111-2222-3333-4444-555555555555" before sending it back to the client.
 +
 
 +
=== Sample Code ===
 +
 
 +
Examples of generating Minecraft-style hex digests:
 +
 
 +
* C++: https://git.io/JfTkx
 +
* C#: https://git.io/fhjp6
 +
* Go: http://git.io/-5ORag
 +
* Java: http://git.io/vzbmS
 +
* node.js: http://git.io/v2ue_A
 +
* PHP: https://git.io/fxcFY
 +
* Python: https://git.io/vQFUL
 +
* Rust: https://git.io/fj6P0
 +
 
 +
== Additional Links ==
 +
[https://msdn.microsoft.com/en-us/library/windows/desktop/bb648640(v=vs.85).aspx DER Encoding of ASN.1 Types]
 +
 
 +
[http://luca.ntop.org/Teaching/Appunti/asn1.html A Layman's Guide to a Subset of ASN.1, BER, and DER]
 +
 
 +
[https://gist.github.com/Lazersmoke/9947ada8acdd74a8b2e37d77cf1e0fdc Serializing an RSA Key Manually]
 +
 
 +
[https://gist.github.com/3900517 Encrypt shared secret using OpenSSL]
 +
 
 +
[http://pastebin.com/8eYyKZn6 Generate RSA-Keys and building the ASN.1v8 structure of the x.509 certificate using Crypto++]
 +
 
 +
[http://pastebin.com/7Jvaama1 Decrypt shared secret using Crypto++]
 +
 
 +
[http://pastebin.com/MjvR0T98 De/Encrypt data via AES using Crypto++]
 +
 
 +
[https://github.com/SirCmpwn/Craft.Net/blob/master/source/Craft.Net.Networking/AesStream.cs C# AES/CFB support with bouncy castle on Mono]
 +
 
  
If the response is '''YES''' then the client is authenticated and allowed to join. Otherwise the client will/should be [[Protocol#Disconnect.2FKick_.280xFF.29|kicked]] (unencrypted) with "Failed to verify username!"
+
[[Category:Protocol Details]]
 +
[[Category:Minecraft Modern]]

Revision as of 20:01, 2 April 2024

As of 12w17a, Minecraft uses encrypted connections for online-mode servers.

Overview

  1. CS: Handshake State=2
  2. CS: Login Start
  3. SC: Encryption Request
  4. Client authentication
  5. CS: Encryption Response
  6. Server authentication, both enable encryption
  7. SC: Login Success

See Protocol FAQ for a full list of packets exchanged after encryption.

Server ID String

Update (1.7.x): The server ID is now sent as an empty string. Hashes also utilize the public key, so they will still be correct.

Pre-1.7.x: The server ID string is a randomly-generated string of characters with a maximum length of 20 code points (the client disconnects with an exception if the length is longer than 20).

The client appears to arrive at incorrect hashes if the server ID string contains certain unprintable characters, so for consistent results only characters with code points in the range U+0021-U+007E (inclusive) should be sent. This range corresponds to all of ASCII with the exception of the space character (U+0020) and all control characters (U+0000-U+001F, U+007F).

The client appears to arrive at incorrect hashes if the server ID string is too short. 15 to 20 (inclusive) length strings have been observed from the Notchian server and confirmed to work as of 1.5.2.

Key Exchange

The server generates a 1024-bit RSA keypair on startup. The public key sent in the Encryption Request packet is encoded in ASN.1 DER format. This is a general-purpose binary format common in cryptography, conceptually similar to NBT. The schema is the same as the SubjectPublicKeyInfo structure defined by X.509 (not a full-blown X.509 certificate!):

SubjectPublicKeyInfo ::= SEQUENCE {
  algorithm SEQUENCE {
    algorithm         OBJECT IDENTIFIER
    parameters        ANY OPTIONAL
  }
  subjectPublicKey  BIT STRING
}

SubjectPublicKey ::= SEQUENCE {
  modulus           INTEGER
  publicExponent    INTEGER
}

(See the #Additional Links section of this article for further information.)

If you're struggling to import this using a crypto library, try to find a function that loads a DER encoded public key. If you can't find one, you can convert it to the more common PEM encoding by base64-encoding the raw bytes and wrapping the base64 text in '-----BEGIN PUBLIC KEY-----' and '-----END PUBLIC KEY-----'. See this example of a PEM encoded key: https://git.io/v7Ol9

It is also possible for a modified or custom server to use a longer RSA key, without breaking official clients.

Symmetric Encryption

On receipt of a Encryption Request from the server, the client will generate a random 16-byte shared secret, to be used with the AES/CFB8 stream cipher. It then encrypts it with the server's public key (PKCS#1 v1.5 padded), and also encrypts the verify token received in the Encryption Request packet in the same way, then sends both to the server in a Encryption Response packet. Both byte arrays in the Encryption Response packet will be 128 bytes long because of the padding. This is the only time the client uses the server's public key.

:!: In your crypto library, ensure that you set up your "feedback/segment size" to 8 bits or 1 byte, as indicated in the name AES/CFB8. Any other feedback size will result in encryption mismatch.

The server decrypts the shared secret and token using its private key, and checks if the token is the same. It then sends a Login Success, and enables AES/CFB8 encryption. For the Initial Vector (IV) and AES setup, both sides use the shared secret as both the IV and the key. Similarly, the client will also enable encryption upon sending Encryption Response. From this point forward, everything is encrypted. Note: the entire packet is encrypted, including the length fields and the packet's data.

The Login Success packet is sent encrypted.

:!: Note that the AES cipher is updated continuously, not finished and restarted every packet.

Authentication

Both server and client need to make a request to sessionserver.mojang.com if the server is in online-mode.

Client

After generating the shared secret, the client generates the following hash:

sha1 := Sha1()
sha1.update(ASCII encoding of the server id string from Encryption Request) 
sha1.update(shared secret) 
sha1.update(server's encoded public key from Encryption Request) 
hash := sha1.hexdigest()  # String of hex characters

:!: Note that the Sha1.hexdigest() method used by minecraft is non standard. It doesn't match the digest method found in most programming languages and libraries. It works by treating the sha1 output bytes as one large integer in two's complement and then printing the integer in base 16, placing a minus sign if the interpreted number is negative. Some examples of the minecraft digest are found below:

sha1(Notch) :  4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48
sha1(jeb_)  : -7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1
sha1(simon) :  88e16a1019277b15d58faf0541e11910eb756f6

The resulting hash is then sent via an HTTP POST request to

https://sessionserver.mojang.com/session/minecraft/join

With the following sent as post data. You must have the Content-Type header set to application/json or you will get a 415 Unsupported Media Type or 403 Forbidden response.

  {
    "accessToken": "<accessToken>",
    "selectedProfile": "<player's uuid without dashes>",
    "serverId": "<serverHash>"
  }

The fields <accessToken> and the player's uuid were received by the client during authentication.

If everything goes well, the client will receive a "204 No Content" response.

The server will respond with "403 Forbidden" if the player's Xbox profile has multiplayer disabled, with the following response:

{
    "error": "InsufficientPrivilegesException",
    "path": "/session/minecraft/join"
}

Similarly, if the player was banned from Multiplayer then the server will respond with the following error:

{
    "error": "UserBannedException",
    "path": "/session/minecraft/join"
}

If you forget to include a body with your request and just send an empty POST request, or if you use a malformed Content-Type header, you'll get this non-descriptive error:

{
    "error": "Forbidden",
    "path": "/session/minecraft/join"
}

Server

After decrypting the shared secret in the second Encryption Response, the server generates the login hash as above and sends a HTTP GET to

https://sessionserver.mojang.com/session/minecraft/hasJoined?username=username&serverId=hash&ip=ip

The username is case insensitive and must match the client's username (which was received in the Login Start packet). Note that this is the in-game nickname of the selected profile, not the Mojang account name (which is never sent to the server). Servers should use the name sent in the "name" field.

The ip field is optional and when present should be the IP address of the connecting player; it is the one that originally initiated the session request. The notchian server includes this only when prevent-proxy-connections is set to true in server.properties.

The response is a JSON object containing the user's UUID and skin blob

{
    "id": "<profile identifier>",
    "name": "<player name>",
    "properties": [ 
        {
            "name": "textures",
            "value": "<base64 string>",
            "signature": "<base64 string; signed data using Yggdrasil's private key>"
        }
    ]
}

The "id" and "name" fields are then sent back to the client using a Login Success packet. The profile id in the json response has format "11111111222233334444555555555555" which needs to be changed into format "11111111-2222-3333-4444-555555555555" before sending it back to the client.

Sample Code

Examples of generating Minecraft-style hex digests:

Additional Links

DER Encoding of ASN.1 Types

A Layman's Guide to a Subset of ASN.1, BER, and DER

Serializing an RSA Key Manually

Encrypt shared secret using OpenSSL

Generate RSA-Keys and building the ASN.1v8 structure of the x.509 certificate using Crypto++

Decrypt shared secret using Crypto++

De/Encrypt data via AES using Crypto++

C# AES/CFB support with bouncy castle on Mono