diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index 58e7a41f..caa38846 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -310,7 +310,7 @@ bool WinsockNetLayer::SendAckAndActivateClientSendCipher() { // Activate send cipher immediately after the ack is on the wire EnterCriticalSection(&s_clientCipherLock); - s_clientSendCipher.Initialize(s_clientPendingKey); + s_clientSendCipher.Initialize(s_clientPendingKey, ServerRuntime::Security::StreamCipher::Client); LeaveCriticalSection(&s_clientCipherLock); app.DebugPrintf("Client: Send cipher activated (MC|CAck sent)\n"); } @@ -329,7 +329,7 @@ bool WinsockNetLayer::SendAckAndActivateClientSendCipher() void WinsockNetLayer::ActivateClientRecvCipher() { EnterCriticalSection(&s_clientCipherLock); - s_clientRecvCipher.Initialize(s_clientPendingKey); + s_clientRecvCipher.Initialize(s_clientPendingKey, ServerRuntime::Security::StreamCipher::Client); SecureZeroMemory(s_clientPendingKey, sizeof(s_clientPendingKey)); s_clientKeyStored = false; LeaveCriticalSection(&s_clientCipherLock); diff --git a/Minecraft.Server/Security/IdentityTokenManager.cpp b/Minecraft.Server/Security/IdentityTokenManager.cpp index 3cba5eed..f20a3d85 100644 --- a/Minecraft.Server/Security/IdentityTokenManager.cpp +++ b/Minecraft.Server/Security/IdentityTokenManager.cpp @@ -1,6 +1,9 @@ #include "stdafx.h" #include "IdentityTokenManager.h" -#include "StreamCipher.h" + +#ifdef _WINDOWS64 +#include +#endif #include "..\Common\FileUtils.h" #include "..\Common\StringUtils.h" @@ -136,15 +139,20 @@ namespace ServerRuntime bool IdentityTokenManager::IssueToken(PlayerUID xuid, uint8_t outToken[TOKEN_SIZE]) { - // Generate a random 32-byte token using two 16-byte CryptGenRandom calls + // Generate a random 32-byte identity token uint8_t token[TOKEN_SIZE]; - bool ok1 = StreamCipher::GenerateKey(token); - bool ok2 = StreamCipher::GenerateKey(token + StreamCipher::KEY_SIZE); - if (!ok1 || !ok2) +#ifdef _WINDOWS64 + NTSTATUS status = BCryptGenRandom(nullptr, token, TOKEN_SIZE, + BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (!BCRYPT_SUCCESS(status)) { SecureZeroMemory(token, sizeof(token)); return false; } +#else + for (int i = 0; i < TOKEN_SIZE; ++i) + token[i] = static_cast(rand() & 0xFF); +#endif EnterCriticalSection(&m_lock); m_tokens[xuid] = std::vector(token, token + TOKEN_SIZE); diff --git a/Minecraft.Server/Security/StreamCipher.cpp b/Minecraft.Server/Security/StreamCipher.cpp index acc7b4b8..1275b566 100644 --- a/Minecraft.Server/Security/StreamCipher.cpp +++ b/Minecraft.Server/Security/StreamCipher.cpp @@ -2,9 +2,8 @@ #include "StreamCipher.h" #ifdef _WINDOWS64 -#include -#include -#pragma comment(lib, "Advapi32.lib") +#include +#pragma comment(lib, "bcrypt.lib") #endif #include @@ -14,29 +13,144 @@ namespace ServerRuntime namespace Security { StreamCipher::StreamCipher() - : m_sendPos(0) - , m_recvPos(0) + : m_sendKeystreamPos(AES_BLOCK) + , m_recvKeystreamPos(AES_BLOCK) , m_active(false) { - memset(m_key, 0, sizeof(m_key)); +#ifdef _WINDOWS64 + m_hAlg = nullptr; + m_hKey = nullptr; +#endif + memset(m_sendCounter, 0, sizeof(m_sendCounter)); + memset(m_recvCounter, 0, sizeof(m_recvCounter)); + memset(m_sendKeystream, 0, sizeof(m_sendKeystream)); + memset(m_recvKeystream, 0, sizeof(m_recvKeystream)); } - void StreamCipher::Initialize(const uint8_t key[KEY_SIZE]) + StreamCipher::~StreamCipher() { - memcpy(m_key, key, KEY_SIZE); - m_sendPos = 0; - m_recvPos = 0; + Reset(); + } + + void StreamCipher::Initialize(const uint8_t key[KEY_SIZE], Role role) + { + if (m_active) + { + Reset(); + } + +#ifdef _WINDOWS64 + NTSTATUS status; + + status = BCryptOpenAlgorithmProvider(&m_hAlg, BCRYPT_AES_ALGORITHM, nullptr, 0); + if (!BCRYPT_SUCCESS(status)) + { + m_hAlg = nullptr; + return; + } + + // Set ECB mode -- we manage CTR ourselves for streaming support + status = BCryptSetProperty(m_hAlg, BCRYPT_CHAINING_MODE, + (PUCHAR)BCRYPT_CHAIN_MODE_ECB, sizeof(BCRYPT_CHAIN_MODE_ECB), 0); + if (!BCRYPT_SUCCESS(status)) + { + BCryptCloseAlgorithmProvider(m_hAlg, 0); + m_hAlg = nullptr; + return; + } + + // Create symmetric key from first 16 bytes + status = BCryptGenerateSymmetricKey(m_hAlg, &m_hKey, nullptr, 0, + (PUCHAR)key, AES_BLOCK, 0); + if (!BCRYPT_SUCCESS(status)) + { + BCryptCloseAlgorithmProvider(m_hAlg, 0); + m_hAlg = nullptr; + m_hKey = nullptr; + return; + } + + // Derive separate counters for send and recv to prevent CTR nonce reuse. + // Flipping the top bit of byte 0 guarantees the two counter spaces never + // overlap (one in 0x00-0x7F range, the other in 0x80-0xFF for byte 0). + // Server send = IV, Server recv = IV^0x80 + // Client send = IV^0x80, Client recv = IV + // This ensures: server-send matches client-recv, client-send matches server-recv. + uint8_t ivBase[AES_BLOCK]; + uint8_t ivFlipped[AES_BLOCK]; + memcpy(ivBase, key + AES_BLOCK, AES_BLOCK); + memcpy(ivFlipped, key + AES_BLOCK, AES_BLOCK); + ivFlipped[0] ^= 0x80; + + if (role == Server) + { + memcpy(m_sendCounter, ivBase, AES_BLOCK); + memcpy(m_recvCounter, ivFlipped, AES_BLOCK); + } + else + { + memcpy(m_sendCounter, ivFlipped, AES_BLOCK); + memcpy(m_recvCounter, ivBase, AES_BLOCK); + } + + SecureZeroMemory(ivBase, sizeof(ivBase)); + SecureZeroMemory(ivFlipped, sizeof(ivFlipped)); + + m_sendKeystreamPos = AES_BLOCK; // force generation on first use + m_recvKeystreamPos = AES_BLOCK; m_active = true; +#endif } void StreamCipher::Reset() { - SecureZeroMemory(m_key, sizeof(m_key)); - m_sendPos = 0; - m_recvPos = 0; +#ifdef _WINDOWS64 + if (m_hKey != nullptr) + { + BCryptDestroyKey(m_hKey); + m_hKey = nullptr; + } + if (m_hAlg != nullptr) + { + BCryptCloseAlgorithmProvider(m_hAlg, 0); + m_hAlg = nullptr; + } +#endif + SecureZeroMemory(m_sendCounter, sizeof(m_sendCounter)); + SecureZeroMemory(m_recvCounter, sizeof(m_recvCounter)); + SecureZeroMemory(m_sendKeystream, sizeof(m_sendKeystream)); + SecureZeroMemory(m_recvKeystream, sizeof(m_recvKeystream)); + m_sendKeystreamPos = AES_BLOCK; + m_recvKeystreamPos = AES_BLOCK; m_active = false; } + void StreamCipher::IncrementCounter(uint8_t counter[AES_BLOCK]) + { + // Big-endian 128-bit increment (standard NIST CTR convention) + for (int i = AES_BLOCK - 1; i >= 0; --i) + { + if (++counter[i] != 0) + break; + } + } + + void StreamCipher::GenerateKeystream(uint8_t counter[AES_BLOCK], uint8_t keystream[AES_BLOCK]) + { +#ifdef _WINDOWS64 + ULONG cbResult = 0; + NTSTATUS status = BCryptEncrypt(m_hKey, counter, AES_BLOCK, nullptr, + nullptr, 0, keystream, AES_BLOCK, &cbResult, 0); // flags=0: exact block, no padding + if (!BCRYPT_SUCCESS(status)) + { + SecureZeroMemory(keystream, AES_BLOCK); + m_active = false; + return; + } + IncrementCounter(counter); +#endif + } + void StreamCipher::Encrypt(uint8_t *data, int length) { if (!m_active || data == nullptr || length <= 0) @@ -46,8 +160,12 @@ namespace ServerRuntime for (int i = 0; i < length; ++i) { - data[i] ^= m_key[m_sendPos]; - m_sendPos = (m_sendPos + 1) % KEY_SIZE; + if (m_sendKeystreamPos >= AES_BLOCK) + { + GenerateKeystream(m_sendCounter, m_sendKeystream); + m_sendKeystreamPos = 0; + } + data[i] ^= m_sendKeystream[m_sendKeystreamPos++]; } } @@ -60,25 +178,23 @@ namespace ServerRuntime for (int i = 0; i < length; ++i) { - data[i] ^= m_key[m_recvPos]; - m_recvPos = (m_recvPos + 1) % KEY_SIZE; + if (m_recvKeystreamPos >= AES_BLOCK) + { + GenerateKeystream(m_recvCounter, m_recvKeystream); + m_recvKeystreamPos = 0; + } + data[i] ^= m_recvKeystream[m_recvKeystreamPos++]; } } bool StreamCipher::GenerateKey(uint8_t outKey[KEY_SIZE]) { #ifdef _WINDOWS64 - HCRYPTPROV hProv = 0; - if (!CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) - { - return false; - } - - BOOL result = CryptGenRandom(hProv, KEY_SIZE, outKey); - CryptReleaseContext(hProv, 0); - return result != FALSE; + NTSTATUS status = BCryptGenRandom(nullptr, outKey, KEY_SIZE, + BCRYPT_USE_SYSTEM_PREFERRED_RNG); + return BCRYPT_SUCCESS(status); #else - // Fallback: not cryptographically random, but better than nothing + // Fallback: not cryptographically random for (int i = 0; i < KEY_SIZE; ++i) { outKey[i] = static_cast(rand() & 0xFF); diff --git a/Minecraft.Server/Security/StreamCipher.h b/Minecraft.Server/Security/StreamCipher.h index 75343fa2..5db2327f 100644 --- a/Minecraft.Server/Security/StreamCipher.h +++ b/Minecraft.Server/Security/StreamCipher.h @@ -2,68 +2,96 @@ #include +#ifdef _WINDOWS64 +#include +#include +#endif + namespace ServerRuntime { namespace Security { /** - * Lightweight XOR stream cipher for traffic obfuscation. + * AES-128-CTR stream cipher for game traffic encryption. * - * This is NOT cryptographically secure. It prevents passive packet sniffing - * (e.g., Wireshark-based XUID harvesting) but does not protect against - * active man-in-the-middle attacks. For real encryption, use TLS via a - * reverse proxy (stunnel, nginx stream). + * Uses the Windows BCrypt API to generate AES-encrypted keystream + * blocks that are XOR'd with plaintext. Each direction (send/recv) + * maintains its own counter for independent keystream generation. + * + * Key material: 32 bytes (16-byte AES key + 16-byte IV/nonce). + * The IV is used as the initial counter block for both directions. * * Usage: - * 1. Server generates a random 16-byte key during PreLogin handshake - * 2. Key is sent to the client (in a SecurityHandshakePacket) - * 3. Both sides create a StreamCipher with the same key - * 4. All subsequent TCP traffic is XOR'd through the cipher - * 5. The cipher maintains separate send/recv rolling key positions + * 1. Server generates a random 32-byte key via GenerateKey() + * 2. Key is sent to the client in the MC|CKey CustomPayloadPacket + * 3. Both sides call Initialize() with the same 32 bytes + * 4. All subsequent TCP traffic is encrypted via Encrypt/Decrypt */ class StreamCipher { public: - static const int KEY_SIZE = 16; + static const int KEY_SIZE = 32; // 16 AES key + 16 IV + + enum Role { Server, Client }; StreamCipher(); + ~StreamCipher(); + + StreamCipher(const StreamCipher &) = delete; + StreamCipher &operator=(const StreamCipher &) = delete; + StreamCipher(StreamCipher &&) = delete; + StreamCipher &operator=(StreamCipher &&) = delete; /** - * Initialize with a key. Call before any encrypt/decrypt. + * Initialize with key material. First 16 bytes = AES key, last 16 bytes = IV. + * Role determines counter assignment to prevent nonce reuse between directions: + * Server: send=IV, recv=IV^0x80 (top bit flipped) + * Client: send=IV^0x80, recv=IV + * This ensures server-send matches client-recv and vice versa. */ - void Initialize(const uint8_t key[KEY_SIZE]); + void Initialize(const uint8_t key[KEY_SIZE], Role role = Server); /** - * XOR-encrypt data in place for sending. - * Advances the send key position. + * AES-CTR encrypt data in place for sending. */ void Encrypt(uint8_t *data, int length); /** - * XOR-decrypt data in place after receiving. - * Advances the recv key position. + * AES-CTR decrypt data in place after receiving. */ void Decrypt(uint8_t *data, int length); /** - * Returns true if the cipher has been initialized with a key. + * Returns true if the cipher has been initialized. */ bool IsActive() const { return m_active; } /** - * Reset to inactive state and securely wipe key material. + * Reset to inactive state and securely wipe all key material. */ void Reset(); /** - * Generates a cryptographically random key using CryptGenRandom (Windows). + * Generate 32 cryptographically random bytes (16 AES key + 16 IV). */ static bool GenerateKey(uint8_t outKey[KEY_SIZE]); private: - uint8_t m_key[KEY_SIZE]; - int m_sendPos; - int m_recvPos; + static const int AES_BLOCK = 16; + + static void IncrementCounter(uint8_t counter[AES_BLOCK]); + void GenerateKeystream(uint8_t counter[AES_BLOCK], uint8_t keystream[AES_BLOCK]); + +#ifdef _WINDOWS64 + BCRYPT_ALG_HANDLE m_hAlg; + BCRYPT_KEY_HANDLE m_hKey; +#endif + uint8_t m_sendCounter[AES_BLOCK]; + uint8_t m_recvCounter[AES_BLOCK]; + uint8_t m_sendKeystream[AES_BLOCK]; + uint8_t m_recvKeystream[AES_BLOCK]; + int m_sendKeystreamPos; + int m_recvKeystreamPos; bool m_active; }; } diff --git a/Minecraft.World/CustomPayloadPacket.h b/Minecraft.World/CustomPayloadPacket.h index b06951b8..d6b7ef35 100644 --- a/Minecraft.World/CustomPayloadPacket.h +++ b/Minecraft.World/CustomPayloadPacket.h @@ -18,7 +18,7 @@ public: static const wstring SET_ITEM_NAME_PACKET; // Security: stream cipher handshake channels - static const wstring CIPHER_KEY_CHANNEL; // server->client: carries 16-byte key + static const wstring CIPHER_KEY_CHANNEL; // server->client: carries 32-byte key (16 AES key + 16 IV) static const wstring CIPHER_ACK_CHANNEL; // client->server: ack (empty payload) static const wstring CIPHER_ON_CHANNEL; // server->client: activation signal (empty payload) diff --git a/README.md b/README.md index a5549540..910f0948 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The dedicated server now includes a comprehensive security system to protect aga | Key | Default | Description | |-----|---------|-------------| -| `enable-stream-cipher` | `true` | Encrypt all game traffic with a per-session stream cipher | +| `enable-stream-cipher` | `true` | Encrypt all game traffic with AES-128-CTR | | `require-secure-client` | `true` | Kick clients that don't complete the cipher handshake (blocks old clients) | | `require-challenge-token` | `false` | Require identity token verification to prevent XUID impersonation | | `proxy-protocol` | `false` | Parse PROXY protocol v1 headers for real client IPs behind a tunnel |