Difference between revisions of "ZH:Protocol Encryption"

From wiki.vg
Jump to navigation Jump to search
 
(2 intermediate revisions by the same user not shown)
Line 117: Line 117:
  
 
Examples of generating Minecraft-style hex digests:
 
Examples of generating Minecraft-style hex digests:
一些关于生成Minecraft风格的以二进制呈现的十六进制数据例子
+
一些关于生成Minecraft风格的十六进制数据的例子
  
 
* C#: https://git.io/fhjp6
 
* C#: https://git.io/fhjp6
Line 127: Line 127:
 
* Rust: https://git.io/fj6P0
 
* 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]
 
[https://msdn.microsoft.com/en-us/library/windows/desktop/bb648640(v=vs.85).aspx DER Encoding of ASN.1 Types]
  

Latest revision as of 11:57, 20 December 2019

自快照12w17a起,Minecraft对开启正版验证的服务器使用加密的连接。

概览

  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
  1. 参见协议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风格的十六进制数据的例子

参考链接

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