MinecraftConsoles/Minecraft.World/GameUUID.cpp
MrTheShy 4f2352361a Add Mojang/Ely.by/offline authentication system
The old Windows64 port had no real player identity — it used hardcoded
fake XUIDs, so anyone could impersonate anyone. This replaces that with
proper auth supporting Mojang, Ely.by, and offline accounts.

MCAuth library (new, MCAuth/):
  Mojang auth via MSA device code flow (XBL, SISU, MC services),
  Ely.by via Yggdrasil with 2FA, offline UUID generation matching
  Java Edition (MD5 v3 from "OfflinePlayer:<name>"). Multi-account
  manager with background token refresh, per-slot sessions, and
  on-disk token persistence. Server-side session verification via
  Mojang/Ely.by hasJoined API. Skin fetching and PNG validation
  from texture servers.

Network protocol (version bumped to 80):
  Three new packets (AuthScheme, AuthResponse, AuthResult) implement
  a server-driven auth handshake before login completes. Player
  identity migrated from 64-bit XUID to 128-bit GameUUID backed by
  two uint64 fields (hi/lo). readPlayerUID/writePlayerUID now
  serialize 16 bytes on the wire. Old and new clients cannot connect
  to each other — version mismatch is rejected at PreLogin.

Save migration:
  Map data mappings auto-migrate from old format: the old 64-bit
  XUID is placed in hi, lo is set to 0 as a sentinel. On first
  access by the real player, the sentinel entry is upgraded in-place
  to the full 128-bit UUID. Format detection is by file size (2080,
  2112, or 4160 bytes). Player .dat filenames inside saveData.ms
  change from decimal XUID to dashed UUID — old saves need manual
  entry renaming in the archive.

UI:
  NativeUIRenderer: immediate-mode drawing system (quads, text,
  9-slice panels, scrollbars, focus lists) for rendering auth
  screens without Flash/Scaleform. UIScene_MSAuth handles device
  code display, Ely.by credential input with 2FA, per-account
  skin head preview, and multi-account add/remove/switch.

Server:
  online-mode and auth-provider (mojang/elyby) in server.properties.
  Whitelist and ban checks validate against the server-verified UUID.
  Incompatible auth scheme logs which provider the server expects
  vs what the client is using.

Also fixes a pre-existing exploit where any client could send a
DebugOptionsPacket to grant themselves CraftAnything and other debug
privileges on any server — now requires OP status server-side.
2026-03-23 01:14:23 +01:00

244 lines
6.2 KiB
C++

