Zh:Protocol Encryption
自快照12w17a起,Minecraft对开启正版验证的服务器使用加密的连接。
Contents
概览
C->S : Handshake State=2 C->S : Login Start S->C : Encryption Key Request (Client Auth) C->S : Encryption Key Response (Server Auth, Both enable encryption) S->C : Login Success
- 参见协议FAQ来获取更多关于下文内容的信息(about what happens next)。
服务器的ID字符串
Update (1.7.x): 服务器ID现在可以用空字符串来代替发送。哈希值也使用公钥(Hashes also utilize the public key),所以它们仍然是正确的。
Pre-1.7.x: 服务器ID是一个随机产生的最大长度为20(Unicode字符)的字符串(客户端将会与服务端失去连接,如果传输的服务器ID字符串长度大于20)。
客户端似乎会生成错误的哈希值,如果传入的服务器ID字符串包含了一些无法被打印的字符。所以为了得到正常的哈希值,我们应该只发送 [U+0021,U+007E] 范围内的Unicode字符。这一范围对应于除了“空格”(U+0020)以及起控制作用的字符(U+0000-U+001F, U+007F)外其他所有的ASCII字符。
客户端似乎也会生成错误的哈希值,如果传入的服务器ID字符串太短。经监视与Notchian Server(?)发送不同长度服务器ID字符串的试验验证,在Minecraft 1.5.2中[12,20]长度范围内的服务器ID字符串可以正常工作。
密钥交换
在服务端启动的时候,服务端会生成一个1024位的RSA密钥对。服务端会以DER编码将公钥写进经加密的数据请求包中。从技术实现层面上讲,这个公钥是以由x.509定义的ASN.1形式呈现,其具体的数据结构如下所示:
SubjectPublicKeyInfo ::= SEQUENCE { algorithm SEQUENCE { algorithm OBJECT IDENTIFIER parameters ANY OPTIONAL } subjectPublicKey BITSTRING } SubjectPublicKey ::= SEQUENCE { modulus INTEGER publicExponent INTEGER }
如果你还在为如何使用加密库导入上述数据而苦恼的话,不妨尝试着去找个(或自己写个)函数来解码DER形式的公钥。如果你无法找到这样的函数,可以先通过base64将DER编码的公钥转化为PEM编码的公钥,然后再将转化好的公钥放在'-----BEGIN PUBLIC KEY-----'和'-----END PUBLIC KEY-----'之间。访问https://git.io/v7Ol9就能看到一个经PEM编码的公钥的例子。 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
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.
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, Content-Type: application/json
{
"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 "HTTP/1.1 204 No Content" response.
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.
样例代码
Examples of generating Minecraft-style hex digests: 一些关于生成Minecraft风格的以二进制呈现的十六进制数据例子
- 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
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++