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

212 lines
8.4 KiB
C++

#pragma once
/*
* MCAuthManager - Thread-safe singleton that wraps JavaAuthManager with
* multi-account and per-slot session support (for splitscreen).
*
* Architecture:
*
* - Account list (m_javaAccounts) is SHARED — one global list of saved accounts.
* - Each player slot (0..XUSER_MAX_COUNT-1) has its own AuthSlot containing:
* - A shared_ptr<JavaAuthManager> (ownership shared with background worker)
* - Its own session, mutex, generation counter
* - Slot 0 is the primary player. Slots 1-3 are splitscreen.
*
* Thread model (fire-and-forget):
* - Background workers are detached threads that capture a shared_ptr copy
* of the auth engine they operate on.
* - To cancel: increment generation + RequestCancel on the old auth engine,
* then create a fresh shared_ptr<JavaAuthManager> for the slot.
* - The old thread runs to completion harmlessly — generation mismatch causes
* it to discard results, and the old auth engine is freed when the thread
* exits (shared_ptr ref-count drops to zero).
* - The main thread NEVER calls join(). No blocking. Ever.
*
* Usage:
* auto& mgr = MCAuthManager::Get();
* mgr.LoadJavaAccountIndex();
* mgr.TryRestoreActiveJavaAccount(); // restore slot 0
*
* mgr.SetAccountForSlot(0, accountIndex); // switch slot 0's account
* auto session = mgr.GetSlotSession(0); // get slot 0's session
*/
#include "MCAuth.h"
#include <functional>
#include <mutex>
#include <atomic>
#include <string>
#include <vector>
#include <memory>
#include <condition_variable>
#ifndef XUSER_MAX_COUNT
#define XUSER_MAX_COUNT 4
#endif
class MCAuthManager {
public:
enum class State {
Idle,
WaitingForCode, // device code displayed, waiting for user
Authenticating, // polling / running auth chain
Success,
Failed,
};
// Per-account info stored in the index.
struct JavaAccountInfo {
std::string username; // Minecraft username
std::string uuid; // Player UUID (dashed)
std::string tokenFile; // e.g. "java_auth_0.json" (empty for offline)
bool isOffline = false;
std::string authProvider; // "mojang", "elyby", "offline" (default: "mojang")
};
// Per-player-slot auth state. Each slot has its own auth engine + session.
struct AuthSlot {
// Auth engine is heap-allocated and shared with background worker threads.
// When we cancel, we abandon the old shared_ptr and create a fresh one.
// The old thread's copy prevents use-after-free; it finishes harmlessly.
std::shared_ptr<MCAuth::JavaAuthManager> auth;
MCAuth::ElybyTokens elybyTokens; // Ely.by tokens (no engine class needed)
MCAuth::JavaSession session; // current session data
std::atomic<int> accountIndex{-1}; // index into m_javaAccounts (-1 = none)
mutable std::mutex mutex;
std::atomic<uint32_t> generation{0}; // invalidates stale workers
std::atomic<State> state{State::Idle};
mutable std::condition_variable cv; // signals when state changes
std::string lastError;
// No std::thread member — workers are detached (fire-and-forget).
bool hasSession() const { return !session.uuid.empty(); }
AuthSlot() : auth(std::make_shared<MCAuth::JavaAuthManager>()) {}
};
using DeviceCodeCb = MCAuth::DeviceCodeCallback;
using JavaCompleteCb = std::function<void(bool, const MCAuth::JavaSession&, const std::string&)>;
static MCAuthManager& Get();
MCAuthManager(const MCAuthManager&) = delete;
MCAuthManager& operator=(const MCAuthManager&) = delete;
// ---- Java Edition — Account Index ----
bool LoadJavaAccountIndex();
bool SaveJavaAccountIndex() const;
std::vector<JavaAccountInfo> GetJavaAccounts() const;
// ---- Java Edition — Per-Slot Session Management ----
// Get the auth slot for a player index (0 = primary, 1-3 = splitscreen).
const AuthSlot& GetSlot(int slot) const;
// Set which account a slot uses. Loads tokens + refreshes in background.
// The old session remains valid until the new one is ready (no empty window).
bool SetAccountForSlot(int slot, int accountIndex);
// Clear a slot (e.g. when a splitscreen player leaves).
void ClearSlot(int slot);
// Get the session for a specific slot.
MCAuth::JavaSession GetSlotSession(int slot) const;
// Check if a slot has a valid logged-in session.
bool IsSlotLoggedIn(int slot) const;
// Check if the slot's token is expiring soon (within 60s).
bool IsTokenExpiringSoon(int slot) const;
// Proactively refresh a slot's token in the background.
void RefreshSlot(int slot);
// Block until a slot's auth settles (state leaves Authenticating/WaitingForCode).
// Returns true if login succeeded, false on timeout or failure.
bool WaitForSlotReady(int slot, int timeoutMs = 15000) const;
// Check if an online account is already assigned to another slot.
bool IsAccountInUseByOtherSlot(int slot, int accountIndex) const;
// ---- Java Edition — Slot 0 Convenience (backward compat) ----
int GetActiveJavaAccountIndex() const;
bool SetActiveJavaAccount(int index) { return SetAccountForSlot(0, index); }
MCAuth::JavaSession GetJavaSession() const;
bool IsJavaLoggedIn() const;
State GetState() const { return m_slots[0].state.load(); }
State GetSlotState(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) slot = 0;
return m_slots[slot].state.load();
}
bool WaitForAuthReady(int timeoutMs = 15000) const { return WaitForSlotReady(0, timeoutMs); }
// ---- Java Edition — Account Management ----
// Add a new account via device code flow (background thread on slot 0).
void BeginAddJavaAccount(DeviceCodeCb onDeviceCode, JavaCompleteCb onComplete,
int timeoutSeconds = 300);
// Add an Ely.by account via username+password.
using ElybyCompleteCb = std::function<void(bool ok, const MCAuth::JavaSession& session, const std::string& error)>;
using Elyby2FACb = std::function<void()>;
void BeginAddElybyAccount(const std::string& username, const std::string& password,
ElybyCompleteCb onComplete, Elyby2FACb on2FA = nullptr);
// Add an offline account. Returns new account index or -1.
int AddOfflineJavaAccount(const std::string& username);
// Remove an account by index. Deletes its token file.
bool RemoveJavaAccount(int index);
// Restore the active account from disk at startup (slot 0).
void TryRestoreActiveJavaAccount();
// ---- Device code info (valid during add-account flow) ----
std::string GetJavaDeviceCode() const;
std::string GetJavaDirectUri() const;
std::string GetLastError() const;
// File paths
static constexpr const char* kJavaAccountsFile = "java_accounts.json";
private:
MCAuthManager();
~MCAuthManager();
// Generate a unique token filename for a new account.
std::string AllocTokenFile(const std::string& uuid) const;
// Synthesize an offline session from account info.
static MCAuth::JavaSession SynthesizeOfflineSession(const JavaAccountInfo& acct);
// Abandon the current auth engine for a slot and create a fresh one.
// Calls RequestCancel on the old engine so its worker exits promptly.
// Returns the fresh shared_ptr for capturing in the new worker lambda.
std::shared_ptr<MCAuth::JavaAuthManager> ResetSlotAuth(AuthSlot& s);
// Shared Ely.by refresh logic used by RefreshSlot and SetAccountForSlot.
// If failOpen is true, sets state to Success even when refresh fails (fail-open).
// If alwaysSaveIndex is true, calls SaveJavaAccountIndex even on failure.
void RunElybyRefresh(int slot, uint32_t gen, const std::string& tokenFile,
bool failOpen, bool alwaysSaveIndex);
// Per-slot auth state
AuthSlot m_slots[XUSER_MAX_COUNT];
// Shared account list (protected by m_accountsMutex)
mutable std::mutex m_accountsMutex;
std::vector<JavaAccountInfo> m_javaAccounts;
// Device code state (for BeginAddJavaAccount, always on slot 0)
mutable std::mutex m_deviceCodeMutex;
std::string m_javaDeviceCode;
std::string m_javaDirectUri;
bool m_javaRestoreAttempted = false;
};