MinecraftConsoles/MCAuth/include/MCAuth.h
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

231 lines
8.5 KiB
C++

#pragma once
/*
* MCAuth - Microsoft/Xbox Live Authentication library for Minecraft Java Edition
* C++ port of the MinecraftAuth Java library by RaphiMC
*
* Usage:
* MCAuth::JavaAuthManager auth;
* MCAuth::JavaSession session;
* std::string error;
*
* bool ok = auth.Login(
* [](const MCAuth::DeviceCodeInfo& dc) {
* // Show dc.userCode and dc.directUri to the user
* },
* session, error
* );
*/
#include <string>
#include <functional>
#include <cstdint>
#include <memory>
#include <vector>
namespace MCAuth {
// ----- Error codes -----
enum class AuthErrorCode {
None = 0,
NetworkTimeout, // connection timed out or DNS failure
NetworkError, // general HTTP/TLS failure (send/receive)
HttpError, // unexpected HTTP status code
InvalidCredentials, // wrong token, expired refresh token, account not authorised
TokenExpired, // access token expired (needs refresh)
ServerUnavailable, // Mojang/MS service returned 5xx
RateLimited, // HTTP 429
ProfileNotOwned, // MS account exists but doesn't own MC Java Edition
Cancelled, // operation was cancelled (e.g. device code timeout)
InternalError, // unexpected exception / crypto failure
};
struct AuthError {
AuthErrorCode code = AuthErrorCode::None;
std::string message;
int httpStatus = 0; // 0 if not applicable
bool ok() const { return code == AuthErrorCode::None; }
explicit operator bool() const { return !ok(); } // true means error
};
// ----- Constants ----
// Java Edition title ID (Win32, used for SISU auth flow)
static constexpr const char* JAVA_CLIENT_ID = "00000000402b5328";
static constexpr const char* MSA_SCOPE = "service::user.auth.xboxlive.com::MBI_SSL";
// ----- Public data types -----
struct DeviceCodeInfo {
std::string userCode; // e.g. "ABCD1234"
std::string verificationUri; // e.g. "https://www.microsoft.com/link"
std::string directUri; // verificationUri + "?otc=" + userCode
std::string deviceCode; // internal polling token
int64_t expiresMs; // absolute timestamp (ms since epoch)
int64_t intervalMs; // polling interval in ms
};
using DeviceCodeCallback = std::function<void(const DeviceCodeInfo&)>;
struct JavaSession {
std::string username; // Minecraft username (from /minecraft/profile)
std::string uuid; // Player UUID (dashed, e.g. "069a79f4-44e9-4726-a5be-fca90e38aaf5")
std::string accessToken; // Full Authorization header value ("Bearer <token>")
int64_t expireMs; // When the access token expires (ms since epoch)
};
// ----- Java Edition auth manager -----
class JavaAuthManager {
public:
JavaAuthManager();
~JavaAuthManager();
JavaAuthManager(const JavaAuthManager&) = delete;
JavaAuthManager& operator=(const JavaAuthManager&) = delete;
// Synchronous device-code login (blocks until complete or timeout).
bool Login(DeviceCodeCallback onDeviceCode,
JavaSession& outSession,
std::string& error,
int timeoutSeconds = 300);
// Refresh using the saved MSA refresh token.
bool Refresh(JavaSession& outSession, std::string& error);
bool IsLoggedIn() const;
void Logout();
// Signal a blocking Login() to abort as soon as possible.
// Thread-safe — can be called from any thread while Login() is running.
void RequestCancel();
bool SaveTokens(const std::string& filePath) const;
bool LoadTokens(const std::string& filePath);
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
// ----- Ely.by Yggdrasil auth (free functions, no class) -----
struct ElybyTokens {
std::string accessToken;
std::string clientToken;
std::string uuid; // undashed
std::string username;
};
// POST https://authserver.ely.by/auth/authenticate
// On 2FA requirement, error is set to "elyby_2fa_required"; caller retries with password = "pass:totp_code"
bool ElybyLogin(const std::string& username, const std::string& password,
ElybyTokens& outTokens, std::string& error);
// POST https://authserver.ely.by/auth/refresh
bool ElybyRefresh(ElybyTokens& tokens, std::string& error);
// POST https://authserver.ely.by/auth/validate
bool ElybyValidate(const std::string& accessToken, std::string& error);
// Token persistence — simple JSON: {accessToken, clientToken, uuid, username}
bool ElybyLoadTokens(const std::string& path, ElybyTokens& out);
bool ElybySaveTokens(const std::string& path, const ElybyTokens& tokens);
// ----- Skin key utility -----
// Build the canonical memory-texture key for a Mojang skin.
// uuid can be dashed or undashed — dashes are stripped automatically.
// Returns e.g. "mojang_skin_069a79f444e94726a5befca90e38aaf5.png"
std::string MakeSkinKey(const std::string& uuid);
// ----- UUID utilities -----
// Remove dashes from a UUID string: "069a79f4-44e9-..." → "069a79f444e9..."
std::string UndashUuid(const std::string& dashed);
// Insert dashes into a 32-char hex UUID: "069a79f444e9..." → "069a79f4-44e9-..."
std::string DashUuid(const std::string& undashed);
// Generate a Minecraft-compatible offline UUID (UUID v3, namespace "OfflinePlayer:" + username).
// Returns dashed format.
std::string GenerateOfflineUuid(const std::string& username);
// Parse a dashed UUID into two 64-bit halves (big-endian).
struct Uuid128 {
uint64_t hi = 0;
uint64_t lo = 0;
bool isValid() const { return hi != 0 || lo != 0; }
};
Uuid128 ParseUuid128(const std::string& dashed);
// ----- Session verification (Mojang sessionserver) -----
// Client-side: call BEFORE connecting to an online-mode server.
// Posts to sessionserver.mojang.com/session/minecraft/join.
// accessToken = raw token (without "Bearer " prefix).
// undashedUuid = 32-char hex UUID of the player.
// serverId = the server's challenge string.
// Returns true on success (HTTP 204).
bool JoinServer(const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
std::string& error);
// Server-side result from HasJoined verification.
struct HasJoinedResult {
bool success = false;
AuthError error; // structured error (code + message + httpStatus)
std::string username; // verified username from Mojang
std::string uuid; // undashed UUID
std::string skinUrl; // Mojang skin texture URL (from base64 properties)
std::string capeUrl; // Mojang cape texture URL (from base64 properties)
};
// Server-side: verify that a player has called /join.
// GETs sessionserver.mojang.com/session/minecraft/hasJoined?username=X&serverId=Y.
// Returns success=true with username+uuid on HTTP 200, success=false on 204 (not joined).
HasJoinedResult HasJoined(const std::string& username,
const std::string& serverId,
std::string& error);
// Download a skin/cape PNG from a URL.
// Returns the raw PNG bytes, or empty vector on failure.
std::vector<uint8_t> FetchSkinPng(const std::string& url, std::string& error);
// Fetch the skin URL for a player UUID from Mojang's session server.
// uuid can be dashed or undashed.
// Returns the skin texture URL, or empty string on failure.
std::string FetchProfileSkinUrl(const std::string& uuid, std::string& error);
// Download a skin PNG and return the raw bytes WITHOUT cropping to 64x32.
// Use this when you need the full 64x64 texture (e.g. for rendering the head).
std::vector<uint8_t> FetchSkinPngRaw(const std::string& url, std::string& error);
// ----- Ely.by session functions (same Yggdrasil protocol, different URLs) -----
bool ElybyJoinServer(const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
std::string& error);
HasJoinedResult ElybyHasJoined(const std::string& username,
const std::string& serverId,
std::string& error);
std::string ElybyFetchProfileSkinUrl(const std::string& uuid, std::string& error);
std::string MakeElybySkinKey(const std::string& uuid);
// ----- Skin validation -----
// Maximum allowed skin PNG size in bytes (32KB — a 64x64 RGBA PNG is ~4KB compressed)
static constexpr size_t kMaxSkinBytes = 32768;
// Check PNG magic, size cap, and 64x32 or 64x64 dimensions.
bool ValidateSkinPng(const uint8_t* data, size_t size);
} // namespace MCAuth