#include "stdafx.h"
#include "GameUUID.h"
#include <cstdio>
#include <cstring>
#include <ctime>
#ifdef _WIN32
#include <Windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
#endif
static uint8_t hexVal(char c)
{
if (c >= '0' && c <= '9') return (uint8_t)(c - '0');
if (c >= 'a' && c <= 'f') return (uint8_t)(c - 'a' + 10);
if (c >= 'A' && c <= 'F') return (uint8_t)(c - 'A' + 10);
return 0;
}
std::string GameUUID::toDashed() const
{
// Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
char buf[37];
sprintf_s(buf, sizeof(buf),
"%08x-%04x-%04x-%04x-%012llx",
(unsigned int)(hi >> 32),
(unsigned int)((hi >> 16) & 0xFFFF),
(unsigned int)(hi & 0xFFFF),
(unsigned int)(lo >> 48),
(unsigned long long)(lo & 0x0000FFFFFFFFFFFFULL));
return std::string(buf);
}
std::string GameUUID::toUndashed() const
{
char buf[33];
sprintf_s(buf, sizeof(buf),
"%016llx%016llx",
(unsigned long long)hi,
(unsigned long long)lo);
return std::string(buf);
}
std::wstring GameUUID::toWDashed() const
{
std::string s = toDashed();
return std::wstring(s.begin(), s.end());
}
GameUUID GameUUID::fromDashed(const std::string& s)
{
std::string undashed;
undashed.reserve(32);
for (size_t i = 0; i < s.size(); i++)
{
if (s[i] != '-')
undashed.push_back(s[i]);
}
return fromUndashed(undashed);
}
GameUUID GameUUID::fromUndashed(const std::string& s)
{
GameUUID uuid;
if (s.size() < 32)
return uuid;
uuid.hi = 0;
for (int i = 0; i < 16; i++)
{
uuid.hi = (uuid.hi << 4) | hexVal(s[i]);
}
uuid.lo = 0;
for (int i = 16; i < 32; i++)
{
uuid.lo = (uuid.lo << 4) | hexVal(s[i]);
}
return uuid;
}
// ---- UUID v4 (random) ----
GameUUID GameUUID::generateV4()
{
GameUUID uuid;
uint8_t bytes[16] = {};
#ifdef _WIN32
NTSTATUS status = BCryptGenRandom(NULL, bytes, sizeof(bytes), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
if (!BCRYPT_SUCCESS(status))
{
// Fallback: weak but avoids uninitialized memory
for (int i = 0; i < 16; i++) bytes[i] = (uint8_t)(rand() & 0xFF);
}
#else
// Fallback: mix runtime entropy
static uint64_t counter = 0;
uint64_t seed = (uint64_t)time(NULL) ^ (++counter * 6364136223846793005ULL);
for (int i = 0; i < 16; i++)
{
seed = seed * 6364136223846793005ULL + 1442695040888963407ULL;
bytes[i] = (uint8_t)(seed >> 56);
}
#endif
// Set version 4 (bits 48-51 of hi = 0100)
bytes[6] = (bytes[6] & 0x0F) | 0x40;
// Set variant 1 (bits 0-1 of byte 8 = 10)
bytes[8] = (bytes[8] & 0x3F) | 0x80;
uuid.hi = 0;
for (int i = 0; i < 8; i++)
uuid.hi = (uuid.hi << 8) | bytes[i];
uuid.lo = 0;
for (int i = 8; i < 16; i++)
uuid.lo = (uuid.lo << 8) | bytes[i];
return uuid;
}
// ---- UUID v3 (MD5 name-based) for offline players ----
// Matches Java's UUID.nameUUIDFromBytes() used by Mojang for offline UUIDs
// Minimal MD5 implementation (RFC 1321) for offline UUID generation
namespace
{
struct MD5State
{
uint32_t state[4];
uint64_t count;
uint8_t buffer[64];
};
static const uint32_t md5_T[64] = {
0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,
0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,0xa679438e,0x49b40821,
0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,
0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a,
0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,
0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,
0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,
0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391
};
static const int md5_S[64] = {
7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,
5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,
4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,
6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21
};
static inline uint32_t rotl32(uint32_t x, int n) { return (x << n) | (x >> (32 - n)); }
static void md5_transform(uint32_t state[4], const uint8_t block[64])
{
uint32_t M[16];
for (int i = 0; i < 16; i++)
M[i] = (uint32_t)block[i * 4] | ((uint32_t)block[i * 4 + 1] << 8) |
((uint32_t)block[i * 4 + 2] << 16) | ((uint32_t)block[i * 4 + 3] << 24);
uint32_t a = state[0], b = state[1], c = state[2], d = state[3];
for (int i = 0; i < 64; i++)
{
uint32_t f;
int g;
if (i < 16) { f = (b & c) | (~b & d); g = i; }
else if (i < 32) { f = (d & b) | (~d & c); g = (5 * i + 1) % 16; }
else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) % 16; }
else { f = c ^ (b | ~d); g = (7 * i) % 16; }
uint32_t temp = d;
d = c;
c = b;
b = b + rotl32(a + f + md5_T[i] + M[g], md5_S[i]);
a = temp;
}
state[0] += a; state[1] += b; state[2] += c; state[3] += d;
}
static void md5(const uint8_t* data, size_t len, uint8_t digest[16])
{
uint32_t state[4] = { 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476 };
size_t i = 0;
for (; i + 64 <= len; i += 64)
md5_transform(state, data + i);
uint8_t block[64] = {};
size_t remain = len - i;
if (remain > 0)
memcpy(block, data + i, remain);
block[remain] = 0x80;
if (remain >= 56)
{
md5_transform(state, block);
memset(block, 0, 64);
}
uint64_t bitLen = (uint64_t)len * 8;
for (int b = 0; b < 8; b++)
block[56 + b] = (uint8_t)(bitLen >> (b * 8));
md5_transform(state, block);
for (int b = 0; b < 4; b++)
{
digest[b * 4 + 0] = (uint8_t)(state[b]);
digest[b * 4 + 1] = (uint8_t)(state[b] >> 8);
digest[b * 4 + 2] = (uint8_t)(state[b] >> 16);
digest[b * 4 + 3] = (uint8_t)(state[b] >> 24);
}
}
}
GameUUID GameUUID::generateOffline(const std::string& playerName)
{
// Matches Mojang's offline UUID: UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes("UTF-8"))
std::string input = "OfflinePlayer:" + playerName;
uint8_t digest[16];
md5((const uint8_t*)input.c_str(), input.size(), digest);
// Set version 3
digest[6] = (digest[6] & 0x0F) | 0x30;
// Set variant 1
digest[8] = (digest[8] & 0x3F) | 0x80;
GameUUID uuid;
uuid.hi = 0;
for (int i = 0; i < 8; i++)
uuid.hi = (uuid.hi << 8) | digest[i];
uuid.lo = 0;
for (int i = 8; i < 16; i++)
uuid.lo = (uuid.lo << 8) | digest[i];
return uuid;
}