From 186982b458fd1d11926b7116169df51b5547c0fc Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sat, 28 Mar 2026 00:07:18 -0400 Subject: [PATCH 01/18] feat: add basic authentication packet handling. --- Minecraft.World/AuthPackets.cpp | 47 +++++++++++++++++++ Minecraft.World/AuthPackets.h | 36 ++++++++++++++ Minecraft.World/Packet.cpp | 1 + Minecraft.World/PacketListener.cpp | 5 ++ Minecraft.World/PacketListener.h | 4 ++ Minecraft.World/cmake/sources/Common.cmake | 2 + .../net.minecraft.network.packet.h | 1 + 7 files changed, 96 insertions(+) create mode 100644 Minecraft.World/AuthPackets.cpp create mode 100644 Minecraft.World/AuthPackets.h diff --git a/Minecraft.World/AuthPackets.cpp b/Minecraft.World/AuthPackets.cpp new file mode 100644 index 0000000000..4bd2bb9534 --- /dev/null +++ b/Minecraft.World/AuthPackets.cpp @@ -0,0 +1,47 @@ +#include "stdafx.h" +#include "InputOutputStream.h" +#include "PacketListener.h" +#include "AuthPackets.h" + +AuthPacket::AuthPacket(AuthStage stage, vector> fields) + : stage(stage), fields(std::move(fields)) +{ +} + +void AuthPacket::read(DataInputStream *dis) +{ + stage = static_cast(dis->readByte()); + short count = dis->readShort(); + fields.clear(); + fields.reserve(count); + for (short i = 0; i < count; i++) + { + wstring key = readUtf(dis, 256); + wstring value = readUtf(dis, 4096); + fields.emplace_back(std::move(key), std::move(value)); + } +} + +void AuthPacket::write(DataOutputStream *dos) +{ + dos->writeByte(static_cast(stage)); + dos->writeShort(static_cast(fields.size())); + for (const auto &field : fields) + { + writeUtf(field.first, dos); + writeUtf(field.second, dos); + } +} + +void AuthPacket::handle(PacketListener *listener) +{ + listener->handleAuth(shared_from_this()); +} + +int AuthPacket::getEstimatedSize() +{ + int size = 1 + 2; + for (const auto &field : fields) + size += 4 + static_cast((field.first.length() + field.second.length()) * sizeof(wchar_t)); + return size; +} diff --git a/Minecraft.World/AuthPackets.h b/Minecraft.World/AuthPackets.h new file mode 100644 index 0000000000..6c73067f1f --- /dev/null +++ b/Minecraft.World/AuthPackets.h @@ -0,0 +1,36 @@ +#pragma once +using namespace std; + +#include "Packet.h" + +enum class AuthStage : uint8_t +{ + ANNOUNCE_VERSION, + DECLARE_SCHEME, + ACCEPT_SCHEME, + SCHEME_SETTINGS, + BEGIN_AUTH, + AUTH_DATA, + AUTH_DONE, + ASSIGN_IDENTITY, + CONFIRM_IDENTITY, + AUTH_SUCCESS, + AUTH_FAILURE +}; + +class AuthPacket : public Packet, public enable_shared_from_this +{ +public: + AuthStage stage; + vector> fields; + + AuthPacket(AuthStage stage = AuthStage::ANNOUNCE_VERSION, vector> fields = {}); + + virtual void read(DataInputStream *dis); + virtual void write(DataOutputStream *dos); + virtual void handle(PacketListener *listener); + virtual int getEstimatedSize(); + + static shared_ptr create() { return std::make_shared(); } + virtual int getId() { return 72; } +}; diff --git a/Minecraft.World/Packet.cpp b/Minecraft.World/Packet.cpp index 05bf932dcb..325976c633 100644 --- a/Minecraft.World/Packet.cpp +++ b/Minecraft.World/Packet.cpp @@ -150,6 +150,7 @@ void Packet::staticCtor() //map(253, true, false, ServerAuthDataPacket.class); map(254, false, true, false, false, typeid(GetInfoPacket), GetInfoPacket::create); // TODO New for 1.8.2 - Needs sendToAny? map(255, true, true, true, false, typeid(DisconnectPacket), DisconnectPacket::create); + map(72, true, true, true, false, typeid(AuthPacket), AuthPacket::create); } IllegalArgumentException::IllegalArgumentException(const wstring& information) diff --git a/Minecraft.World/PacketListener.cpp b/Minecraft.World/PacketListener.cpp index 09fbb05251..490922c79c 100644 --- a/Minecraft.World/PacketListener.cpp +++ b/Minecraft.World/PacketListener.cpp @@ -491,3 +491,8 @@ void PacketListener::handleGameCommand(shared_ptr packet) { onUnhandledPacket( (shared_ptr ) packet); } + +void PacketListener::handleAuth(shared_ptr packet) +{ + onUnhandledPacket( (shared_ptr ) packet); +} diff --git a/Minecraft.World/PacketListener.h b/Minecraft.World/PacketListener.h index f63b6bc7fd..9f5b1b9de3 100644 --- a/Minecraft.World/PacketListener.h +++ b/Minecraft.World/PacketListener.h @@ -111,6 +111,7 @@ class KickPlayerPacket; class AdditionalModelPartsPacket; class XZPacket; class GameCommandPacket; +class AuthPacket; class PacketListener { @@ -227,4 +228,7 @@ class PacketListener virtual void handleKickPlayer(shared_ptr packet); virtual void handleXZ(shared_ptr packet); virtual void handleGameCommand(shared_ptr packet); + + // MCConsoles Auth + virtual void handleAuth(shared_ptr packet); }; diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index 8a4d683303..544d1e5712 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -259,6 +259,8 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/AddPlayerPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/AnimatePacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AnimatePacket.h" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.h" "${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/BlockRegionUpdatePacket.cpp" diff --git a/Minecraft.World/net.minecraft.network.packet.h b/Minecraft.World/net.minecraft.network.packet.h index c1d52d1473..2981508e36 100644 --- a/Minecraft.World/net.minecraft.network.packet.h +++ b/Minecraft.World/net.minecraft.network.packet.h @@ -108,4 +108,5 @@ #include "KickPlayerPacket.h" #include "XZPacket.h" #include "GameCommandPacket.h" +#include "AuthPackets.h" From 7a24494d3c541a6b7045d1d8a811efb214e53105 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sat, 28 Mar 2026 00:52:17 -0400 Subject: [PATCH 02/18] add: implement authentication module with validation and identity extraction --- Minecraft.World/AuthModule.cpp | 87 ++++++++++++++++++++++ Minecraft.World/AuthModule.h | 63 ++++++++++++++++ Minecraft.World/cmake/sources/Common.cmake | 2 + 3 files changed, 152 insertions(+) create mode 100644 Minecraft.World/AuthModule.cpp create mode 100644 Minecraft.World/AuthModule.h diff --git a/Minecraft.World/AuthModule.cpp b/Minecraft.World/AuthModule.cpp new file mode 100644 index 0000000000..0d5ffb0f4d --- /dev/null +++ b/Minecraft.World/AuthModule.cpp @@ -0,0 +1,87 @@ +#include "stdafx.h" +#include "AuthModule.h" + +bool AuthModule::validate(const wstring &uid, const wstring &username) +{ + return !uid.empty() && !username.empty() && username.length() <= 16; +} + +bool AuthModule::extractIdentity(const vector> &fields, wstring &outUid, wstring &outUsername) +{ + for (const auto &[key, value] : fields) + { + if (key == L"uid") outUid = value; + else if (key == L"username") outUsername = value; + } + return validate(outUid, outUsername); +} + +ElyByAuthModule::ElyByAuthModule(const wstring &endpoint) + : endpoint(endpoint) +{ +} + +const wchar_t *ElyByAuthModule::schemeName() { return L"mcconsoles:elyby"; } + +vector ElyByAuthModule::supportedVariations() +{ + return {L"java"}; +} + +vector> ElyByAuthModule::getSettings(const wstring &variation) +{ + return {{L"endpoint", endpoint}}; +} + +bool ElyByAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) +{ + return extractIdentity(fields, outUid, outUsername); +} + +MojangAuthModule::MojangAuthModule() + : ElyByAuthModule(L"https://sessionserver.mojang.com") +{ +} + +const wchar_t *MojangAuthModule::schemeName() { return L"mcconsoles:mojang"; } + +vector MojangAuthModule::supportedVariations() +{ + return {L"java", L"bedrock"}; +} + +// --- KeypairOfflineAuthModule --- + +const wchar_t *KeypairOfflineAuthModule::schemeName() { return L"mcconsoles:keypair_offline"; } + +vector KeypairOfflineAuthModule::supportedVariations() +{ + return {L"rsa2048", L"ed25519"}; +} + +vector> KeypairOfflineAuthModule::getSettings(const wstring &variation) +{ + return {{L"key_type", variation}}; +} + +bool KeypairOfflineAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) +{ + return extractIdentity(fields, outUid, outUsername); +} + +const wchar_t *OfflineAuthModule::schemeName() { return L"mcconsoles:offline"; } + +vector OfflineAuthModule::supportedVariations() +{ + return {}; +} + +vector> OfflineAuthModule::getSettings(const wstring &variation) +{ + return {}; +} + +bool OfflineAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) +{ + return extractIdentity(fields, outUid, outUsername); +} diff --git a/Minecraft.World/AuthModule.h b/Minecraft.World/AuthModule.h new file mode 100644 index 0000000000..4de6efa16c --- /dev/null +++ b/Minecraft.World/AuthModule.h @@ -0,0 +1,63 @@ +#pragma once +using namespace std; + +#include +#include +#include + +class AuthModule +{ +public: + virtual ~AuthModule() = default; + + virtual const wchar_t *schemeName() = 0; + virtual vector supportedVariations() = 0; + virtual vector> getSettings(const wstring &variation) = 0; + virtual bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) = 0; + + bool validate(const wstring &uid, const wstring &username); + +protected: + bool extractIdentity(const vector> &fields, wstring &outUid, wstring &outUsername); +}; + +class ElyByAuthModule : public AuthModule +{ +protected: + wstring endpoint; + +public: + ElyByAuthModule(const wstring &endpoint = L"https://authserver.ely.by"); + + const wchar_t *schemeName() override; + vector supportedVariations() override; + vector> getSettings(const wstring &variation) override; + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; +}; + +class MojangAuthModule : public ElyByAuthModule +{ +public: + MojangAuthModule(); + + const wchar_t *schemeName() override; + vector supportedVariations() override; +}; + +class KeypairOfflineAuthModule : public AuthModule +{ +public: + const wchar_t *schemeName() override; + vector supportedVariations() override; + vector> getSettings(const wstring &variation) override; + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; +}; + +class OfflineAuthModule : public AuthModule +{ +public: + const wchar_t *schemeName() override; + vector supportedVariations() override; + vector> getSettings(const wstring &variation) override; + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; +}; diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index 544d1e5712..eb4a63793b 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -259,6 +259,8 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/AddPlayerPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/AnimatePacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AnimatePacket.h" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthModule.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthModule.h" "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.h" "${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.cpp" From d42da07721a60d225d98980f739fc656c6140057 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sat, 28 Mar 2026 04:48:18 -0400 Subject: [PATCH 03/18] add: implement authentication system ui and handshake. --- Minecraft.Client/AuthScreen.cpp | 101 +++++++++ Minecraft.Client/AuthScreen.h | 30 +++ Minecraft.Client/ClientConnection.cpp | 37 ++++ Minecraft.Client/ClientConnection.h | 7 + Minecraft.Client/Common/UI/UIController.cpp | 2 +- .../Common/UI/UIScene_MainMenu.cpp | 173 ++++++++++++++- Minecraft.Client/Common/UI/UIScene_MainMenu.h | 8 +- .../Common/UI/UIScene_MessageBox.cpp | 22 +- .../Common/UI/UIScene_MessageBox.h | 5 + Minecraft.Client/Common/UI/UIStructs.h | 3 + Minecraft.Client/ConnectScreen.cpp | 8 +- Minecraft.Client/ConnectScreen.h | 1 + Minecraft.Client/PendingConnection.cpp | 43 ++++ Minecraft.Client/PendingConnection.h | 5 + Minecraft.Client/cmake/sources/Common.cmake | 2 + Minecraft.Server/cmake/sources/Common.cmake | 1 + Minecraft.World/AuthModule.cpp | 2 - Minecraft.World/AuthPackets.cpp | 10 +- Minecraft.World/HandshakeManager.cpp | 205 ++++++++++++++++++ Minecraft.World/HandshakeManager.h | 57 +++++ Minecraft.World/PacketListener.cpp | 2 +- Minecraft.World/PacketListener.h | 3 +- Minecraft.World/cmake/sources/Common.cmake | 2 + 23 files changed, 701 insertions(+), 28 deletions(-) create mode 100644 Minecraft.Client/AuthScreen.cpp create mode 100644 Minecraft.Client/AuthScreen.h create mode 100644 Minecraft.World/HandshakeManager.cpp create mode 100644 Minecraft.World/HandshakeManager.h diff --git a/Minecraft.Client/AuthScreen.cpp b/Minecraft.Client/AuthScreen.cpp new file mode 100644 index 0000000000..706d454b98 --- /dev/null +++ b/Minecraft.Client/AuthScreen.cpp @@ -0,0 +1,101 @@ +#include "stdafx.h" +#include "AuthScreen.h" +#include "Minecraft.h" +#include "User.h" +#include + +static constexpr auto PROFILES_FILE = L"auth_profiles.dat"; + +vector AuthProfileManager::profiles; +int AuthProfileManager::selectedProfile = -1; + +void AuthProfileManager::load() +{ + profiles.clear(); + std::ifstream file(PROFILES_FILE, std::ios::binary); + if (!file) return; + + uint32_t count = 0; + file.read(reinterpret_cast(&count), sizeof(count)); + + for (uint32_t i = 0; i < count && file.good(); i++) + { + AuthProfile p; + uint8_t type; + file.read(reinterpret_cast(&type), sizeof(type)); + p.type = static_cast(type); + + auto readWstr = [&file]() -> wstring { + uint16_t len = 0; + file.read(reinterpret_cast(&len), sizeof(len)); + wstring s(len, L'\0'); + file.read(reinterpret_cast(s.data()), len * sizeof(wchar_t)); + return s; + }; + + p.uid = readWstr(); + p.username = readWstr(); + p.token = readWstr(); + profiles.push_back(std::move(p)); + } + + if (!profiles.empty()) + selectedProfile = 0; +} + +void AuthProfileManager::save() +{ + std::ofstream file(PROFILES_FILE, std::ios::binary | std::ios::trunc); + if (!file) return; + + uint32_t count = static_cast(profiles.size()); + file.write(reinterpret_cast(&count), sizeof(count)); + + auto writeWstr = [&file](const wstring &s) { + uint16_t len = static_cast(s.length()); + file.write(reinterpret_cast(&len), sizeof(len)); + file.write(reinterpret_cast(s.data()), len * sizeof(wchar_t)); + }; + + for (const auto &p : profiles) + { + uint8_t type = static_cast(p.type); + file.write(reinterpret_cast(&type), sizeof(type)); + writeWstr(p.uid); + writeWstr(p.username); + writeWstr(p.token); + } +} + +void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username) +{ + profiles.push_back({type, L"offline_" + username, username, {}}); + selectedProfile = static_cast(profiles.size()) - 1; + save(); +} + +void AuthProfileManager::removeSelectedProfile() +{ + if (selectedProfile < 0 || selectedProfile >= static_cast(profiles.size())) + return; + + profiles.erase(profiles.begin() + selectedProfile); + if (selectedProfile >= static_cast(profiles.size())) + selectedProfile = static_cast(profiles.size()) - 1; + save(); +} + +bool AuthProfileManager::applySelectedProfile() +{ + if (selectedProfile < 0 || selectedProfile >= static_cast(profiles.size())) + return false; + + const auto &p = profiles[selectedProfile]; + auto *mc = Minecraft::GetInstance(); + + if (mc->user) + delete mc->user; + + mc->user = new User(p.username, p.token); + return true; +} diff --git a/Minecraft.Client/AuthScreen.h b/Minecraft.Client/AuthScreen.h new file mode 100644 index 0000000000..f31f294aa4 --- /dev/null +++ b/Minecraft.Client/AuthScreen.h @@ -0,0 +1,30 @@ +#pragma once +using namespace std; + +struct AuthProfile +{ + enum Type : uint8_t { MICROSOFT, ELYBY, OFFLINE }; + + Type type; + wstring uid; + wstring username; + wstring token; +}; + +class AuthProfileManager +{ +private: + static vector profiles; + static int selectedProfile; + +public: + static void load(); + static void save(); + static void addProfile(AuthProfile::Type type, const wstring &username); + static void removeSelectedProfile(); + static bool applySelectedProfile(); + + static const vector &getProfiles() { return profiles; } + static int getSelectedIndex() { return selectedProfile; } + static void setSelectedIndex(int idx) { selectedProfile = idx; } +}; diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index a80af5d2c9..bdace06483 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -54,6 +54,8 @@ #include "PS3/Network/SonyVoiceChat.h" #endif #include "DLCTexturePack.h" +#include "..\Minecraft.World\HandshakeManager.h" +#include "..\Minecraft.World\AuthModule.h" #ifdef _WINDOWS64 #include "Xbox\Network\NetworkPlayerXbox.h" @@ -140,6 +142,9 @@ ClientConnection::ClientConnection(Minecraft *minecraft, Socket *socket, int iUs } deferredEntityLinkPackets = vector(); + + handshakeManager = nullptr; + authComplete = false; } bool ClientConnection::isPrimaryConnection() const @@ -202,6 +207,7 @@ ClientConnection::~ClientConnection() delete connection; delete random; delete savedDataStorage; + delete handshakeManager; } void ClientConnection::tick() @@ -4129,3 +4135,34 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr< m_recievedTick = GetTickCount(); m_packet = packet; } + +void ClientConnection::beginAuth() +{ + handshakeManager = new HandshakeManager(false); + handshakeManager->registerModule(new MojangAuthModule()); + handshakeManager->registerModule(new ElyByAuthModule()); + handshakeManager->registerModule(new KeypairOfflineAuthModule()); + handshakeManager->registerModule(new OfflineAuthModule()); + + auto initial = handshakeManager->createInitialPacket(); + if (initial) send(initial); +} + +void ClientConnection::handleAuth(const shared_ptr &packet) +{ + if (done || authComplete) return; + if (!handshakeManager) return; + + auto response = handshakeManager->handlePacket(packet); + if (response) send(response); + + if (handshakeManager->isComplete()) + { + authComplete = true; + } + else if (handshakeManager->isFailed()) + { + done = true; + message = L"Auth handshake failed"; + } +} diff --git a/Minecraft.Client/ClientConnection.h b/Minecraft.Client/ClientConnection.h index 3448496d07..69ed280531 100644 --- a/Minecraft.Client/ClientConnection.h +++ b/Minecraft.Client/ClientConnection.h @@ -6,6 +6,7 @@ class MultiPlayerLevel; class SavedDataStorage; class Socket; class MultiplayerLocalPlayer; +class HandshakeManager; class ClientConnection : public PacketListener { @@ -179,4 +180,10 @@ class ClientConnection : public PacketListener static const int MAX_ENTITY_LINK_DEFERRAL_INTERVAL = 1000; void checkDeferredEntityLinkPackets(int newEntityId); + +public: + HandshakeManager *handshakeManager; + bool authComplete; + void beginAuth(); + virtual void handleAuth(const shared_ptr &packet); }; \ No newline at end of file diff --git a/Minecraft.Client/Common/UI/UIController.cpp b/Minecraft.Client/Common/UI/UIController.cpp index b12ea5e739..bf81472cf8 100644 --- a/Minecraft.Client/Common/UI/UIController.cpp +++ b/Minecraft.Client/Common/UI/UIController.cpp @@ -2948,7 +2948,7 @@ C4JStorage::EMessageResult UIController::RequestMessageBox(UINT uiTitle, UINT ui int( *Func)(LPVOID,int,const C4JStorage::EMessageResult),LPVOID lpParam, WCHAR *pwchFormatString,DWORD dwFocusButton, bool bIsError) { - MessageBoxInfo param; + MessageBoxInfo param = {}; param.uiTitle = uiTitle; param.uiText = uiText; param.uiOptionA = uiOptionA; diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index 93f1edf119..f9dbc6ebc5 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -4,6 +4,7 @@ #include "..\..\..\Minecraft.World\Random.h" #include "..\..\User.h" #include "..\..\MinecraftServer.h" +#include "..\..\AuthScreen.h" #include "UI.h" #include "UIScene_MainMenu.h" #ifdef __ORBIS__ @@ -46,12 +47,12 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye if(ProfileManager.IsFullVersion()) { m_bTrialVersion=false; - m_buttons[static_cast(eControl_UnlockOrDLC)].init(IDS_DOWNLOADABLECONTENT,eControl_UnlockOrDLC); + m_buttons[static_cast(eControl_UnlockOrDLC)].init(L"Auth",eControl_UnlockOrDLC); } else { m_bTrialVersion=true; - m_buttons[static_cast(eControl_UnlockOrDLC)].init(IDS_UNLOCK_FULL_GAME,eControl_UnlockOrDLC); + m_buttons[static_cast(eControl_UnlockOrDLC)].init(L"Auth",eControl_UnlockOrDLC); } #ifndef _DURANGO @@ -181,8 +182,8 @@ void UIScene_MainMenu::handleGainFocus(bool navBack) if(navBack && ProfileManager.IsFullVersion()) { - // Replace the Unlock Full Game with Downloadable Content - m_buttons[static_cast(eControl_UnlockOrDLC)].setLabel(IDS_DOWNLOADABLECONTENT); + // once again replacing the shop with auth. not a bad thing. + m_buttons[static_cast(eControl_UnlockOrDLC)].setLabel(L"Auth"); } #if TO_BE_IMPLEMENTED @@ -357,7 +358,7 @@ void UIScene_MainMenu::handlePress(F64 controlId, F64 childId) ui.PlayUISFX(eSFX_Press); m_eAction=eAction_RunUnlockOrDLC; - signInReturnedFunc = &UIScene_MainMenu::UnlockFullGame_SignInReturned; + RunAction(primaryPad); break; case eControl_Exit: //CD - Added for audio @@ -438,8 +439,11 @@ void UIScene_MainMenu::RunAction(int iPad) RunHelpAndOptions(iPad); break; case eAction_RunUnlockOrDLC: - RunUnlockOrDLC(iPad); + { + AuthProfileManager::load(); + ShowAuthMenu(iPad, this); break; + } #ifdef _DURANGO case eAction_RunXboxHelp: // 4J: Launch the dummy xbox help application. @@ -450,6 +454,161 @@ void UIScene_MainMenu::RunAction(int iPad) } } +static int s_authPad = 0; + +static const wchar_t *BuildAuthProfileText() +{ + static wstring text; + const auto &profiles = AuthProfileManager::getProfiles(); + int sel = AuthProfileManager::getSelectedIndex(); + if (profiles.empty()) + { + text = L"No profiles - press Add to create one"; + } + else + { + text.clear(); + for (int i = 0; i < static_cast(profiles.size()); i++) + { + const auto &p = profiles[i]; + const wchar_t *type = (p.type == AuthProfile::MICROSOFT) ? L"[MS]" + : (p.type == AuthProfile::ELYBY) ? L"[Ely]" : L"[Off]"; + if (i > 0) text += L"\n"; + text += (i == sel) ? L"> " : L" "; + text += wstring(type) + L" " + p.username; + } + } + return text.c_str(); +} + +void UIScene_MainMenu::ShowAuthMenu(int iPad, void *pClass) +{ + s_authPad = iPad; + + static const wchar_t *authOptions[] = { L"Next", L"Use", L"Add", L"Back" }; + MessageBoxInfo param = {}; + param.uiOptionC = 4; + param.dwPad = iPad; + param.Func = &UIScene_MainMenu::AuthMenuReturned; + param.lpParam = pClass; + param.rawTitle = L"Authentication"; + param.rawText = BuildAuthProfileText(); + param.rawOptions = authOptions; + ui.NavigateToScene(iPad, eUIScene_MessageBox, ¶m, eUILayer_Alert, eUIGroup_Fullscreen); + UIScene *scene = ui.FindScene(eUIScene_MessageBox); + if (scene) + static_cast(scene)->setKeepOpen(true); +} + +void UIScene_MainMenu::ShowAuthAddMenu(int iPad, void *pClass) +{ + static const wchar_t *addOptions[] = { L"Microsoft Auth", L"Ely.by Auth", L"Add Offline User", L"Back" }; + MessageBoxInfo param = {}; + param.uiOptionC = 4; + param.dwPad = iPad; + param.Func = &UIScene_MainMenu::AuthAddMenuReturned; + param.lpParam = pClass; + param.rawTitle = L"Add Profile"; + param.rawText = L"Select an auth type"; + param.rawOptions = addOptions; + ui.NavigateToScene(iPad, eUIScene_MessageBox, ¶m, eUILayer_Alert, eUIGroup_Fullscreen); +} + +int UIScene_MainMenu::AuthMenuReturned(LPVOID lpParam, int iPad, const C4JStorage::EMessageResult result) +{ + const auto &profiles = AuthProfileManager::getProfiles(); + switch (result) + { + case C4JStorage::EMessage_ResultAccept: + { + if (!profiles.empty()) + { + int next = (AuthProfileManager::getSelectedIndex() + 1) % static_cast(profiles.size()); + AuthProfileManager::setSelectedIndex(next); + } + if (auto *scene = ui.FindScene(eUIScene_MessageBox)) + static_cast(scene)->updateContent(BuildAuthProfileText()); + return 0; + } + case C4JStorage::EMessage_ResultDecline: + { + ui.NavigateBack(iPad); + AuthProfileManager::applySelectedProfile(); + break; + } + case C4JStorage::EMessage_ResultThirdOption: + { + ui.NavigateBack(iPad); + ShowAuthAddMenu(iPad, lpParam); + break; + } + default: + { + ui.NavigateBack(iPad); + break; + } + } + return 0; +} + +int UIScene_MainMenu::AuthAddMenuReturned(LPVOID lpParam, int iPad, const C4JStorage::EMessageResult result) +{ + switch (result) + { + case C4JStorage::EMessage_ResultAccept: + AuthProfileManager::addProfile(AuthProfile::MICROSOFT, L"Player"); + ShowAuthMenu(iPad, lpParam); + break; + case C4JStorage::EMessage_ResultDecline: + AuthProfileManager::addProfile(AuthProfile::ELYBY, L"Player"); + ShowAuthMenu(iPad, lpParam); + break; + case C4JStorage::EMessage_ResultThirdOption: + { +#ifdef _WINDOWS64 + UIKeyboardInitData kbData; + kbData.title = L"Enter Username"; + kbData.defaultText = L"Player"; + kbData.maxChars = 16; + kbData.callback = &UIScene_MainMenu::AuthKeyboardReturned; + kbData.lpParam = lpParam; + kbData.pcMode = g_KBMInput.IsKBMActive(); + ui.NavigateToScene(iPad, eUIScene_Keyboard, &kbData); +#else + InputManager.RequestKeyboard(L"Enter Username", L"Player", (DWORD)0, 16, &UIScene_MainMenu::AuthKeyboardReturned, lpParam, C_4JInput::EKeyboardMode_Default); +#endif + break; + } + default: + ShowAuthMenu(iPad, lpParam); + break; + } + return 0; +} + +int UIScene_MainMenu::AuthKeyboardReturned(LPVOID lpParam, const bool bRes) +{ + if (bRes) + { + uint16_t ui16Text[128]; + ZeroMemory(ui16Text, 128 * sizeof(uint16_t)); +#ifdef _WINDOWS64 + Win64_GetKeyboardText(ui16Text, 128); +#else + InputManager.GetText(ui16Text); +#endif + if (ui16Text[0] != 0) + { + wchar_t wName[128] = {}; + for (int k = 0; k < 127 && ui16Text[k]; k++) + wName[k] = static_cast(ui16Text[k]); + AuthProfileManager::addProfile(AuthProfile::OFFLINE, wName); + } + } + ShowAuthMenu(s_authPad, lpParam); + return 0; +} + void UIScene_MainMenu::customDraw(IggyCustomDrawCallbackRegion *region) { if(wcscmp((wchar_t *)region->name,L"Splash")==0) @@ -1946,7 +2105,7 @@ void UIScene_MainMenu::tick() if(ProfileManager.IsFullVersion()) { m_bTrialVersion=false; - m_buttons[(int)eControl_UnlockOrDLC].init(app.GetString(IDS_DOWNLOADABLECONTENT),eControl_UnlockOrDLC); + m_buttons[(int)eControl_UnlockOrDLC].init(L"Auth",eControl_UnlockOrDLC); } } #endif diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.h b/Minecraft.Client/Common/UI/UIScene_MainMenu.h index 2b49a44b3d..4a059cce21 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.h @@ -144,7 +144,13 @@ class UIScene_MainMenu : public UIScene void RunHelpAndOptions(int iPad); void RunAction(int iPad); - + + static void ShowAuthMenu(int iPad, void *pClass); + static void ShowAuthAddMenu(int iPad, void *pClass); + static int AuthMenuReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); + static int AuthAddMenuReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); + static int AuthKeyboardReturned(LPVOID lpParam, const bool bRes); + static void LoadTrial(); #ifdef _XBOX_ONE diff --git a/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp b/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp index 7a0749d4a2..9767b83bba 100644 --- a/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp @@ -23,28 +23,29 @@ UIScene_MessageBox::UIScene_MessageBox(int iPad, void *initData, UILayer *parent int buttonIndex = 0; if(param->uiOptionC > 3) { - m_buttonButtons[eControl_Button0].init(app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); + m_buttonButtons[eControl_Button0].init(param->rawOptions ? param->rawOptions[buttonIndex] : app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); ++buttonIndex; } if(param->uiOptionC > 2) { - m_buttonButtons[eControl_Button1].init(app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); + m_buttonButtons[eControl_Button1].init(param->rawOptions ? param->rawOptions[buttonIndex] : app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); ++buttonIndex; } if(param->uiOptionC > 1) { - m_buttonButtons[eControl_Button2].init(app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); + m_buttonButtons[eControl_Button2].init(param->rawOptions ? param->rawOptions[buttonIndex] : app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); ++buttonIndex; } - m_buttonButtons[eControl_Button3].init(app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); + m_buttonButtons[eControl_Button3].init(param->rawOptions ? param->rawOptions[buttonIndex] : app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); - m_labelTitle.init(app.GetString(param->uiTitle)); - m_labelContent.init(app.GetString(param->uiText)); + m_labelTitle.init(param->rawTitle ? param->rawTitle : app.GetString(param->uiTitle)); + m_labelContent.init(param->rawText ? param->rawText : app.GetString(param->uiText)); out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcAutoResize , 0 , nullptr ); m_Func = param->Func; m_lpParam = param->lpParam; + m_keepOpen = false; parentLayer->addComponent(iPad,eUIComponent_MenuBackground); @@ -99,7 +100,7 @@ void UIScene_MessageBox::handleInput(int iPad, int key, bool repeat, bool presse case ACTION_MENU_CANCEL: if(pressed) { - navigateBack(); + if(!m_keepOpen) navigateBack(); if(m_Func) m_Func(m_lpParam, iPad, C4JStorage::EMessage_Cancelled); } break; @@ -136,10 +137,15 @@ void UIScene_MessageBox::handlePress(F64 controlId, F64 childId) break; } - navigateBack(); + if(!m_keepOpen) navigateBack(); if(m_Func) m_Func(m_lpParam, m_iPad, result); } +void UIScene_MessageBox::updateContent(const wchar_t *text) +{ + m_labelContent.init(text); +} + bool UIScene_MessageBox::hasFocus(int iPad) { // 4J-JEV: Fix for PS4 #5204 - [TRC][R4033] The application can be locked up by second user logging out of the system. diff --git a/Minecraft.Client/Common/UI/UIScene_MessageBox.h b/Minecraft.Client/Common/UI/UIScene_MessageBox.h index c10f6ab83d..213a825da9 100644 --- a/Minecraft.Client/Common/UI/UIScene_MessageBox.h +++ b/Minecraft.Client/Common/UI/UIScene_MessageBox.h @@ -18,6 +18,7 @@ class UIScene_MessageBox : public UIScene int( *m_Func)(LPVOID,int,const C4JStorage::EMessageResult); LPVOID m_lpParam; int m_buttonCount; + bool m_keepOpen; UIControl_Button m_buttonButtons[eControl_COUNT]; UIControl_Label m_labelTitle, m_labelContent; @@ -57,4 +58,8 @@ class UIScene_MessageBox : public UIScene protected: void handlePress(F64 controlId, F64 childId); + +public: + void updateContent(const wchar_t *text); + void setKeepOpen(bool keep) { m_keepOpen = keep; } }; \ No newline at end of file diff --git a/Minecraft.Client/Common/UI/UIStructs.h b/Minecraft.Client/Common/UI/UIStructs.h index 99c3d7bda4..28fcbcd186 100644 --- a/Minecraft.Client/Common/UI/UIStructs.h +++ b/Minecraft.Client/Common/UI/UIStructs.h @@ -485,6 +485,9 @@ typedef struct _MessageBoxInfo //C4JStringTable *pStringTable; // 4J Stu - We don't need this for our internal message boxes WCHAR *pwchFormatString; DWORD dwFocusButton; + const wchar_t *rawTitle; + const wchar_t *rawText; + const wchar_t **rawOptions; } MessageBoxInfo; typedef struct _DLCOffersParam diff --git a/Minecraft.Client/ConnectScreen.cpp b/Minecraft.Client/ConnectScreen.cpp index 18d50a5275..0a19291d46 100644 --- a/Minecraft.Client/ConnectScreen.cpp +++ b/Minecraft.Client/ConnectScreen.cpp @@ -11,13 +11,14 @@ ConnectScreen::ConnectScreen(Minecraft *minecraft, const wstring& ip, int port) { aborted = false; + preLoginSent = false; // System.out.println("Connecting to " + ip + ", " + port); minecraft->setLevel(nullptr); #if 1 // 4J - removed from separate thread, but need to investigate what we actually need here connection = new ClientConnection(minecraft, ip, port); if (aborted) return; - connection->send(std::make_shared(minecraft->user->name)); + connection->beginAuth(); #else new Thread() { @@ -47,6 +48,11 @@ void ConnectScreen::tick() { if (connection != nullptr) { + if (!preLoginSent && connection->authComplete) + { + preLoginSent = true; + connection->send(std::make_shared(minecraft->user->name)); + } connection->tick(); } } diff --git a/Minecraft.Client/ConnectScreen.h b/Minecraft.Client/ConnectScreen.h index 07e65cbbfd..76424a070f 100644 --- a/Minecraft.Client/ConnectScreen.h +++ b/Minecraft.Client/ConnectScreen.h @@ -10,6 +10,7 @@ class ConnectScreen : public Screen private: ClientConnection *connection; bool aborted; + bool preLoginSent; public: ConnectScreen(Minecraft *minecraft, const wstring& ip, int port); virtual void tick(); diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index f24086c1b6..c10edb90e2 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -14,6 +14,8 @@ #include "..\Minecraft.World\net.minecraft.world.item.h" #include "..\Minecraft.World\SharedConstants.h" #include "Settings.h" +#include "..\Minecraft.World\HandshakeManager.h" +#include "..\Minecraft.World\AuthModule.h" #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "..\Minecraft.Server\ServerLogManager.h" #include "..\Minecraft.Server\Access\Access.h" @@ -59,10 +61,14 @@ PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, co this->server = server; connection = new Connection(socket, id, this); connection->fakeLag = FAKE_LAG; + + handshakeManager = nullptr; + authComplete = false; } PendingConnection::~PendingConnection() { + delete handshakeManager; delete connection; } @@ -98,6 +104,13 @@ void PendingConnection::disconnect(DisconnectPacket::eDisconnectReason reason) void PendingConnection::handlePreLogin(shared_ptr packet) { + if (handshakeManager && !authComplete) + { + app.DebugPrintf("PendingConnection: PreLogin received before auth complete, disconnecting\n"); + disconnect(DisconnectPacket::eDisconnect_Closed); + return; + } + if (packet->m_netcodeVersion != MINECRAFT_NET_VERSION) { app.DebugPrintf("Netcode version is %d not equal to %d\n", packet->m_netcodeVersion, MINECRAFT_NET_VERSION); @@ -394,3 +407,33 @@ bool PendingConnection::isDisconnected() { return done; } + +void PendingConnection::initAuth() +{ + handshakeManager = new HandshakeManager(true); + handshakeManager->registerModule(new MojangAuthModule()); + handshakeManager->registerModule(new ElyByAuthModule()); + handshakeManager->registerModule(new KeypairOfflineAuthModule()); + handshakeManager->registerModule(new OfflineAuthModule()); +} + +void PendingConnection::handleAuth(const shared_ptr &packet) +{ + if (done || authComplete) return; + + if (!handshakeManager) + initAuth(); + + auto response = handshakeManager->handlePacket(packet); + if (response) send(response); + + if (handshakeManager->isComplete()) + { + authComplete = true; + name = std::move(handshakeManager->finalUsername); + } + else if (handshakeManager->isFailed()) + { + disconnect(DisconnectPacket::eDisconnect_Closed); + } +} diff --git a/Minecraft.Client/PendingConnection.h b/Minecraft.Client/PendingConnection.h index e8a493b097..53724896c7 100644 --- a/Minecraft.Client/PendingConnection.h +++ b/Minecraft.Client/PendingConnection.h @@ -5,6 +5,7 @@ class Socket; class LoginPacket; class Connection; class Random; +class HandshakeManager; using namespace std; class PendingConnection : public PacketListener @@ -43,6 +44,10 @@ class PendingConnection : public PacketListener wstring getName(); virtual bool isServerPacketListener(); virtual bool isDisconnected(); + HandshakeManager *handshakeManager; + bool authComplete; + void initAuth(); + virtual void handleAuth(const shared_ptr &packet); private: void sendPreLoginResponse(); diff --git a/Minecraft.Client/cmake/sources/Common.cmake b/Minecraft.Client/cmake/sources/Common.cmake index 3936a9c34b..de727931f5 100644 --- a/Minecraft.Client/cmake/sources/Common.cmake +++ b/Minecraft.Client/cmake/sources/Common.cmake @@ -969,6 +969,8 @@ set(_MINECRAFT_CLIENT_COMMON_NET_MINECRAFT_CLIENT_SKINS source_group("net/minecraft/client/skins" FILES ${_MINECRAFT_CLIENT_COMMON_NET_MINECRAFT_CLIENT_SKINS}) set(_MINECRAFT_CLIENT_COMMON_NET_MINECRAFT_CLIENT_TITLE + "${CMAKE_CURRENT_SOURCE_DIR}/AuthScreen.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthScreen.h" "${CMAKE_CURRENT_SOURCE_DIR}/TitleScreen.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/TitleScreen.h" ) diff --git a/Minecraft.Server/cmake/sources/Common.cmake b/Minecraft.Server/cmake/sources/Common.cmake index 06aa0bfe19..0d6f382b20 100644 --- a/Minecraft.Server/cmake/sources/Common.cmake +++ b/Minecraft.Server/cmake/sources/Common.cmake @@ -5,6 +5,7 @@ set(_MINECRAFT_SERVER_COMMON_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/AllowAllCuller.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ArchiveFile.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ArrowRenderer.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/AuthScreen.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BatModel.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BatRenderer.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BeaconRenderer.cpp" diff --git a/Minecraft.World/AuthModule.cpp b/Minecraft.World/AuthModule.cpp index 0d5ffb0f4d..35bd3e853e 100644 --- a/Minecraft.World/AuthModule.cpp +++ b/Minecraft.World/AuthModule.cpp @@ -50,8 +50,6 @@ vector MojangAuthModule::supportedVariations() return {L"java", L"bedrock"}; } -// --- KeypairOfflineAuthModule --- - const wchar_t *KeypairOfflineAuthModule::schemeName() { return L"mcconsoles:keypair_offline"; } vector KeypairOfflineAuthModule::supportedVariations() diff --git a/Minecraft.World/AuthPackets.cpp b/Minecraft.World/AuthPackets.cpp index 4bd2bb9534..4490b264f7 100644 --- a/Minecraft.World/AuthPackets.cpp +++ b/Minecraft.World/AuthPackets.cpp @@ -26,10 +26,10 @@ void AuthPacket::write(DataOutputStream *dos) { dos->writeByte(static_cast(stage)); dos->writeShort(static_cast(fields.size())); - for (const auto &field : fields) + for (const auto &[key, value] : fields) { - writeUtf(field.first, dos); - writeUtf(field.second, dos); + writeUtf(key, dos); + writeUtf(value, dos); } } @@ -41,7 +41,7 @@ void AuthPacket::handle(PacketListener *listener) int AuthPacket::getEstimatedSize() { int size = 1 + 2; - for (const auto &field : fields) - size += 4 + static_cast((field.first.length() + field.second.length()) * sizeof(wchar_t)); + for (const auto &[key, value] : fields) + size += 4 + static_cast((key.length() + value.length()) * sizeof(wchar_t)); return size; } diff --git a/Minecraft.World/HandshakeManager.cpp b/Minecraft.World/HandshakeManager.cpp new file mode 100644 index 0000000000..2da686dbf4 --- /dev/null +++ b/Minecraft.World/HandshakeManager.cpp @@ -0,0 +1,205 @@ +#include "stdafx.h" +#include "HandshakeManager.h" +#include "AuthModule.h" + +static constexpr auto PROTOCOL_VERSION = L"1.0"; + +HandshakeManager::HandshakeManager(bool isServer) + : isServer(isServer), state(HandshakeState::IDLE), activeModule(nullptr) +{ +} + +HandshakeManager::~HandshakeManager() +{ + for (auto &[name, module] : modules) + delete module; +} + +void HandshakeManager::registerModule(AuthModule *module) +{ + modules[module->schemeName()] = module; +} + +shared_ptr HandshakeManager::handlePacket(const shared_ptr &packet) +{ + return isServer ? handleServer(packet) : handleClient(packet); +} + +shared_ptr HandshakeManager::createInitialPacket() +{ + state = HandshakeState::VERSION_SENT; + return makePacket(AuthStage::ANNOUNCE_VERSION, {{L"version", PROTOCOL_VERSION}}); +} + +shared_ptr HandshakeManager::handleServer(const shared_ptr &packet) +{ + switch (packet->stage) + { + case AuthStage::ANNOUNCE_VERSION: + { + protocolVersion = L""; + for (const auto &[k, v] : packet->fields) + if (k == L"version") protocolVersion = v; + + if (protocolVersion != PROTOCOL_VERSION) + return fail(); + + // Pick first registered module as the scheme + if (modules.empty()) + return fail(); + + activeModule = modules.begin()->second; + state = HandshakeState::SCHEME_DECLARED; + return makePacket(AuthStage::DECLARE_SCHEME, { + {L"version", PROTOCOL_VERSION}, + {L"scheme", activeModule->schemeName()} + }); + } + + case AuthStage::ACCEPT_SCHEME: + { + wstring variation; + for (const auto &[k, v] : packet->fields) + if (k == L"variation") variation = v; + + activeVariation = variation; + state = HandshakeState::SETTINGS_SENT; + auto settings = activeModule->getSettings(activeVariation); + return makePacket(AuthStage::SCHEME_SETTINGS, std::move(settings)); + } + + case AuthStage::BEGIN_AUTH: + { + state = HandshakeState::AUTH_IN_PROGRESS; + return nullptr; + } + + case AuthStage::AUTH_DATA: + { + wstring uid, username; + if (!activeModule->onAuthData(packet->fields, uid, username)) + return fail(); + + state = HandshakeState::AUTH_DATA_EXCHANGED; + return nullptr; + } + + case AuthStage::AUTH_DONE: + { + wstring uid, username; + for (const auto &[k, v] : packet->fields) + { + if (k == L"uid") uid = v; + else if (k == L"username") username = v; + } + + if (!activeModule->validate(uid, username)) + return fail(); + + finalUid = uid; + finalUsername = username; + state = HandshakeState::IDENTITY_ASSIGNED; + return makePacket(AuthStage::ASSIGN_IDENTITY, { + {L"uid", finalUid}, + {L"username", finalUsername} + }); + } + + case AuthStage::CONFIRM_IDENTITY: + { + wstring uid, username; + for (const auto &[k, v] : packet->fields) + { + if (k == L"uid") uid = v; + else if (k == L"username") username = v; + } + + if (uid != finalUid || username != finalUsername) + return fail(); + + state = HandshakeState::COMPLETE; + return makePacket(AuthStage::AUTH_SUCCESS); + } + + default: + return fail(); + } +} + +shared_ptr HandshakeManager::handleClient(const shared_ptr &packet) +{ + switch (packet->stage) + { + case AuthStage::DECLARE_SCHEME: + { + wstring scheme; + for (const auto &[k, v] : packet->fields) + { + if (k == L"version") protocolVersion = v; + else if (k == L"scheme") scheme = v; + } + + if (protocolVersion != PROTOCOL_VERSION) + return fail(); + + auto it = modules.find(scheme); + if (it == modules.end()) + return fail(); + + activeModule = it->second; + + auto variations = activeModule->supportedVariations(); + activeVariation = variations.empty() ? L"" : variations[0]; + + state = HandshakeState::SCHEME_ACCEPTED; + return makePacket(AuthStage::ACCEPT_SCHEME, {{L"variation", activeVariation}}); + } + + case AuthStage::SCHEME_SETTINGS: + { + state = HandshakeState::AUTH_IN_PROGRESS; + return makePacket(AuthStage::BEGIN_AUTH); + } + + case AuthStage::ASSIGN_IDENTITY: + { + for (const auto &[k, v] : packet->fields) + { + if (k == L"uid") finalUid = v; + else if (k == L"username") finalUsername = v; + } + + state = HandshakeState::IDENTITY_CONFIRMED; + return makePacket(AuthStage::CONFIRM_IDENTITY, { + {L"uid", finalUid}, + {L"username", finalUsername} + }); + } + + case AuthStage::AUTH_SUCCESS: + { + state = HandshakeState::COMPLETE; + return nullptr; + } + + case AuthStage::AUTH_FAILURE: + { + state = HandshakeState::FAILED; + return nullptr; + } + + default: + return fail(); + } +} + +shared_ptr HandshakeManager::makePacket(AuthStage stage, vector> fields) +{ + return std::make_shared(stage, std::move(fields)); +} + +shared_ptr HandshakeManager::fail() +{ + state = HandshakeState::FAILED; + return makePacket(AuthStage::AUTH_FAILURE); +} diff --git a/Minecraft.World/HandshakeManager.h b/Minecraft.World/HandshakeManager.h new file mode 100644 index 0000000000..9e9da1c831 --- /dev/null +++ b/Minecraft.World/HandshakeManager.h @@ -0,0 +1,57 @@ +#pragma once +using namespace std; + +#include +#include +#include +#include "AuthPackets.h" + +class AuthModule; + +enum class HandshakeState : uint8_t +{ + IDLE, + VERSION_SENT, + SCHEME_DECLARED, + SCHEME_ACCEPTED, + SETTINGS_SENT, + AUTH_IN_PROGRESS, + AUTH_DATA_EXCHANGED, + IDENTITY_ASSIGNED, + IDENTITY_CONFIRMED, + COMPLETE, + FAILED +}; + +class HandshakeManager +{ +private: + bool isServer; + HandshakeState state; + unordered_map modules; + AuthModule *activeModule; + wstring activeVariation; + wstring protocolVersion; + +public: + wstring finalUid; + wstring finalUsername; + + HandshakeManager(bool isServer); + ~HandshakeManager(); + + void registerModule(AuthModule *module); + shared_ptr handlePacket(const shared_ptr &packet); + shared_ptr createInitialPacket(); + + bool isComplete() const { return state == HandshakeState::COMPLETE; } + bool isFailed() const { return state == HandshakeState::FAILED; } + HandshakeState getState() const { return state; } + +private: + shared_ptr handleServer(const shared_ptr &packet); + shared_ptr handleClient(const shared_ptr &packet); + + shared_ptr makePacket(AuthStage stage, vector> fields = {}); + shared_ptr fail(); +}; diff --git a/Minecraft.World/PacketListener.cpp b/Minecraft.World/PacketListener.cpp index 490922c79c..bd340add3e 100644 --- a/Minecraft.World/PacketListener.cpp +++ b/Minecraft.World/PacketListener.cpp @@ -492,7 +492,7 @@ void PacketListener::handleGameCommand(shared_ptr packet) onUnhandledPacket( (shared_ptr ) packet); } -void PacketListener::handleAuth(shared_ptr packet) +void PacketListener::handleAuth(const shared_ptr &packet) { onUnhandledPacket( (shared_ptr ) packet); } diff --git a/Minecraft.World/PacketListener.h b/Minecraft.World/PacketListener.h index 9f5b1b9de3..bf2ebdadbd 100644 --- a/Minecraft.World/PacketListener.h +++ b/Minecraft.World/PacketListener.h @@ -229,6 +229,5 @@ class PacketListener virtual void handleXZ(shared_ptr packet); virtual void handleGameCommand(shared_ptr packet); - // MCConsoles Auth - virtual void handleAuth(shared_ptr packet); + virtual void handleAuth(const shared_ptr &packet); }; diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index eb4a63793b..6f45dfdf7d 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -318,6 +318,8 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/GameEventPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/GetInfoPacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/GetInfoPacket.h" + "${CMAKE_CURRENT_SOURCE_DIR}/HandshakeManager.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/HandshakeManager.h" "${CMAKE_CURRENT_SOURCE_DIR}/InteractPacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/InteractPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/KeepAlivePacket.cpp" From e8ec7382afe0a1ca80dd043e8e5b8bdb11b978ca Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sun, 29 Mar 2026 16:39:12 -0400 Subject: [PATCH 04/18] fixed a potential memory leak. oopsie! --- Minecraft.Client/AuthScreen.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Minecraft.Client/AuthScreen.cpp b/Minecraft.Client/AuthScreen.cpp index 706d454b98..463e07f9b1 100644 --- a/Minecraft.Client/AuthScreen.cpp +++ b/Minecraft.Client/AuthScreen.cpp @@ -28,8 +28,10 @@ void AuthProfileManager::load() auto readWstr = [&file]() -> wstring { uint16_t len = 0; file.read(reinterpret_cast(&len), sizeof(len)); + if (!file || len > 4096) return {}; wstring s(len, L'\0'); file.read(reinterpret_cast(s.data()), len * sizeof(wchar_t)); + if (!file) return {}; return s; }; From e979be42b252aa54b2a69712522a6e900a9b4ff4 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Fri, 3 Apr 2026 02:41:10 -0400 Subject: [PATCH 05/18] refactored the nlomann json library to be in the include/common. seems quite odd this wasn't the case. --- Minecraft.Server/Access/BanManager.cpp | 2 +- Minecraft.Server/Access/WhitelistManager.cpp | 2 +- Minecraft.Server/Common/AccessStorageUtils.h | 2 +- .../Common}/vendor/nlohmann/LICENSE.MIT | 0 {Minecraft.Server => include/Common}/vendor/nlohmann/json.hpp | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename {Minecraft.Server => include/Common}/vendor/nlohmann/LICENSE.MIT (100%) rename {Minecraft.Server => include/Common}/vendor/nlohmann/json.hpp (100%) diff --git a/Minecraft.Server/Access/BanManager.cpp b/Minecraft.Server/Access/BanManager.cpp index 59f5bccc03..5447ab875c 100644 --- a/Minecraft.Server/Access/BanManager.cpp +++ b/Minecraft.Server/Access/BanManager.cpp @@ -7,7 +7,7 @@ #include "..\Common\NetworkUtils.h" #include "..\Common\StringUtils.h" #include "..\ServerLogger.h" -#include "..\vendor\nlohmann\json.hpp" +#include "Common/vendor/nlohmann/json.hpp" #include #include diff --git a/Minecraft.Server/Access/WhitelistManager.cpp b/Minecraft.Server/Access/WhitelistManager.cpp index 33ea7e46a7..3629914ed5 100644 --- a/Minecraft.Server/Access/WhitelistManager.cpp +++ b/Minecraft.Server/Access/WhitelistManager.cpp @@ -6,7 +6,7 @@ #include "..\Common\FileUtils.h" #include "..\Common\StringUtils.h" #include "..\ServerLogger.h" -#include "..\vendor\nlohmann\json.hpp" +#include "Common/vendor/nlohmann/json.hpp" #include diff --git a/Minecraft.Server/Common/AccessStorageUtils.h b/Minecraft.Server/Common/AccessStorageUtils.h index c5d3477c70..26d0c80e52 100644 --- a/Minecraft.Server/Common/AccessStorageUtils.h +++ b/Minecraft.Server/Common/AccessStorageUtils.h @@ -3,7 +3,7 @@ #include "FileUtils.h" #include "StringUtils.h" -#include "..\vendor\nlohmann\json.hpp" +#include "Common/vendor/nlohmann/json.hpp" #include diff --git a/Minecraft.Server/vendor/nlohmann/LICENSE.MIT b/include/Common/vendor/nlohmann/LICENSE.MIT similarity index 100% rename from Minecraft.Server/vendor/nlohmann/LICENSE.MIT rename to include/Common/vendor/nlohmann/LICENSE.MIT diff --git a/Minecraft.Server/vendor/nlohmann/json.hpp b/include/Common/vendor/nlohmann/json.hpp similarity index 100% rename from Minecraft.Server/vendor/nlohmann/json.hpp rename to include/Common/vendor/nlohmann/json.hpp From 0fa5ae303744d539d1059a11ad38bc7625b58c35 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Fri, 3 Apr 2026 04:28:52 -0400 Subject: [PATCH 06/18] finally added session-based authentication and libcurl for https requests. (serverside for right now.) --- CMakeLists.txt | 16 ++++- Minecraft.Client/ClientConnection.cpp | 3 +- Minecraft.Client/PendingConnection.cpp | 3 +- Minecraft.World/AuthModule.cpp | 83 +++++++++++++++++----- Minecraft.World/AuthModule.h | 26 +++---- Minecraft.World/CMakeLists.txt | 2 + Minecraft.World/HandshakeManager.cpp | 7 +- Minecraft.World/HttpClient.cpp | 57 +++++++++++++++ Minecraft.World/HttpClient.h | 16 +++++ Minecraft.World/cmake/sources/Common.cmake | 2 + 10 files changed, 175 insertions(+), 40 deletions(-) create mode 100644 Minecraft.World/HttpClient.cpp create mode 100644 Minecraft.World/HttpClient.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 63071a0ed7..979dd2a558 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,21 @@ project(MinecraftConsoles LANGUAGES C CXX RC ASM_MASM) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +include(FetchContent) +set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(CURL_USE_SCHANNEL ON CACHE BOOL "" FORCE) +set(BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(CURL_DISABLE_LDAP ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_LDAPS ON CACHE BOOL "" FORCE) +FetchContent_Declare( + curl + URL https://github.com/curl/curl/releases/download/curl-8_11_1/curl-8.11.1.tar.xz + URL_HASH SHA256=c7ca7db48b0909743eaef34250da02c19bc61d4f1dcedd6603f109409536ab56 + FIND_PACKAGE_ARGS +) +FetchContent_MakeAvailable(curl) if(NOT WIN32) message(FATAL_ERROR "This CMake build currently supports Windows only.") @@ -18,7 +33,6 @@ set(CMAKE_CONFIGURATION_TYPES "Release" CACHE STRING "" FORCE ) -set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") function(configure_compiler_target target) # MSVC and compatible compilers (like Clang-cl) diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index bdace06483..92402784aa 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -4139,8 +4139,7 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr< void ClientConnection::beginAuth() { handshakeManager = new HandshakeManager(false); - handshakeManager->registerModule(new MojangAuthModule()); - handshakeManager->registerModule(new ElyByAuthModule()); + handshakeManager->registerModule(new SessionAuthModule()); handshakeManager->registerModule(new KeypairOfflineAuthModule()); handshakeManager->registerModule(new OfflineAuthModule()); diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index c10edb90e2..88fdb96960 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -411,8 +411,7 @@ bool PendingConnection::isDisconnected() void PendingConnection::initAuth() { handshakeManager = new HandshakeManager(true); - handshakeManager->registerModule(new MojangAuthModule()); - handshakeManager->registerModule(new ElyByAuthModule()); + handshakeManager->registerModule(new SessionAuthModule()); handshakeManager->registerModule(new KeypairOfflineAuthModule()); handshakeManager->registerModule(new OfflineAuthModule()); } diff --git a/Minecraft.World/AuthModule.cpp b/Minecraft.World/AuthModule.cpp index 35bd3e853e..e283e2b38b 100644 --- a/Minecraft.World/AuthModule.cpp +++ b/Minecraft.World/AuthModule.cpp @@ -1,5 +1,23 @@ #include "stdafx.h" #include "AuthModule.h" +#include "HttpClient.h" +#include "StringHelpers.h" +#include "Common/vendor/nlohmann/json.hpp" +#include + +static string narrowStr(const wstring &w) +{ + return string(w.begin(), w.end()); +} + +static wstring generateServerId() +{ + static constexpr wchar_t hex[] = L"0123456789abcdef"; + static std::mt19937 rng(std::random_device{}()); + wstring id(16, L'0'); + for (auto &c : id) c = hex[rng() & 0xF]; + return id; +} bool AuthModule::validate(const wstring &uid, const wstring &username) { @@ -16,38 +34,67 @@ bool AuthModule::extractIdentity(const vector> &fields, w return validate(outUid, outUsername); } -ElyByAuthModule::ElyByAuthModule(const wstring &endpoint) - : endpoint(endpoint) +SessionAuthModule::SessionAuthModule() { + endpoints[L"mojang"] = {L"https://authserver.mojang.com", L"https://sessionserver.mojang.com"}; + endpoints[L"elyby"] = {L"https://authserver.ely.by", L"https://authserver.ely.by"}; } -const wchar_t *ElyByAuthModule::schemeName() { return L"mcconsoles:elyby"; } +const wchar_t *SessionAuthModule::schemeName() { return L"mcconsoles:session"; } -vector ElyByAuthModule::supportedVariations() +vector SessionAuthModule::supportedVariations() { - return {L"java"}; + return {L"mojang", L"elyby"}; } -vector> ElyByAuthModule::getSettings(const wstring &variation) +vector> SessionAuthModule::getSettings(const wstring &variation) { - return {{L"endpoint", endpoint}}; -} + auto it = endpoints.find(variation); + if (it == endpoints.end()) return {}; -bool ElyByAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) -{ - return extractIdentity(fields, outUid, outUsername); + activeSessionEndpoint = it->second.sessionEndpoint; + activeServerId = generateServerId(); + + return { + {L"authEndpoint", it->second.authEndpoint}, + {L"sessionEndpoint", it->second.sessionEndpoint}, + {L"serverId", activeServerId} + }; } -MojangAuthModule::MojangAuthModule() - : ElyByAuthModule(L"https://sessionserver.mojang.com") +bool SessionAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) { -} + wstring username; + for (const auto &[k, v] : fields) + { + if (k == L"username") username = v; + } -const wchar_t *MojangAuthModule::schemeName() { return L"mcconsoles:mojang"; } + if (username.empty() || activeServerId.empty() || activeSessionEndpoint.empty()) + return false; -vector MojangAuthModule::supportedVariations() -{ - return {L"java", L"bedrock"}; + string url = narrowStr(activeSessionEndpoint) + + "/session/minecraft/hasJoined?username=" + narrowStr(username) + + "&serverId=" + narrowStr(activeServerId); + + auto response = HttpClient::get(url); + if (response.statusCode != 200) + return false; + + auto json = nlohmann::json::parse(response.body, nullptr, false); + if (json.is_discarded()) + return false; + + string id = json.value("id", ""); + string name = json.value("name", ""); + + if (id.empty() || name.empty()) + return false; + + outUid = convStringToWstring(id); + outUsername = convStringToWstring(name); + + return validate(outUid, outUsername); } const wchar_t *KeypairOfflineAuthModule::schemeName() { return L"mcconsoles:keypair_offline"; } diff --git a/Minecraft.World/AuthModule.h b/Minecraft.World/AuthModule.h index 4de6efa16c..851b1260bf 100644 --- a/Minecraft.World/AuthModule.h +++ b/Minecraft.World/AuthModule.h @@ -4,6 +4,7 @@ using namespace std; #include #include #include +#include class AuthModule { @@ -21,27 +22,26 @@ class AuthModule bool extractIdentity(const vector> &fields, wstring &outUid, wstring &outUsername); }; -class ElyByAuthModule : public AuthModule +class SessionAuthModule : public AuthModule { -protected: - wstring endpoint; - public: - ElyByAuthModule(const wstring &endpoint = L"https://authserver.ely.by"); + struct EndpointPair { + wstring authEndpoint; + wstring sessionEndpoint; + }; - const wchar_t *schemeName() override; - vector supportedVariations() override; - vector> getSettings(const wstring &variation) override; - bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; -}; +private: + unordered_map endpoints; + wstring activeSessionEndpoint; + wstring activeServerId; -class MojangAuthModule : public ElyByAuthModule -{ public: - MojangAuthModule(); + SessionAuthModule(); const wchar_t *schemeName() override; vector supportedVariations() override; + vector> getSettings(const wstring &variation) override; + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; }; class KeypairOfflineAuthModule : public AuthModule diff --git a/Minecraft.World/CMakeLists.txt b/Minecraft.World/CMakeLists.txt index e397bf2925..d649031419 100644 --- a/Minecraft.World/CMakeLists.txt +++ b/Minecraft.World/CMakeLists.txt @@ -27,4 +27,6 @@ target_compile_definitions(Minecraft.World PRIVATE ) target_precompile_headers(Minecraft.World PRIVATE "$<$:stdafx.h>") +target_link_libraries(Minecraft.World PUBLIC CURL::libcurl) + configure_compiler_target(Minecraft.World) diff --git a/Minecraft.World/HandshakeManager.cpp b/Minecraft.World/HandshakeManager.cpp index 2da686dbf4..8b6b87d225 100644 --- a/Minecraft.World/HandshakeManager.cpp +++ b/Minecraft.World/HandshakeManager.cpp @@ -79,7 +79,8 @@ shared_ptr HandshakeManager::handleServer(const shared_ptronAuthData(packet->fields, uid, username)) return fail(); - + finalUid = uid; + finalUsername = username; state = HandshakeState::AUTH_DATA_EXCHANGED; return nullptr; } @@ -93,11 +94,9 @@ shared_ptr HandshakeManager::handleServer(const shared_ptrvalidate(uid, username)) + if (uid != finalUid || username != finalUsername) return fail(); - finalUid = uid; - finalUsername = username; state = HandshakeState::IDENTITY_ASSIGNED; return makePacket(AuthStage::ASSIGN_IDENTITY, { {L"uid", finalUid}, diff --git a/Minecraft.World/HttpClient.cpp b/Minecraft.World/HttpClient.cpp new file mode 100644 index 0000000000..b59d1b1f97 --- /dev/null +++ b/Minecraft.World/HttpClient.cpp @@ -0,0 +1,57 @@ +#include "stdafx.h" +#include "HttpClient.h" +#include + +static size_t writeCallback(char *data, size_t size, size_t nmemb, void *userdata) +{ + auto *buf = static_cast(userdata); + buf->append(data, size * nmemb); + return size * nmemb; +} + +static HttpResponse performRequest(CURL *curl) +{ + std::string responseBody; + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + CURLcode res = curl_easy_perform(curl); + + int statusCode = 0; + if (res == CURLE_OK) + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode); + + curl_easy_cleanup(curl); + return {statusCode, std::move(responseBody)}; +} + +HttpResponse HttpClient::get(const std::string &url) +{ + CURL *curl = curl_easy_init(); + if (!curl) + return {0, ""}; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + return performRequest(curl); +} + +HttpResponse HttpClient::post(const std::string &url, const std::string &body, const std::string &contentType) +{ + CURL *curl = curl_easy_init(); + if (!curl) + return {0, ""}; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); + + struct curl_slist *headers = nullptr; + headers = curl_slist_append(headers, ("Content-Type: " + contentType).c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + HttpResponse resp = performRequest(curl); + curl_slist_free_all(headers); + return resp; +} diff --git a/Minecraft.World/HttpClient.h b/Minecraft.World/HttpClient.h new file mode 100644 index 0000000000..317941c01d --- /dev/null +++ b/Minecraft.World/HttpClient.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +struct HttpResponse +{ + int statusCode; + std::string body; +}; + +class HttpClient +{ +public: + static HttpResponse get(const std::string &url); + static HttpResponse post(const std::string &url, const std::string &body, const std::string &contentType = "application/json"); +}; diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index 6f45dfdf7d..07b322d9fa 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -320,6 +320,8 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/GetInfoPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/HandshakeManager.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/HandshakeManager.h" + "${CMAKE_CURRENT_SOURCE_DIR}/HttpClient.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/HttpClient.h" "${CMAKE_CURRENT_SOURCE_DIR}/InteractPacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/InteractPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/KeepAlivePacket.cpp" From 1daae8e2a8b8ebb4543397b3167b1e238a2e93a4 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Fri, 3 Apr 2026 21:39:59 -0400 Subject: [PATCH 07/18] major feat: auth v1 --- Minecraft.Client/AuthScreen.cpp | 280 +++++++++++++++++- Minecraft.Client/AuthScreen.h | 46 ++- Minecraft.Client/ClientConnection.cpp | 14 + .../Common/UI/UIScene_Keyboard.cpp | 9 +- Minecraft.Client/Common/UI/UIScene_Keyboard.h | 1 + .../Common/UI/UIScene_MainMenu.cpp | 207 ++++++++++--- Minecraft.Client/Common/UI/UIScene_MainMenu.h | 3 + .../Common/UI/UIScene_MessageBox.cpp | 2 + Minecraft.Client/Common/UI/UIStructs.h | 3 +- Minecraft.World/AuthModule.cpp | 5 - Minecraft.World/AuthModule.h | 5 + Minecraft.World/HandshakeManager.cpp | 101 ++++--- Minecraft.World/HandshakeManager.h | 9 + Minecraft.World/HttpClient.cpp | 29 +- Minecraft.World/HttpClient.h | 5 +- 15 files changed, 623 insertions(+), 96 deletions(-) diff --git a/Minecraft.Client/AuthScreen.cpp b/Minecraft.Client/AuthScreen.cpp index 463e07f9b1..c0d1d89464 100644 --- a/Minecraft.Client/AuthScreen.cpp +++ b/Minecraft.Client/AuthScreen.cpp @@ -2,9 +2,16 @@ #include "AuthScreen.h" #include "Minecraft.h" #include "User.h" +#include "..\Minecraft.World\AuthModule.h" +#include "..\Minecraft.World\HttpClient.h" +#include "..\Minecraft.World\StringHelpers.h" +#include "Common/vendor/nlohmann/json.hpp" #include +#include +using json = nlohmann::json; static constexpr auto PROFILES_FILE = L"auth_profiles.dat"; +static constexpr auto MS_CLIENT_ID = "00000000441cc96b"; vector AuthProfileManager::profiles; int AuthProfileManager::selectedProfile = -1; @@ -41,8 +48,10 @@ void AuthProfileManager::load() profiles.push_back(std::move(p)); } + int32_t savedIdx = 0; + file.read(reinterpret_cast(&savedIdx), sizeof(savedIdx)); if (!profiles.empty()) - selectedProfile = 0; + selectedProfile = (savedIdx >= 0 && savedIdx < static_cast(profiles.size())) ? savedIdx : 0; } void AuthProfileManager::save() @@ -67,11 +76,15 @@ void AuthProfileManager::save() writeWstr(p.username); writeWstr(p.token); } + + int32_t idx = static_cast(selectedProfile); + file.write(reinterpret_cast(&idx), sizeof(idx)); } -void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username) +void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid, const wstring &token) { - profiles.push_back({type, L"offline_" + username, username, {}}); + wstring finalUid = uid.empty() ? L"offline_" + username : uid; + profiles.push_back({type, finalUid, username, token}); selectedProfile = static_cast(profiles.size()) - 1; save(); } @@ -99,5 +112,266 @@ bool AuthProfileManager::applySelectedProfile() delete mc->user; mc->user = new User(p.username, p.token); + + // push auth name into the platform globals so ProfileManager.GetGamertag() picks it up + // instead of returning the default "Player" + extern char g_Win64Username[17]; + extern wchar_t g_Win64UsernameW[17]; + string narrow = narrowStr(p.username); + strncpy_s(g_Win64Username, sizeof(g_Win64Username), narrow.c_str(), _TRUNCATE); + wcsncpy_s(g_Win64UsernameW, 17, p.username.c_str(), _TRUNCATE); + return true; } + +// --- AuthFlow --- + +std::thread AuthFlow::workerThread; +std::atomic AuthFlow::state{AuthFlowState::IDLE}; +std::atomic AuthFlow::cancelRequested{false}; +AuthResult AuthFlow::result; +wstring AuthFlow::userCode; +wstring AuthFlow::verificationUri; + +void AuthFlow::reset() +{ + cancelRequested = true; + if (workerThread.joinable()) + workerThread.detach(); + state = AuthFlowState::IDLE; + result = {}; + userCode.clear(); + verificationUri.clear(); + cancelRequested = false; +} + +void AuthFlow::startMicrosoft() +{ + reset(); + state = AuthFlowState::WAITING_FOR_USER; + workerThread = std::thread(microsoftFlowThread); +} + +void AuthFlow::startElyBy(const wstring &username, const wstring &password) +{ + reset(); + state = AuthFlowState::EXCHANGING; + workerThread = std::thread(elybyFlowThread, narrowStr(username), narrowStr(password)); +} + +static void authFail(AuthResult &result, std::atomic &state, const wchar_t *msg) +{ + result = {false, {}, {}, {}, msg}; + state = AuthFlowState::FAILED; +} + +// parse json response body, return discarded json on bad status +static json parseResponse(const HttpResponse &resp, int expectedStatus = 200) +{ + if (resp.statusCode != expectedStatus) return json::value_t::discarded; + return json::parse(resp.body, nullptr, false); +} + +void AuthFlow::microsoftFlowThread() +{ + auto dcResp = HttpClient::post( + "https://login.live.com/oauth20_connect.srf", + "client_id=" + string(MS_CLIENT_ID) + "&scope=service::user.auth.xboxlive.com::MBI_SSL&response_type=device_code", + "application/x-www-form-urlencoded" + ); + + auto dcJson = parseResponse(dcResp); + if (dcJson.is_discarded()) + { + authFail(result, state, L"Failed to get device code"); + return; + } + + string deviceCode = dcJson.value("device_code", ""); + string uCode = dcJson.value("user_code", ""); + string vUri = dcJson.value("verification_uri", ""); + int interval = dcJson.value("interval", 5); + + if (deviceCode.empty() || uCode.empty()) + { + authFail(result, state, L"Missing device code fields"); + return; + } + + userCode = convStringToWstring(uCode); + verificationUri = convStringToWstring(vUri); + + // copy code to clipboard so the user can just paste it + if (OpenClipboard(nullptr)) + { + EmptyClipboard(); + size_t bytes = (uCode.size() + 1) * sizeof(char); + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes); + if (hMem) + { + memcpy(GlobalLock(hMem), uCode.c_str(), bytes); + GlobalUnlock(hMem); + SetClipboardData(CF_TEXT, hMem); + } + CloseClipboard(); + } + + if (!vUri.empty()) + ShellExecuteW(nullptr, L"open", verificationUri.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + + state = AuthFlowState::POLLING; + string msAccessToken; + + for (int attempt = 0; attempt < 180; attempt++) + { + for (int ms = 0; ms < interval * 1000; ms += 250) + { + if (cancelRequested) return; + Sleep(250); + } + + auto pollResp = HttpClient::post( + "https://login.live.com/oauth20_token.srf", + "client_id=" + string(MS_CLIENT_ID) + "&device_code=" + deviceCode + "&grant_type=urn:ietf:params:oauth:grant-type:device_code", + "application/x-www-form-urlencoded" + ); + + auto pollJson = json::parse(pollResp.body, nullptr, false); + if (pollJson.is_discarded()) continue; + + if (pollResp.statusCode == 200) + { + msAccessToken = pollJson.value("access_token", ""); + if (!msAccessToken.empty()) break; + } + + string err = pollJson.value("error", ""); + if (err == "authorization_pending") continue; + if (err == "slow_down") { interval += 5; continue; } + if (!err.empty()) + { + result = {false, {}, {}, {}, convStringToWstring("Auth error: " + err)}; + state = AuthFlowState::FAILED; + return; + } + } + + if (msAccessToken.empty()) + { + authFail(result, state, L"Timed out waiting for login"); + return; + } + + state = AuthFlowState::EXCHANGING; + if (cancelRequested) return; + + // xbox live auth + auto xblResp = HttpClient::post("https://user.auth.xboxlive.com/user/authenticate", json({ + {"Properties", {{"AuthMethod", "RPS"}, {"SiteName", "user.auth.xboxlive.com"}, {"RpsTicket", msAccessToken}}}, + {"RelyingParty", "http://auth.xboxlive.com"}, + {"TokenType", "JWT"} + }).dump()); + + auto xblJson = parseResponse(xblResp); + if (xblJson.is_discarded()) + { + authFail(result, state, L"Xbox Live auth failed"); + return; + } + + string xblToken = xblJson.value("Token", ""); + string userHash; + try { userHash = xblJson["DisplayClaims"]["xui"][0].value("uhs", ""); } catch (...) {} + + if (xblToken.empty() || userHash.empty()) + { + authFail(result, state, L"Bad Xbox Live response"); + return; + } + + // xsts auth + auto xstsResp = HttpClient::post("https://xsts.auth.xboxlive.com/xsts/authorize", json({ + {"Properties", {{"SandboxId", "RETAIL"}, {"UserTokens", {xblToken}}}}, + {"RelyingParty", "rp://api.minecraftservices.com/"}, + {"TokenType", "JWT"} + }).dump()); + + auto xstsJson = parseResponse(xstsResp); + string xstsToken = xstsJson.is_discarded() ? "" : xstsJson.value("Token", ""); + + if (xstsToken.empty()) + { + authFail(result, state, L"XSTS auth failed"); + return; + } + + // minecraft login + auto mcResp = HttpClient::post("https://api.minecraftservices.com/authentication/login_with_xbox", + json({{"identityToken", "XBL3.0 x=" + userHash + ";" + xstsToken}}).dump()); + + auto mcJson = parseResponse(mcResp); + string mcAccessToken = mcJson.is_discarded() ? "" : mcJson.value("access_token", ""); + + if (mcAccessToken.empty()) + { + authFail(result, state, L"Minecraft auth failed"); + return; + } + + // get profile + auto profResp = HttpClient::get("https://api.minecraftservices.com/minecraft/profile", + {"Authorization: Bearer " + mcAccessToken}); + + auto profJson = parseResponse(profResp); + if (profJson.is_discarded()) + { + authFail(result, state, L"Failed to get Minecraft profile"); + return; + } + + string profId = profJson.value("id", ""); + string profName = profJson.value("name", ""); + + if (profId.empty() || profName.empty()) + { + authFail(result, state, L"Profile missing id or name"); + return; + } + + result = {true, convStringToWstring(profName), convStringToWstring(profId), convStringToWstring(mcAccessToken), {}}; + state = AuthFlowState::COMPLETE; +} + +void AuthFlow::elybyFlowThread(const string &username, const string &password) +{ + auto resp = HttpClient::post("https://authserver.ely.by/auth/authenticate", json({ + {"username", username}, + {"password", password}, + {"clientToken", "mcconsoles"}, + {"agent", {{"name", "Minecraft"}, {"version", 1}}} + }).dump()); + + auto respJson = json::parse(resp.body, nullptr, false); + + if (resp.statusCode != 200 || respJson.is_discarded()) + { + string msg = "Ely.by auth failed"; + if (!respJson.is_discarded()) msg = respJson.value("errorMessage", msg); + result = {false, {}, {}, {}, convStringToWstring(msg)}; + state = AuthFlowState::FAILED; + return; + } + + string accessToken = respJson.value("accessToken", ""); + string uuid, name; + try { uuid = respJson["selectedProfile"].value("id", ""); name = respJson["selectedProfile"].value("name", ""); } catch (...) {} + + if (accessToken.empty() || uuid.empty() || name.empty()) + { + authFail(result, state, L"Ely.by response missing profile"); + return; + } + + result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), {}}; + state = AuthFlowState::COMPLETE; +} diff --git a/Minecraft.Client/AuthScreen.h b/Minecraft.Client/AuthScreen.h index f31f294aa4..d01ab41aa6 100644 --- a/Minecraft.Client/AuthScreen.h +++ b/Minecraft.Client/AuthScreen.h @@ -1,5 +1,7 @@ #pragma once using namespace std; +#include +#include struct AuthProfile { @@ -20,7 +22,7 @@ class AuthProfileManager public: static void load(); static void save(); - static void addProfile(AuthProfile::Type type, const wstring &username); + static void addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid = L"", const wstring &token = L""); static void removeSelectedProfile(); static bool applySelectedProfile(); @@ -28,3 +30,45 @@ class AuthProfileManager static int getSelectedIndex() { return selectedProfile; } static void setSelectedIndex(int idx) { selectedProfile = idx; } }; +struct AuthResult +{ + bool success; + wstring username; + wstring uuid; + wstring accessToken; + wstring error; +}; + +enum class AuthFlowState : uint8_t +{ + IDLE, + WAITING_FOR_USER, + POLLING, + EXCHANGING, + COMPLETE, + FAILED +}; + +class AuthFlow +{ +private: + static std::thread workerThread; + static std::atomic state; + static std::atomic cancelRequested; + static AuthResult result; + static wstring userCode; + static wstring verificationUri; + + static void microsoftFlowThread(); + static void elybyFlowThread(const string &username, const string &password); + +public: + static void startMicrosoft(); + static void startElyBy(const wstring &username, const wstring &password); + + static AuthFlowState getState() { return state.load(); } + static const AuthResult &getResult() { return result; } + static const wstring &getUserCode() { return userCode; } + static const wstring &getVerificationUri() { return verificationUri; } + static void reset(); +}; diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index 92402784aa..2661535298 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -56,6 +56,7 @@ #include "DLCTexturePack.h" #include "..\Minecraft.World\HandshakeManager.h" #include "..\Minecraft.World\AuthModule.h" +#include "AuthScreen.h" #ifdef _WINDOWS64 #include "Xbox\Network\NetworkPlayerXbox.h" @@ -4143,6 +4144,17 @@ void ClientConnection::beginAuth() handshakeManager->registerModule(new KeypairOfflineAuthModule()); handshakeManager->registerModule(new OfflineAuthModule()); + const auto &profiles = AuthProfileManager::getProfiles(); + int idx = AuthProfileManager::getSelectedIndex(); + if (idx >= 0 && idx < static_cast(profiles.size())) + { + const auto &p = profiles[idx]; + wstring variation; + if (p.type == AuthProfile::MICROSOFT) variation = L"mojang"; + else if (p.type == AuthProfile::ELYBY) variation = L"elyby"; + handshakeManager->setCredentials(p.token, p.uid, p.username, variation); + } + auto initial = handshakeManager->createInitialPacket(); if (initial) send(initial); } @@ -4155,6 +4167,8 @@ void ClientConnection::handleAuth(const shared_ptr &packet) auto response = handshakeManager->handlePacket(packet); if (response) send(response); + for (auto &p : handshakeManager->drainPendingPackets()) + send(p); if (handshakeManager->isComplete()) { authComplete = true; diff --git a/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp b/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp index 35edf17fda..e21cf85b5f 100644 --- a/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp +++ b/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp @@ -27,6 +27,7 @@ UIScene_Keyboard::UIScene_Keyboard(int iPad, void *initData, UILayer *parentLaye const wchar_t* defaultText = L""; m_bPCMode = false; + m_eKeyboardMode = C_4JInput::EKeyboardMode_Default; if (initData) { UIKeyboardInitData* kbData = static_cast(initData); @@ -36,6 +37,7 @@ UIScene_Keyboard::UIScene_Keyboard(int iPad, void *initData, UILayer *parentLaye if (kbData->defaultText) defaultText = kbData->defaultText; m_win64MaxChars = kbData->maxChars; m_bPCMode = kbData->pcMode; + m_eKeyboardMode = kbData->keyboardMode; } m_win64TextBuffer = defaultText; @@ -276,7 +278,12 @@ void UIScene_Keyboard::tick() } if (changed) - m_KeyboardTextInput.setLabel(m_win64TextBuffer.c_str(), true /*instant*/); + { + if (m_eKeyboardMode == C_4JInput::EKeyboardMode_Password) + m_KeyboardTextInput.setLabel(wstring(m_win64TextBuffer.length(), L'*').c_str(), true); + else + m_KeyboardTextInput.setLabel(m_win64TextBuffer.c_str(), true /*instant*/); + } if (m_bPCMode) { diff --git a/Minecraft.Client/Common/UI/UIScene_Keyboard.h b/Minecraft.Client/Common/UI/UIScene_Keyboard.h index 146934c1f6..72ada08fdc 100644 --- a/Minecraft.Client/Common/UI/UIScene_Keyboard.h +++ b/Minecraft.Client/Common/UI/UIScene_Keyboard.h @@ -13,6 +13,7 @@ class UIScene_Keyboard : public UIScene wstring m_win64TextBuffer; int m_win64MaxChars; bool m_bPCMode; // Hides on-screen keyboard buttons; physical keyboard only + C_4JInput::EKeyboardMode m_eKeyboardMode; int m_iCursorPos; #endif diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index f9dbc6ebc5..a2095cd370 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -33,6 +33,9 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye m_eAction=eAction_None; m_bIgnorePress=false; + // auto-apply saved auth profile on startup + AuthProfileManager::load(); + AuthProfileManager::applySelectedProfile(); m_buttons[static_cast(eControl_PlayGame)].init(IDS_PLAY_GAME,eControl_PlayGame); @@ -455,6 +458,41 @@ void UIScene_MainMenu::RunAction(int iPad) } static int s_authPad = 0; +static void *s_authParam = nullptr; +static wstring s_elybyUsername; +static bool s_authFlowActive = false; + +static wstring ReadKeyboardText(int maxLen) +{ + vector buf(maxLen, 0); +#ifdef _WINDOWS64 + Win64_GetKeyboardText(buf.data(), maxLen); +#else + InputManager.GetText(buf.data()); +#endif + wstring result; + for (int i = 0; i < maxLen && buf[i]; i++) + result += static_cast(buf[i]); + return result; +} + +static void ShowAuthMessageBox(int iPad, const wchar_t *title, const wchar_t *text, + const wchar_t **options, int optionCount, + int(*func)(LPVOID, int, C4JStorage::EMessageResult), void *lpParam, bool keepOpen = false) +{ + MessageBoxInfo param = {}; + param.uiOptionC = optionCount; + param.dwPad = iPad; + param.Func = func; + param.lpParam = lpParam; + param.rawTitle = title; + param.rawText = text; + param.rawOptions = options; + ui.NavigateToScene(iPad, eUIScene_MessageBox, ¶m, eUILayer_Alert, eUIGroup_Fullscreen); + if (keepOpen) + if (auto *scene = ui.FindScene(eUIScene_MessageBox)) + static_cast(scene)->setKeepOpen(true); +} static const wchar_t *BuildAuthProfileText() { @@ -484,34 +522,18 @@ static const wchar_t *BuildAuthProfileText() void UIScene_MainMenu::ShowAuthMenu(int iPad, void *pClass) { s_authPad = iPad; + s_authParam = pClass; static const wchar_t *authOptions[] = { L"Next", L"Use", L"Add", L"Back" }; - MessageBoxInfo param = {}; - param.uiOptionC = 4; - param.dwPad = iPad; - param.Func = &UIScene_MainMenu::AuthMenuReturned; - param.lpParam = pClass; - param.rawTitle = L"Authentication"; - param.rawText = BuildAuthProfileText(); - param.rawOptions = authOptions; - ui.NavigateToScene(iPad, eUIScene_MessageBox, ¶m, eUILayer_Alert, eUIGroup_Fullscreen); - UIScene *scene = ui.FindScene(eUIScene_MessageBox); - if (scene) - static_cast(scene)->setKeepOpen(true); + ShowAuthMessageBox(iPad, L"Authentication", BuildAuthProfileText(), + authOptions, 4, &UIScene_MainMenu::AuthMenuReturned, pClass, true); } void UIScene_MainMenu::ShowAuthAddMenu(int iPad, void *pClass) { static const wchar_t *addOptions[] = { L"Microsoft Auth", L"Ely.by Auth", L"Add Offline User", L"Back" }; - MessageBoxInfo param = {}; - param.uiOptionC = 4; - param.dwPad = iPad; - param.Func = &UIScene_MainMenu::AuthAddMenuReturned; - param.lpParam = pClass; - param.rawTitle = L"Add Profile"; - param.rawText = L"Select an auth type"; - param.rawOptions = addOptions; - ui.NavigateToScene(iPad, eUIScene_MessageBox, ¶m, eUILayer_Alert, eUIGroup_Fullscreen); + ShowAuthMessageBox(iPad, L"Add Profile", L"Select an auth type", + addOptions, 4, &UIScene_MainMenu::AuthAddMenuReturned, pClass); } int UIScene_MainMenu::AuthMenuReturned(LPVOID lpParam, int iPad, const C4JStorage::EMessageResult result) @@ -534,6 +556,7 @@ int UIScene_MainMenu::AuthMenuReturned(LPVOID lpParam, int iPad, const C4JStorag { ui.NavigateBack(iPad); AuthProfileManager::applySelectedProfile(); + AuthProfileManager::save(); break; } case C4JStorage::EMessage_ResultThirdOption: @@ -556,13 +579,31 @@ int UIScene_MainMenu::AuthAddMenuReturned(LPVOID lpParam, int iPad, const C4JSto switch (result) { case C4JStorage::EMessage_ResultAccept: - AuthProfileManager::addProfile(AuthProfile::MICROSOFT, L"Player"); - ShowAuthMenu(iPad, lpParam); + { + AuthFlow::startMicrosoft(); + s_authFlowActive = true; + + static const wchar_t *cancelOptions[] = { L"Cancel" }; + ShowAuthMessageBox(iPad, L"Microsoft Login", L"Starting...", + cancelOptions, 1, &UIScene_MainMenu::AuthMsFlowReturned, lpParam, true); break; + } case C4JStorage::EMessage_ResultDecline: - AuthProfileManager::addProfile(AuthProfile::ELYBY, L"Player"); - ShowAuthMenu(iPad, lpParam); + { +#ifdef _WINDOWS64 + UIKeyboardInitData kbData; + kbData.title = L"Ely.by Username"; + kbData.defaultText = L""; + kbData.maxChars = 64; + kbData.callback = &UIScene_MainMenu::ElyByUsernameReturned; + kbData.lpParam = lpParam; + kbData.pcMode = g_KBMInput.IsKBMActive(); + ui.NavigateToScene(iPad, eUIScene_Keyboard, &kbData); +#else + InputManager.RequestKeyboard(L"Ely.by Username", L"", (DWORD)0, 64, &UIScene_MainMenu::ElyByUsernameReturned, lpParam, C_4JInput::EKeyboardMode_Default); +#endif break; + } case C4JStorage::EMessage_ResultThirdOption: { #ifdef _WINDOWS64 @@ -586,29 +627,77 @@ int UIScene_MainMenu::AuthAddMenuReturned(LPVOID lpParam, int iPad, const C4JSto return 0; } -int UIScene_MainMenu::AuthKeyboardReturned(LPVOID lpParam, const bool bRes) +int UIScene_MainMenu::AuthMsFlowReturned(LPVOID lpParam, int iPad, const C4JStorage::EMessageResult result) +{ + s_authFlowActive = false; + AuthFlow::reset(); + ui.NavigateBack(iPad); + ShowAuthMenu(iPad, lpParam); + return 0; +} + +int UIScene_MainMenu::ElyByUsernameReturned(LPVOID lpParam, const bool bRes) { if (bRes) { - uint16_t ui16Text[128]; - ZeroMemory(ui16Text, 128 * sizeof(uint16_t)); + wstring name = ReadKeyboardText(128); + if (!name.empty()) + { + s_elybyUsername = std::move(name); #ifdef _WINDOWS64 - Win64_GetKeyboardText(ui16Text, 128); + UIKeyboardInitData kbData; + kbData.title = L"Ely.by Password"; + kbData.defaultText = L""; + kbData.maxChars = 128; + kbData.callback = &UIScene_MainMenu::ElyByPasswordReturned; + kbData.lpParam = lpParam; + kbData.pcMode = g_KBMInput.IsKBMActive(); + kbData.keyboardMode = C_4JInput::EKeyboardMode_Password; + ui.NavigateToScene(s_authPad, eUIScene_Keyboard, &kbData); #else - InputManager.GetText(ui16Text); + InputManager.RequestKeyboard(L"Ely.by Password", L"", (DWORD)0, 128, &UIScene_MainMenu::ElyByPasswordReturned, lpParam, C_4JInput::EKeyboardMode_Password); #endif - if (ui16Text[0] != 0) + return 0; + } + } + ShowAuthMenu(s_authPad, lpParam); + return 0; +} + +int UIScene_MainMenu::ElyByPasswordReturned(LPVOID lpParam, const bool bRes) +{ + if (bRes) + { + wstring pass = ReadKeyboardText(256); + if (!pass.empty()) { - wchar_t wName[128] = {}; - for (int k = 0; k < 127 && ui16Text[k]; k++) - wName[k] = static_cast(ui16Text[k]); - AuthProfileManager::addProfile(AuthProfile::OFFLINE, wName); + AuthFlow::startElyBy(s_elybyUsername, pass); + s_authFlowActive = true; + + static const wchar_t *waitOptions[] = { L"Cancel" }; + ShowAuthMessageBox(s_authPad, L"Ely.by Login", L"Authenticating...", + waitOptions, 1, &UIScene_MainMenu::AuthMsFlowReturned, lpParam, true); + + SecureZeroMemory(pass.data(), pass.size() * sizeof(wchar_t)); + return 0; } } ShowAuthMenu(s_authPad, lpParam); return 0; } +int UIScene_MainMenu::AuthKeyboardReturned(LPVOID lpParam, const bool bRes) +{ + if (bRes) + { + wstring name = ReadKeyboardText(128); + if (!name.empty()) + AuthProfileManager::addProfile(AuthProfile::OFFLINE, name); + } + ShowAuthMenu(s_authPad, lpParam); + return 0; +} + void UIScene_MainMenu::customDraw(IggyCustomDrawCallbackRegion *region) { if(wcscmp((wchar_t *)region->name,L"Splash")==0) @@ -2016,6 +2105,54 @@ void UIScene_MainMenu::RunUnlockOrDLC(int iPad) void UIScene_MainMenu::tick() { UIScene::tick(); + if (s_authFlowActive) + { + auto flowState = AuthFlow::getState(); + auto *scene = ui.FindScene(eUIScene_MessageBox); + auto *msgBox = scene ? static_cast(scene) : nullptr; + + switch (flowState) + { + case AuthFlowState::WAITING_FOR_USER: + case AuthFlowState::POLLING: + { + if (msgBox && !AuthFlow::getUserCode().empty()) + { + static wstring statusText; + statusText = L"Go to: " + AuthFlow::getVerificationUri() + L"\nEnter code: " + AuthFlow::getUserCode(); + if (flowState == AuthFlowState::POLLING) statusText += L"\n\nWaiting for login..."; + msgBox->updateContent(statusText.c_str()); + } + break; + } + case AuthFlowState::EXCHANGING: + { + if (msgBox) msgBox->updateContent(L"Exchanging tokens..."); + break; + } + case AuthFlowState::COMPLETE: + { + s_authFlowActive = false; + const auto &r = AuthFlow::getResult(); + auto type = AuthFlow::getUserCode().empty() ? AuthProfile::ELYBY : AuthProfile::MICROSOFT; + AuthProfileManager::addProfile(type, r.username, r.uuid, r.accessToken); + AuthFlow::reset(); + if (scene) ui.NavigateBack(s_authPad); + ShowAuthMenu(s_authPad, s_authParam); + break; + } + case AuthFlowState::FAILED: + { + s_authFlowActive = false; + const auto &r = AuthFlow::getResult(); + if (msgBox) msgBox->updateContent(r.error.empty() ? L"Authentication failed" : r.error.c_str()); + AuthFlow::reset(); + break; + } + default: + break; + } + } if ( (eNavigateWhenReady >= 0) ) { diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.h b/Minecraft.Client/Common/UI/UIScene_MainMenu.h index 4a059cce21..87de602c69 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.h @@ -150,6 +150,9 @@ class UIScene_MainMenu : public UIScene static int AuthMenuReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); static int AuthAddMenuReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); static int AuthKeyboardReturned(LPVOID lpParam, const bool bRes); + static int AuthMsFlowReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); + static int ElyByUsernameReturned(LPVOID lpParam, const bool bRes); + static int ElyByPasswordReturned(LPVOID lpParam, const bool bRes); static void LoadTrial(); diff --git a/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp b/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp index 9767b83bba..08556054d5 100644 --- a/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp @@ -144,6 +144,8 @@ void UIScene_MessageBox::handlePress(F64 controlId, F64 childId) void UIScene_MessageBox::updateContent(const wchar_t *text) { m_labelContent.init(text); + IggyDataValue result; + IggyPlayerCallMethodRS(getMovie(), &result, IggyPlayerRootPath(getMovie()), m_funcAutoResize, 0, nullptr); } bool UIScene_MessageBox::hasFocus(int iPad) diff --git a/Minecraft.Client/Common/UI/UIStructs.h b/Minecraft.Client/Common/UI/UIStructs.h index 28fcbcd186..2c08daffa7 100644 --- a/Minecraft.Client/Common/UI/UIStructs.h +++ b/Minecraft.Client/Common/UI/UIStructs.h @@ -295,8 +295,9 @@ typedef struct _UIKeyboardInitData int(*callback)(LPVOID, const bool); LPVOID lpParam; bool pcMode; // When true, disables on-screen keyboard buttons (PC keyboard users only need the text field) + C_4JInput::EKeyboardMode keyboardMode; - _UIKeyboardInitData() : title(nullptr), defaultText(nullptr), maxChars(25), callback(nullptr), lpParam(nullptr), pcMode(false) {} + _UIKeyboardInitData() : title(nullptr), defaultText(nullptr), maxChars(25), callback(nullptr), lpParam(nullptr), pcMode(false), keyboardMode(C_4JInput::EKeyboardMode_Default) {} } UIKeyboardInitData; // Stores the text typed in UIScene_Keyboard so callbacks can retrieve it diff --git a/Minecraft.World/AuthModule.cpp b/Minecraft.World/AuthModule.cpp index e283e2b38b..45b11734d1 100644 --- a/Minecraft.World/AuthModule.cpp +++ b/Minecraft.World/AuthModule.cpp @@ -5,11 +5,6 @@ #include "Common/vendor/nlohmann/json.hpp" #include -static string narrowStr(const wstring &w) -{ - return string(w.begin(), w.end()); -} - static wstring generateServerId() { static constexpr wchar_t hex[] = L"0123456789abcdef"; diff --git a/Minecraft.World/AuthModule.h b/Minecraft.World/AuthModule.h index 851b1260bf..e5f4a6fa4e 100644 --- a/Minecraft.World/AuthModule.h +++ b/Minecraft.World/AuthModule.h @@ -6,6 +6,11 @@ using namespace std; #include #include +inline string narrowStr(const wstring &w) +{ + return string(w.begin(), w.end()); +} + class AuthModule { public: diff --git a/Minecraft.World/HandshakeManager.cpp b/Minecraft.World/HandshakeManager.cpp index 8b6b87d225..969c1a53c8 100644 --- a/Minecraft.World/HandshakeManager.cpp +++ b/Minecraft.World/HandshakeManager.cpp @@ -1,9 +1,19 @@ #include "stdafx.h" #include "HandshakeManager.h" #include "AuthModule.h" +#include "HttpClient.h" +#include "StringHelpers.h" +#include "Common/vendor/nlohmann/json.hpp" static constexpr auto PROTOCOL_VERSION = L"1.0"; +static wstring getField(const vector> &fields, const wchar_t *key) +{ + for (const auto &[k, v] : fields) + if (k == key) return v; + return {}; +} + HandshakeManager::HandshakeManager(bool isServer) : isServer(isServer), state(HandshakeState::IDLE), activeModule(nullptr) { @@ -20,6 +30,21 @@ void HandshakeManager::registerModule(AuthModule *module) modules[module->schemeName()] = module; } +void HandshakeManager::setCredentials(const wstring &token, const wstring &uid, const wstring &username, const wstring &variation) +{ + accessToken = token; + clientUid = uid; + clientUsername = username; + preferredVariation = variation; +} + +vector> HandshakeManager::drainPendingPackets() +{ + vector> out; + out.swap(pendingPackets); + return out; +} + shared_ptr HandshakeManager::handlePacket(const shared_ptr &packet) { return isServer ? handleServer(packet) : handleClient(packet); @@ -37,10 +62,7 @@ shared_ptr HandshakeManager::handleServer(const shared_ptrfields) - if (k == L"version") protocolVersion = v; - + protocolVersion = getField(packet->fields, L"version"); if (protocolVersion != PROTOCOL_VERSION) return fail(); @@ -58,11 +80,7 @@ shared_ptr HandshakeManager::handleServer(const shared_ptrfields) - if (k == L"variation") variation = v; - - activeVariation = variation; + activeVariation = getField(packet->fields, L"variation"); state = HandshakeState::SETTINGS_SENT; auto settings = activeModule->getSettings(activeVariation); return makePacket(AuthStage::SCHEME_SETTINGS, std::move(settings)); @@ -87,14 +105,7 @@ shared_ptr HandshakeManager::handleServer(const shared_ptrfields) - { - if (k == L"uid") uid = v; - else if (k == L"username") username = v; - } - - if (uid != finalUid || username != finalUsername) + if (getField(packet->fields, L"uid") != finalUid || getField(packet->fields, L"username") != finalUsername) return fail(); state = HandshakeState::IDENTITY_ASSIGNED; @@ -106,14 +117,7 @@ shared_ptr HandshakeManager::handleServer(const shared_ptrfields) - { - if (k == L"uid") uid = v; - else if (k == L"username") username = v; - } - - if (uid != finalUid || username != finalUsername) + if (getField(packet->fields, L"uid") != finalUid || getField(packet->fields, L"username") != finalUsername) return fail(); state = HandshakeState::COMPLETE; @@ -131,12 +135,8 @@ shared_ptr HandshakeManager::handleClient(const shared_ptrfields) - { - if (k == L"version") protocolVersion = v; - else if (k == L"scheme") scheme = v; - } + protocolVersion = getField(packet->fields, L"version"); + wstring scheme = getField(packet->fields, L"scheme"); if (protocolVersion != PROTOCOL_VERSION) return fail(); @@ -148,7 +148,11 @@ shared_ptr HandshakeManager::handleClient(const shared_ptrsecond; auto variations = activeModule->supportedVariations(); - activeVariation = variations.empty() ? L"" : variations[0]; + if (!preferredVariation.empty() && + std::find(variations.begin(), variations.end(), preferredVariation) != variations.end()) + activeVariation = preferredVariation; + else + activeVariation = variations.empty() ? L"" : variations[0]; state = HandshakeState::SCHEME_ACCEPTED; return makePacket(AuthStage::ACCEPT_SCHEME, {{L"variation", activeVariation}}); @@ -156,17 +160,38 @@ shared_ptr HandshakeManager::handleClient(const shared_ptrfields, L"serverId"); + wstring sessionEndpoint = getField(packet->fields, L"sessionEndpoint"); + wstring scheme(activeModule->schemeName()); + if (scheme == L"mcconsoles:session" && !accessToken.empty()) + { + nlohmann::json body = { + {"accessToken", narrowStr(accessToken)}, + {"selectedProfile", narrowStr(clientUid)}, + {"serverId", narrowStr(serverId)} + }; + auto resp = HttpClient::post(narrowStr(sessionEndpoint) + "/session/minecraft/join", body.dump()); + if (resp.statusCode != 204) + return fail(); + } + state = HandshakeState::AUTH_IN_PROGRESS; - return makePacket(AuthStage::BEGIN_AUTH); + pendingPackets.push_back(makePacket(AuthStage::BEGIN_AUTH)); + pendingPackets.push_back(makePacket(AuthStage::AUTH_DATA, { + {L"uid", clientUid}, + {L"username", clientUsername} + })); + pendingPackets.push_back(makePacket(AuthStage::AUTH_DONE, { + {L"uid", clientUid}, + {L"username", clientUsername} + })); + return nullptr; } case AuthStage::ASSIGN_IDENTITY: { - for (const auto &[k, v] : packet->fields) - { - if (k == L"uid") finalUid = v; - else if (k == L"username") finalUsername = v; - } + finalUid = getField(packet->fields, L"uid"); + finalUsername = getField(packet->fields, L"username"); state = HandshakeState::IDENTITY_CONFIRMED; return makePacket(AuthStage::CONFIRM_IDENTITY, { diff --git a/Minecraft.World/HandshakeManager.h b/Minecraft.World/HandshakeManager.h index 9e9da1c831..360f53eae8 100644 --- a/Minecraft.World/HandshakeManager.h +++ b/Minecraft.World/HandshakeManager.h @@ -33,6 +33,13 @@ class HandshakeManager wstring activeVariation; wstring protocolVersion; + wstring accessToken; + wstring clientUid; + wstring clientUsername; + wstring preferredVariation; + + vector> pendingPackets; + public: wstring finalUid; wstring finalUsername; @@ -41,8 +48,10 @@ class HandshakeManager ~HandshakeManager(); void registerModule(AuthModule *module); + void setCredentials(const wstring &token, const wstring &uid, const wstring &username, const wstring &variation = L""); shared_ptr handlePacket(const shared_ptr &packet); shared_ptr createInitialPacket(); + vector> drainPendingPackets(); bool isComplete() const { return state == HandshakeState::COMPLETE; } bool isFailed() const { return state == HandshakeState::FAILED; } diff --git a/Minecraft.World/HttpClient.cpp b/Minecraft.World/HttpClient.cpp index b59d1b1f97..6e92963576 100644 --- a/Minecraft.World/HttpClient.cpp +++ b/Minecraft.World/HttpClient.cpp @@ -9,13 +9,15 @@ static size_t writeCallback(char *data, size_t size, size_t nmemb, void *userdat return size * nmemb; } -static HttpResponse performRequest(CURL *curl) +static HttpResponse performRequest(CURL *curl, struct curl_slist *extraHeaders = nullptr) { std::string responseBody; curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + if (extraHeaders) + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, extraHeaders); CURLcode res = curl_easy_perform(curl); @@ -24,20 +26,30 @@ static HttpResponse performRequest(CURL *curl) curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode); curl_easy_cleanup(curl); + if (extraHeaders) + curl_slist_free_all(extraHeaders); return {statusCode, std::move(responseBody)}; } -HttpResponse HttpClient::get(const std::string &url) +static struct curl_slist *buildHeaders(const std::vector &headers) +{ + struct curl_slist *list = nullptr; + for (const auto &h : headers) + list = curl_slist_append(list, h.c_str()); + return list; +} + +HttpResponse HttpClient::get(const std::string &url, const std::vector &headers) { CURL *curl = curl_easy_init(); if (!curl) return {0, ""}; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - return performRequest(curl); + return performRequest(curl, headers.empty() ? nullptr : buildHeaders(headers)); } -HttpResponse HttpClient::post(const std::string &url, const std::string &body, const std::string &contentType) +HttpResponse HttpClient::post(const std::string &url, const std::string &body, const std::string &contentType, const std::vector &headers) { CURL *curl = curl_easy_init(); if (!curl) @@ -47,11 +59,8 @@ HttpResponse HttpClient::post(const std::string &url, const std::string &body, c curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); - struct curl_slist *headers = nullptr; - headers = curl_slist_append(headers, ("Content-Type: " + contentType).c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + auto *headerList = buildHeaders(headers); + headerList = curl_slist_append(headerList, ("Content-Type: " + contentType).c_str()); - HttpResponse resp = performRequest(curl); - curl_slist_free_all(headers); - return resp; + return performRequest(curl, headerList); } diff --git a/Minecraft.World/HttpClient.h b/Minecraft.World/HttpClient.h index 317941c01d..f08c1e797f 100644 --- a/Minecraft.World/HttpClient.h +++ b/Minecraft.World/HttpClient.h @@ -1,6 +1,7 @@ #pragma once #include +#include struct HttpResponse { @@ -11,6 +12,6 @@ struct HttpResponse class HttpClient { public: - static HttpResponse get(const std::string &url); - static HttpResponse post(const std::string &url, const std::string &body, const std::string &contentType = "application/json"); + static HttpResponse get(const std::string &url, const std::vector &headers = {}); + static HttpResponse post(const std::string &url, const std::string &body, const std::string &contentType = "application/json", const std::vector &headers = {}); }; From 452588fc5d11e6dd022562c40465db2af7145f57 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Fri, 3 Apr 2026 23:40:48 -0400 Subject: [PATCH 08/18] patch: make client token handling better --- Minecraft.Client/AuthScreen.cpp | 216 +++++++++++------- Minecraft.Client/AuthScreen.h | 4 +- .../Common/UI/UIScene_MainMenu.cpp | 2 +- 3 files changed, 142 insertions(+), 80 deletions(-) diff --git a/Minecraft.Client/AuthScreen.cpp b/Minecraft.Client/AuthScreen.cpp index c0d1d89464..9376b9efb9 100644 --- a/Minecraft.Client/AuthScreen.cpp +++ b/Minecraft.Client/AuthScreen.cpp @@ -12,6 +12,11 @@ using json = nlohmann::json; static constexpr auto PROFILES_FILE = L"auth_profiles.dat"; static constexpr auto MS_CLIENT_ID = "00000000441cc96b"; +static json parseResponse(const HttpResponse &resp, int expectedStatus = 200); +static bool msTokenExchange(const string &msAccessToken, string &mcToken, string &profId, string &profName); +static bool msRefreshOAuth(const string &refreshToken, string &newAccessToken, string &newRefreshToken); +static bool elybyValidate(const string &accessToken, const string &clientToken); +static bool elybyRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken); vector AuthProfileManager::profiles; int AuthProfileManager::selectedProfile = -1; @@ -45,6 +50,7 @@ void AuthProfileManager::load() p.uid = readWstr(); p.username = readWstr(); p.token = readWstr(); + p.clientToken = readWstr(); profiles.push_back(std::move(p)); } @@ -75,16 +81,17 @@ void AuthProfileManager::save() writeWstr(p.uid); writeWstr(p.username); writeWstr(p.token); + writeWstr(p.clientToken); } int32_t idx = static_cast(selectedProfile); file.write(reinterpret_cast(&idx), sizeof(idx)); } -void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid, const wstring &token) +void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid, const wstring &token, const wstring &clientToken) { wstring finalUid = uid.empty() ? L"offline_" + username : uid; - profiles.push_back({type, finalUid, username, token}); + profiles.push_back({type, finalUid, username, token, clientToken}); selectedProfile = static_cast(profiles.size()) - 1; save(); } @@ -105,7 +112,44 @@ bool AuthProfileManager::applySelectedProfile() if (selectedProfile < 0 || selectedProfile >= static_cast(profiles.size())) return false; - const auto &p = profiles[selectedProfile]; + auto &p = profiles[selectedProfile]; + + if (p.type == AuthProfile::MICROSOFT && !p.clientToken.empty()) + { + auto checkResp = HttpClient::get("https://api.minecraftservices.com/minecraft/profile", + {"Authorization: Bearer " + narrowStr(p.token)}); + + if (checkResp.statusCode != 200) + { + string newMsAccess, newMsRefresh; + if (msRefreshOAuth(narrowStr(p.clientToken), newMsAccess, newMsRefresh)) + { + string mcToken, profId, profName; + if (msTokenExchange(newMsAccess, mcToken, profId, profName)) + { + p.token = convStringToWstring(mcToken); + p.clientToken = convStringToWstring(newMsRefresh); + p.username = convStringToWstring(profName); + p.uid = convStringToWstring(profId); + save(); + } + } + } + } + else if (p.type == AuthProfile::ELYBY && !p.token.empty()) + { + if (!elybyValidate(narrowStr(p.token), narrowStr(p.clientToken))) + { + string newAccess, newClient; + if (elybyRefresh(narrowStr(p.token), narrowStr(p.clientToken), newAccess, newClient)) + { + p.token = convStringToWstring(newAccess); + if (!newClient.empty()) p.clientToken = convStringToWstring(newClient); + save(); + } + } + } + auto *mc = Minecraft::GetInstance(); if (mc->user) @@ -161,16 +205,95 @@ void AuthFlow::startElyBy(const wstring &username, const wstring &password) static void authFail(AuthResult &result, std::atomic &state, const wchar_t *msg) { - result = {false, {}, {}, {}, msg}; + result = {false, {}, {}, {}, {}, msg}; state = AuthFlowState::FAILED; } // parse json response body, return discarded json on bad status -static json parseResponse(const HttpResponse &resp, int expectedStatus = 200) +static json parseResponse(const HttpResponse &resp, int expectedStatus) { if (resp.statusCode != expectedStatus) return json::value_t::discarded; return json::parse(resp.body, nullptr, false); } +static bool msTokenExchange(const string &msAccessToken, string &mcToken, string &profId, string &profName) +{ + auto xblResp = HttpClient::post("https://user.auth.xboxlive.com/user/authenticate", json({ + {"Properties", {{"AuthMethod", "RPS"}, {"SiteName", "user.auth.xboxlive.com"}, {"RpsTicket", msAccessToken}}}, + {"RelyingParty", "http://auth.xboxlive.com"}, + {"TokenType", "JWT"} + }).dump()); + + auto xblJson = parseResponse(xblResp); + if (xblJson.is_discarded()) return false; + + string xblToken = xblJson.value("Token", ""); + string userHash; + try { userHash = xblJson["DisplayClaims"]["xui"][0].value("uhs", ""); } catch (...) {} + if (xblToken.empty() || userHash.empty()) return false; + + auto xstsResp = HttpClient::post("https://xsts.auth.xboxlive.com/xsts/authorize", json({ + {"Properties", {{"SandboxId", "RETAIL"}, {"UserTokens", {xblToken}}}}, + {"RelyingParty", "rp://api.minecraftservices.com/"}, + {"TokenType", "JWT"} + }).dump()); + + auto xstsJson = parseResponse(xstsResp); + string xstsToken = xstsJson.is_discarded() ? "" : xstsJson.value("Token", ""); + if (xstsToken.empty()) return false; + + auto mcResp = HttpClient::post("https://api.minecraftservices.com/authentication/login_with_xbox", + json({{"identityToken", "XBL3.0 x=" + userHash + ";" + xstsToken}}).dump()); + + auto mcJson = parseResponse(mcResp); + mcToken = mcJson.is_discarded() ? "" : mcJson.value("access_token", ""); + if (mcToken.empty()) return false; + + auto profResp = HttpClient::get("https://api.minecraftservices.com/minecraft/profile", + {"Authorization: Bearer " + mcToken}); + + auto profJson = parseResponse(profResp); + if (profJson.is_discarded()) return false; + + profId = profJson.value("id", ""); + profName = profJson.value("name", ""); + return !profId.empty() && !profName.empty(); +} + +static bool msRefreshOAuth(const string &refreshToken, string &newAccessToken, string &newRefreshToken) +{ + auto resp = HttpClient::post("https://login.live.com/oauth20_token.srf", + "client_id=" + string(MS_CLIENT_ID) + "&refresh_token=" + refreshToken + "&grant_type=refresh_token&scope=service::user.auth.xboxlive.com::MBI_SSL", + "application/x-www-form-urlencoded"); + + auto j = parseResponse(resp); + if (j.is_discarded()) return false; + + newAccessToken = j.value("access_token", ""); + newRefreshToken = j.value("refresh_token", ""); + return !newAccessToken.empty(); +} + +// validate ely.by token via yggdrasil /validate endpoint +static bool elybyValidate(const string &accessToken, const string &clientToken) +{ + auto resp = HttpClient::post("https://authserver.ely.by/auth/validate", + json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump()); + return resp.statusCode == 200; +} + +// refresh ely.by token via yggdrasil /refresh endpoint +static bool elybyRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken) +{ + auto resp = HttpClient::post("https://authserver.ely.by/auth/refresh", + json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump()); + + auto j = parseResponse(resp); + if (j.is_discarded()) return false; + + newAccessToken = j.value("accessToken", ""); + newClientToken = j.value("clientToken", ""); + return !newAccessToken.empty(); +} void AuthFlow::microsoftFlowThread() { @@ -221,6 +344,7 @@ void AuthFlow::microsoftFlowThread() state = AuthFlowState::POLLING; string msAccessToken; + string msRefreshToken; for (int attempt = 0; attempt < 180; attempt++) { @@ -242,6 +366,7 @@ void AuthFlow::microsoftFlowThread() if (pollResp.statusCode == 200) { msAccessToken = pollJson.value("access_token", ""); + msRefreshToken = pollJson.value("refresh_token", ""); if (!msAccessToken.empty()) break; } @@ -250,7 +375,7 @@ void AuthFlow::microsoftFlowThread() if (err == "slow_down") { interval += 5; continue; } if (!err.empty()) { - result = {false, {}, {}, {}, convStringToWstring("Auth error: " + err)}; + result = {false, {}, {}, {}, {}, convStringToWstring("Auth error: " + err)}; state = AuthFlowState::FAILED; return; } @@ -265,80 +390,14 @@ void AuthFlow::microsoftFlowThread() state = AuthFlowState::EXCHANGING; if (cancelRequested) return; - // xbox live auth - auto xblResp = HttpClient::post("https://user.auth.xboxlive.com/user/authenticate", json({ - {"Properties", {{"AuthMethod", "RPS"}, {"SiteName", "user.auth.xboxlive.com"}, {"RpsTicket", msAccessToken}}}, - {"RelyingParty", "http://auth.xboxlive.com"}, - {"TokenType", "JWT"} - }).dump()); - - auto xblJson = parseResponse(xblResp); - if (xblJson.is_discarded()) - { - authFail(result, state, L"Xbox Live auth failed"); - return; - } - - string xblToken = xblJson.value("Token", ""); - string userHash; - try { userHash = xblJson["DisplayClaims"]["xui"][0].value("uhs", ""); } catch (...) {} - - if (xblToken.empty() || userHash.empty()) - { - authFail(result, state, L"Bad Xbox Live response"); - return; - } - - // xsts auth - auto xstsResp = HttpClient::post("https://xsts.auth.xboxlive.com/xsts/authorize", json({ - {"Properties", {{"SandboxId", "RETAIL"}, {"UserTokens", {xblToken}}}}, - {"RelyingParty", "rp://api.minecraftservices.com/"}, - {"TokenType", "JWT"} - }).dump()); - - auto xstsJson = parseResponse(xstsResp); - string xstsToken = xstsJson.is_discarded() ? "" : xstsJson.value("Token", ""); - - if (xstsToken.empty()) - { - authFail(result, state, L"XSTS auth failed"); - return; - } - - // minecraft login - auto mcResp = HttpClient::post("https://api.minecraftservices.com/authentication/login_with_xbox", - json({{"identityToken", "XBL3.0 x=" + userHash + ";" + xstsToken}}).dump()); - - auto mcJson = parseResponse(mcResp); - string mcAccessToken = mcJson.is_discarded() ? "" : mcJson.value("access_token", ""); - - if (mcAccessToken.empty()) - { - authFail(result, state, L"Minecraft auth failed"); - return; - } - - // get profile - auto profResp = HttpClient::get("https://api.minecraftservices.com/minecraft/profile", - {"Authorization: Bearer " + mcAccessToken}); - - auto profJson = parseResponse(profResp); - if (profJson.is_discarded()) - { - authFail(result, state, L"Failed to get Minecraft profile"); - return; - } - - string profId = profJson.value("id", ""); - string profName = profJson.value("name", ""); - - if (profId.empty() || profName.empty()) + string mcAccessToken, profId, profName; + if (!msTokenExchange(msAccessToken, mcAccessToken, profId, profName)) { - authFail(result, state, L"Profile missing id or name"); + authFail(result, state, L"Token exchange failed"); return; } - result = {true, convStringToWstring(profName), convStringToWstring(profId), convStringToWstring(mcAccessToken), {}}; + result = {true, convStringToWstring(profName), convStringToWstring(profId), convStringToWstring(mcAccessToken), convStringToWstring(msRefreshToken), {}}; state = AuthFlowState::COMPLETE; } @@ -357,12 +416,13 @@ void AuthFlow::elybyFlowThread(const string &username, const string &password) { string msg = "Ely.by auth failed"; if (!respJson.is_discarded()) msg = respJson.value("errorMessage", msg); - result = {false, {}, {}, {}, convStringToWstring(msg)}; + result = {false, {}, {}, {}, {}, convStringToWstring(msg)}; state = AuthFlowState::FAILED; return; } string accessToken = respJson.value("accessToken", ""); + string elyClientToken = respJson.value("clientToken", ""); string uuid, name; try { uuid = respJson["selectedProfile"].value("id", ""); name = respJson["selectedProfile"].value("name", ""); } catch (...) {} @@ -372,6 +432,6 @@ void AuthFlow::elybyFlowThread(const string &username, const string &password) return; } - result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), {}}; + result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), convStringToWstring(elyClientToken), {}}; state = AuthFlowState::COMPLETE; } diff --git a/Minecraft.Client/AuthScreen.h b/Minecraft.Client/AuthScreen.h index d01ab41aa6..c24afc262d 100644 --- a/Minecraft.Client/AuthScreen.h +++ b/Minecraft.Client/AuthScreen.h @@ -11,6 +11,7 @@ struct AuthProfile wstring uid; wstring username; wstring token; + wstring clientToken; }; class AuthProfileManager @@ -22,7 +23,7 @@ class AuthProfileManager public: static void load(); static void save(); - static void addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid = L"", const wstring &token = L""); + static void addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid = L"", const wstring &token = L"", const wstring &clientToken = L""); static void removeSelectedProfile(); static bool applySelectedProfile(); @@ -36,6 +37,7 @@ struct AuthResult wstring username; wstring uuid; wstring accessToken; + wstring clientToken; wstring error; }; diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index a2095cd370..139f6b7789 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -2135,7 +2135,7 @@ void UIScene_MainMenu::tick() s_authFlowActive = false; const auto &r = AuthFlow::getResult(); auto type = AuthFlow::getUserCode().empty() ? AuthProfile::ELYBY : AuthProfile::MICROSOFT; - AuthProfileManager::addProfile(type, r.username, r.uuid, r.accessToken); + AuthProfileManager::addProfile(type, r.username, r.uuid, r.accessToken, r.clientToken); AuthFlow::reset(); if (scene) ui.NavigateBack(s_authPad); ShowAuthMenu(s_authPad, s_authParam); From 6169b133ec80e6ed11c0d2585593d15f1a7a22ea Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sat, 4 Apr 2026 01:14:32 -0400 Subject: [PATCH 09/18] patch: add GameUUID for UUID handling --- Minecraft.World/UUID.cpp | 160 +++++++++++++++++++++ Minecraft.World/UUID.h | 27 ++++ Minecraft.World/cmake/sources/Common.cmake | 2 + 3 files changed, 189 insertions(+) create mode 100644 Minecraft.World/UUID.cpp create mode 100644 Minecraft.World/UUID.h diff --git a/Minecraft.World/UUID.cpp b/Minecraft.World/UUID.cpp new file mode 100644 index 0000000000..45e1bf9b6c --- /dev/null +++ b/Minecraft.World/UUID.cpp @@ -0,0 +1,160 @@ +#include "stdafx.h" +#include "UUID.h" +#include "Random.h" +#include +static void sha1(const uint8_t* data, size_t len, uint8_t out[20]) +{ + uint32_t h[5] = { 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0 }; + uint64_t bitLen = len * 8; + size_t fullBlocks = len / 64; + for (size_t blk = 0; blk < fullBlocks; blk++) { + const uint8_t* p = data + blk * 64; + uint32_t w[80]; + for (int i = 0; i < 16; i++) + w[i] = (p[i * 4] << 24) | (p[i * 4 + 1] << 16) | (p[i * 4 + 2] << 8) | p[i * 4 + 3]; + for (int i = 16; i < 80; i++) { + uint32_t x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; + w[i] = (x << 1) | (x >> 31); + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4]; + for (int i = 0; i < 80; i++) { + uint32_t f, k; + if (i < 20) { f = (b & c) | (~b & d); k = 0x5A827999; } + else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; } + else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; } + else { f = b ^ c ^ d; k = 0xCA62C1D6; } + uint32_t tmp = ((a << 5) | (a >> 27)) + f + e + k + w[i]; + e = d; d = c; c = (b << 30) | (b >> 2); b = a; a = tmp; + } + h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; + } + uint8_t tail[128] = {}; + size_t rem = len - fullBlocks * 64; + if (rem) memcpy(tail, data + fullBlocks * 64, rem); + tail[rem] = 0x80; + size_t tailLen = (rem < 56) ? 64 : 128; + for (int i = 0; i < 8; i++) + tail[tailLen - 1 - i] = (uint8_t)(bitLen >> (i * 8)); + + for (size_t off = 0; off < tailLen; off += 64) { + uint32_t w[80]; + for (int i = 0; i < 16; i++) + w[i] = (tail[off + i * 4] << 24) | (tail[off + i * 4 + 1] << 16) | + (tail[off + i * 4 + 2] << 8) | tail[off + i * 4 + 3]; + for (int i = 16; i < 80; i++) { + uint32_t x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; + w[i] = (x << 1) | (x >> 31); + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4]; + for (int i = 0; i < 80; i++) { + uint32_t f, k; + if (i < 20) { f = (b & c) | (~b & d); k = 0x5A827999; } + else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; } + else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; } + else { f = b ^ c ^ d; k = 0xCA62C1D6; } + uint32_t tmp = ((a << 5) | (a >> 27)) + f + e + k + w[i]; + e = d; d = c; c = (b << 30) | (b >> 2); b = a; a = tmp; + } + h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; + } + + for (int i = 0; i < 5; i++) { + out[i * 4] = (uint8_t)(h[i] >> 24); + out[i * 4 + 1] = (uint8_t)(h[i] >> 16); + out[i * 4 + 2] = (uint8_t)(h[i] >> 8); + out[i * 4 + 3] = (uint8_t)(h[i]); + } +} +static constexpr char HEX[] = "0123456789abcdef"; +static constexpr uint8_t hexVal(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return 10 + c - 'a'; + if (c >= 'A' && c <= 'F') return 10 + c - 'A'; + return 0; +} + +void GameUUID::toBytes(uint8_t out[16]) const +{ + for (int i = 7; i >= 0; i--) out[7 - i] = (uint8_t)(msb >> (i * 8)); + for (int i = 7; i >= 0; i--) out[15 - i] = (uint8_t)(lsb >> (i * 8)); +} + +GameUUID GameUUID::fromBytes(const uint8_t b[16]) +{ + GameUUID u; + for (int i = 0; i < 8; i++) u.msb = (u.msb << 8) | b[i]; + for (int i = 0; i < 8; i++) u.lsb = (u.lsb << 8) | b[8 + i]; + return u; +} +std::string GameUUID::toString() const +{ + uint8_t b[16]; + toBytes(b); + char buf[37]; + int p = 0; + for (int i = 0; i < 16; i++) { + if (i == 4 || i == 6 || i == 8 || i == 10) buf[p++] = '-'; + buf[p++] = HEX[b[i] >> 4]; + buf[p++] = HEX[b[i] & 0xf]; + } + buf[p] = '\0'; + return buf; +} + +std::wstring GameUUID::toWString() const +{ + std::string s = toString(); + return { s.begin(), s.end() }; +} + +GameUUID GameUUID::fromString(const std::string& s) +{ + uint8_t b[16] = {}; + int bi = 0; + for (size_t i = 0; i < s.size() && bi < 16; i++) { + if (s[i] == '-') continue; + if (i + 1 >= s.size()) break; + b[bi++] = (hexVal(s[i]) << 4) | hexVal(s[i + 1]); + i++; + } + return fromBytes(b); +} + +GameUUID GameUUID::fromWString(const std::wstring& s) +{ + return fromString({ s.begin(), s.end() }); +} + +GameUUID GameUUID::v4(uint64_t high, uint64_t low) +{ + return { (high & ~0xF000ULL) | 0x4000ULL, (low & ~0xC000000000000000ULL) | 0x8000000000000000ULL }; +} + +GameUUID GameUUID::v5(const GameUUID& ns, const std::string& name) +{ + uint8_t input[256]; + ns.toBytes(input); + size_t total = 16 + name.size(); + // names over 240 chars would be insane but just in case + if (name.size() <= sizeof(input) - 16) + memcpy(input + 16, name.data(), name.size()); + + uint8_t hash[20]; + sha1(input, total, hash); + GameUUID u = fromBytes(hash); + u.msb = (u.msb & ~0xF000ULL) | 0x5000ULL; + u.lsb = (u.lsb & ~0xC000000000000000ULL) | 0x8000000000000000ULL; + return u; +} + +GameUUID GameUUID::fromXuid(uint64_t xuid) +{ + return v5(MCCONSOLES_NAMESPACE_UUID, std::to_string(xuid)); +} + +GameUUID GameUUID::random() +{ + Random r; + return v4(r.nextLong(), r.nextLong()); +} diff --git a/Minecraft.World/UUID.h b/Minecraft.World/UUID.h new file mode 100644 index 0000000000..3bce7773b6 --- /dev/null +++ b/Minecraft.World/UUID.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include + +struct GameUUID { + uint64_t msb = 0; + uint64_t lsb = 0; + + constexpr bool isNil() const { return msb == 0 && lsb == 0; } + constexpr bool operator==(const GameUUID& o) const { return msb == o.msb && lsb == o.lsb; } + constexpr bool operator!=(const GameUUID& o) const { return !(*this == o); } + constexpr bool operator<(const GameUUID& o) const { return msb < o.msb || (msb == o.msb && lsb < o.lsb); } + + void toBytes(uint8_t out[16]) const; + std::string toString() const; + std::wstring toWString() const; + + static GameUUID fromBytes(const uint8_t b[16]); + static GameUUID fromString(const std::string& s); + static GameUUID fromWString(const std::wstring& s); + static GameUUID v4(uint64_t high, uint64_t low); + static GameUUID v5(const GameUUID& ns, const std::string& name); + static GameUUID fromXuid(uint64_t xuid); + static GameUUID random(); +}; + +inline constexpr GameUUID MCCONSOLES_NAMESPACE_UUID = { 0x4d696e6563726166ULL, 0x74436f6e736f6c65ULL }; diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index 07b322d9fa..ae1fad9d0b 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -263,6 +263,8 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/AuthModule.h" "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.h" + "${CMAKE_CURRENT_SOURCE_DIR}/UUID.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/UUID.h" "${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/BlockRegionUpdatePacket.cpp" From 692a1ffc8ed5a8520caa5fc50ffd8cb1b75006f5 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sat, 4 Apr 2026 02:17:59 -0400 Subject: [PATCH 10/18] optimized gameuuid handling --- Minecraft.Client/ClientConnection.cpp | 1 + Minecraft.Server/Access/Access.cpp | 5 +- Minecraft.Server/Access/BanManager.cpp | 3 + Minecraft.Server/Access/BanManager.h | 1 + Minecraft.Server/Access/WhitelistManager.cpp | 3 + Minecraft.Server/Access/WhitelistManager.h | 1 + Minecraft.World/AddPlayerPacket.cpp | 7 +- Minecraft.World/AddPlayerPacket.h | 2 + Minecraft.World/LoginPacket.cpp | 4 ++ Minecraft.World/LoginPacket.h | 2 + Minecraft.World/Player.cpp | 2 + Minecraft.World/Player.h | 5 +- Minecraft.World/UUID.cpp | 70 ++++++++------------ 13 files changed, 61 insertions(+), 45 deletions(-) diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index 2661535298..5a33545b56 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -877,6 +877,7 @@ void ClientConnection::handleAddPlayer(shared_ptr packet) player->yRotp = packet->yRot; player->yHeadRot = packet->yHeadRot * 360 / 256.0f; player->setXuid(packet->xuid); + player->setGameUUID(packet->gameUuid); #ifdef _DURANGO // On Durango request player display name from network manager diff --git a/Minecraft.Server/Access/Access.cpp b/Minecraft.Server/Access/Access.cpp index 5767a9555c..85c7642b92 100644 --- a/Minecraft.Server/Access/Access.cpp +++ b/Minecraft.Server/Access/Access.cpp @@ -371,7 +371,10 @@ namespace ServerRuntime } auto whitelistManager = std::make_shared(*current); - const WhitelistedPlayerEntry entry = { formatted, name, metadata }; + WhitelistedPlayerEntry entry; + entry.xuid = formatted; + entry.name = name; + entry.metadata = metadata; if (!whitelistManager->AddPlayer(entry)) { return false; diff --git a/Minecraft.Server/Access/BanManager.cpp b/Minecraft.Server/Access/BanManager.cpp index 5447ab875c..76f14b00ed 100644 --- a/Minecraft.Server/Access/BanManager.cpp +++ b/Minecraft.Server/Access/BanManager.cpp @@ -199,6 +199,7 @@ namespace ServerRuntime } AccessStorageUtils::TryGetStringField(object, "name", &entry.name); + AccessStorageUtils::TryGetStringField(object, "uuid", &entry.uuid); AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created); AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source); AccessStorageUtils::TryGetStringField(object, "expires", &entry.metadata.expires); @@ -302,6 +303,8 @@ namespace ServerRuntime { OrderedJson object = OrderedJson::object(); object["xuid"] = AccessStorageUtils::NormalizeXuid(entry.xuid); + if (!entry.uuid.empty()) + object["uuid"] = entry.uuid; object["name"] = entry.name; object["created"] = entry.metadata.created; object["source"] = entry.metadata.source; diff --git a/Minecraft.Server/Access/BanManager.h b/Minecraft.Server/Access/BanManager.h index 59103bec7a..f02c2383e9 100644 --- a/Minecraft.Server/Access/BanManager.h +++ b/Minecraft.Server/Access/BanManager.h @@ -22,6 +22,7 @@ namespace ServerRuntime struct BannedPlayerEntry { std::string xuid; + std::string uuid; std::string name; BanMetadata metadata; }; diff --git a/Minecraft.Server/Access/WhitelistManager.cpp b/Minecraft.Server/Access/WhitelistManager.cpp index 3629914ed5..ec0104b751 100644 --- a/Minecraft.Server/Access/WhitelistManager.cpp +++ b/Minecraft.Server/Access/WhitelistManager.cpp @@ -117,6 +117,7 @@ namespace ServerRuntime } AccessStorageUtils::TryGetStringField(object, "name", &entry.name); + AccessStorageUtils::TryGetStringField(object, "uuid", &entry.uuid); AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created); AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source); NormalizeMetadata(&entry.metadata); @@ -134,6 +135,8 @@ namespace ServerRuntime { OrderedJson object = OrderedJson::object(); object["xuid"] = AccessStorageUtils::NormalizeXuid(entry.xuid); + if (!entry.uuid.empty()) + object["uuid"] = entry.uuid; object["name"] = entry.name; object["created"] = entry.metadata.created; object["source"] = entry.metadata.source; diff --git a/Minecraft.Server/Access/WhitelistManager.h b/Minecraft.Server/Access/WhitelistManager.h index 1c2c5a0b57..40139141d9 100644 --- a/Minecraft.Server/Access/WhitelistManager.h +++ b/Minecraft.Server/Access/WhitelistManager.h @@ -16,6 +16,7 @@ namespace ServerRuntime struct WhitelistedPlayerEntry { std::string xuid; + std::string uuid; std::string name; WhitelistMetadata metadata; }; diff --git a/Minecraft.World/AddPlayerPacket.cpp b/Minecraft.World/AddPlayerPacket.cpp index fbee1fc524..259fe758aa 100644 --- a/Minecraft.World/AddPlayerPacket.cpp +++ b/Minecraft.World/AddPlayerPacket.cpp @@ -55,6 +55,7 @@ AddPlayerPacket::AddPlayerPacket(shared_ptr player, PlayerUID xuid, Play this->xuid = xuid; this->OnlineXuid = OnlineXuid; + this->gameUuid = player->getGameUUID(); m_playerIndex = static_cast(player->getPlayerIndex()); m_skinId = player->getCustomSkin(); m_capeId = player->getCustomCape(); @@ -77,6 +78,8 @@ void AddPlayerPacket::read(DataInputStream *dis) //throws IOException carriedItem = dis->readShort(); xuid = dis->readPlayerUID(); OnlineXuid = dis->readPlayerUID(); + gameUuid.msb = dis->readLong(); + gameUuid.lsb = dis->readLong(); m_playerIndex = dis->readByte(); INT skinId = dis->readInt(); m_skinId = *(DWORD *)&skinId; @@ -102,6 +105,8 @@ void AddPlayerPacket::write(DataOutputStream *dos) //throws IOException dos->writeShort(carriedItem); dos->writePlayerUID(xuid); dos->writePlayerUID(OnlineXuid); + dos->writeLong(gameUuid.msb); + dos->writeLong(gameUuid.lsb); dos->writeByte(m_playerIndex); dos->writeInt(m_skinId); dos->writeInt(m_capeId); @@ -117,7 +122,7 @@ void AddPlayerPacket::handle(PacketListener *listener) int AddPlayerPacket::getEstimatedSize() { - int iSize= sizeof(int) + Player::MAX_NAME_LENGTH + sizeof(int) + sizeof(int) + sizeof(int) + sizeof(BYTE) + sizeof(BYTE) +sizeof(short) + sizeof(PlayerUID) + sizeof(PlayerUID) + sizeof(int) + sizeof(BYTE) + sizeof(unsigned int) + sizeof(byte); + int iSize= sizeof(int) + Player::MAX_NAME_LENGTH + sizeof(int) + sizeof(int) + sizeof(int) + sizeof(BYTE) + sizeof(BYTE) +sizeof(short) + sizeof(PlayerUID) + sizeof(PlayerUID) + sizeof(GameUUID) + sizeof(int) + sizeof(BYTE) + sizeof(unsigned int) + sizeof(byte); if( entityData != nullptr ) { diff --git a/Minecraft.World/AddPlayerPacket.h b/Minecraft.World/AddPlayerPacket.h index af90c97dfe..4bf44dee1b 100644 --- a/Minecraft.World/AddPlayerPacket.h +++ b/Minecraft.World/AddPlayerPacket.h @@ -3,6 +3,7 @@ using namespace std; #include "Packet.h" #include "SynchedEntityData.h" +#include "UUID.h" class Player; @@ -21,6 +22,7 @@ class AddPlayerPacket : public Packet, public enable_shared_from_thisreadByte(); m_offlineXuid = dis->readPlayerUID(); m_onlineXuid = dis->readPlayerUID(); + m_gameUuid.msb = dis->readLong(); + m_gameUuid.lsb = dis->readLong(); m_friendsOnlyUGC = dis->readBoolean(); m_ugcPlayersVersion = dis->readInt(); difficulty = dis->readByte(); @@ -150,6 +152,8 @@ void LoginPacket::write(DataOutputStream *dos) //throws IOException dos->writeByte(maxPlayers); dos->writePlayerUID(m_offlineXuid); dos->writePlayerUID(m_onlineXuid); + dos->writeLong(m_gameUuid.msb); + dos->writeLong(m_gameUuid.lsb); dos->writeBoolean(m_friendsOnlyUGC); dos->writeInt(m_ugcPlayersVersion); dos->writeByte(difficulty); diff --git a/Minecraft.World/LoginPacket.h b/Minecraft.World/LoginPacket.h index 02a62b60b9..18d4afa301 100644 --- a/Minecraft.World/LoginPacket.h +++ b/Minecraft.World/LoginPacket.h @@ -2,6 +2,7 @@ using namespace std; #include "Packet.h" +#include "UUID.h" class LevelType; class LoginPacket : public Packet, public enable_shared_from_this @@ -12,6 +13,7 @@ class LoginPacket : public Packet, public enable_shared_from_this int64_t seed; char dimension; PlayerUID m_offlineXuid, m_onlineXuid; // 4J Added + GameUUID m_gameUuid; char difficulty; // 4J Added bool m_friendsOnlyUGC; // 4J Added DWORD m_ugcPlayersVersion; // 4J Added diff --git a/Minecraft.World/Player.cpp b/Minecraft.World/Player.cpp index 00c7148e41..e0e72ff2fc 100644 --- a/Minecraft.World/Player.cpp +++ b/Minecraft.World/Player.cpp @@ -762,6 +762,8 @@ unsigned int Player::getSkinAnimOverrideBitmask(DWORD skinId) void Player::setXuid(PlayerUID xuid) { m_xuid = xuid; + if (m_gameUuid.isNil() && xuid != INVALID_XUID) + m_gameUuid = GameUUID::fromXuid(xuid); #ifdef _XBOX_ONE // 4J Stu - For XboxOne (and probably in the future all other platforms) we store a UUID for the player to use as the owner key for tamed animals // This should just be a string version of the xuid diff --git a/Minecraft.World/Player.h b/Minecraft.World/Player.h index 2e223a1e55..08e190bbc3 100644 --- a/Minecraft.World/Player.h +++ b/Minecraft.World/Player.h @@ -8,6 +8,7 @@ using namespace std; #include "PlayerEnderChestContainer.h" #include "CommandSender.h" #include "ScoreHolder.h" +#include "UUID.h" class AbstractContainerMenu; class Stats; @@ -417,7 +418,8 @@ class Player : public LivingEntity, public CommandSender, public ScoreHolder PlayerUID getXuid() { return m_xuid; } void setOnlineXuid(PlayerUID xuid) { m_OnlineXuid = xuid; } PlayerUID getOnlineXuid() { return m_OnlineXuid; } - + void setGameUUID(const GameUUID& uuid) { m_gameUuid = uuid; } + GameUUID getGameUUID() const { return m_gameUuid; } void setPlayerIndex(DWORD dwIndex) { m_playerIndex = dwIndex; } DWORD getPlayerIndex() { return m_playerIndex; } @@ -431,6 +433,7 @@ class Player : public LivingEntity, public CommandSender, public ScoreHolder private: PlayerUID m_xuid; PlayerUID m_OnlineXuid; + GameUUID m_gameUuid; protected: bool m_bShownOnMaps; diff --git a/Minecraft.World/UUID.cpp b/Minecraft.World/UUID.cpp index 45e1bf9b6c..381349cbe0 100644 --- a/Minecraft.World/UUID.cpp +++ b/Minecraft.World/UUID.cpp @@ -2,32 +2,37 @@ #include "UUID.h" #include "Random.h" #include +static void sha1_block(uint32_t h[5], const uint8_t block[64]) +{ + uint32_t w[80]; + for (int i = 0; i < 16; i++) + w[i] = (block[i * 4] << 24) | (block[i * 4 + 1] << 16) | (block[i * 4 + 2] << 8) | block[i * 4 + 3]; + for (int i = 16; i < 80; i++) { + uint32_t x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; + w[i] = (x << 1) | (x >> 31); + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4]; + for (int i = 0; i < 80; i++) { + uint32_t f, k; + if (i < 20) { f = (b & c) | (~b & d); k = 0x5A827999; } + else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; } + else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; } + else { f = b ^ c ^ d; k = 0xCA62C1D6; } + uint32_t tmp = ((a << 5) | (a >> 27)) + f + e + k + w[i]; + e = d; d = c; c = (b << 30) | (b >> 2); b = a; a = tmp; + } + h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; +} + static void sha1(const uint8_t* data, size_t len, uint8_t out[20]) { uint32_t h[5] = { 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0 }; uint64_t bitLen = len * 8; + size_t fullBlocks = len / 64; - for (size_t blk = 0; blk < fullBlocks; blk++) { - const uint8_t* p = data + blk * 64; - uint32_t w[80]; - for (int i = 0; i < 16; i++) - w[i] = (p[i * 4] << 24) | (p[i * 4 + 1] << 16) | (p[i * 4 + 2] << 8) | p[i * 4 + 3]; - for (int i = 16; i < 80; i++) { - uint32_t x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; - w[i] = (x << 1) | (x >> 31); - } - uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4]; - for (int i = 0; i < 80; i++) { - uint32_t f, k; - if (i < 20) { f = (b & c) | (~b & d); k = 0x5A827999; } - else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; } - else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; } - else { f = b ^ c ^ d; k = 0xCA62C1D6; } - uint32_t tmp = ((a << 5) | (a >> 27)) + f + e + k + w[i]; - e = d; d = c; c = (b << 30) | (b >> 2); b = a; a = tmp; - } - h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; - } + for (size_t blk = 0; blk < fullBlocks; blk++) + sha1_block(h, data + blk * 64); + uint8_t tail[128] = {}; size_t rem = len - fullBlocks * 64; if (rem) memcpy(tail, data + fullBlocks * 64, rem); @@ -36,27 +41,8 @@ static void sha1(const uint8_t* data, size_t len, uint8_t out[20]) for (int i = 0; i < 8; i++) tail[tailLen - 1 - i] = (uint8_t)(bitLen >> (i * 8)); - for (size_t off = 0; off < tailLen; off += 64) { - uint32_t w[80]; - for (int i = 0; i < 16; i++) - w[i] = (tail[off + i * 4] << 24) | (tail[off + i * 4 + 1] << 16) | - (tail[off + i * 4 + 2] << 8) | tail[off + i * 4 + 3]; - for (int i = 16; i < 80; i++) { - uint32_t x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; - w[i] = (x << 1) | (x >> 31); - } - uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4]; - for (int i = 0; i < 80; i++) { - uint32_t f, k; - if (i < 20) { f = (b & c) | (~b & d); k = 0x5A827999; } - else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; } - else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; } - else { f = b ^ c ^ d; k = 0xCA62C1D6; } - uint32_t tmp = ((a << 5) | (a >> 27)) + f + e + k + w[i]; - e = d; d = c; c = (b << 30) | (b >> 2); b = a; a = tmp; - } - h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; - } + for (size_t off = 0; off < tailLen; off += 64) + sha1_block(h, tail + off); for (int i = 0; i < 5; i++) { out[i * 4] = (uint8_t)(h[i] >> 24); From acef537a9482d7d7370eb5450f1404fe5537bf10 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sat, 4 Apr 2026 15:20:58 -0400 Subject: [PATCH 11/18] add: migration from XUID to UUID --- Minecraft.Server/Access/BanManager.cpp | 11 +++++ Minecraft.Server/Access/WhitelistManager.cpp | 11 +++++ Minecraft.World/DirectoryLevelStorage.cpp | 47 +++++++++++++++++++- Minecraft.World/DirectoryLevelStorage.h | 1 + 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/Minecraft.Server/Access/BanManager.cpp b/Minecraft.Server/Access/BanManager.cpp index 76f14b00ed..937a811322 100644 --- a/Minecraft.Server/Access/BanManager.cpp +++ b/Minecraft.Server/Access/BanManager.cpp @@ -8,6 +8,7 @@ #include "..\Common\StringUtils.h" #include "..\ServerLogger.h" #include "Common/vendor/nlohmann/json.hpp" +#include "..\..\Minecraft.World\UUID.h" #include #include @@ -121,9 +122,19 @@ namespace ServerRuntime { return false; } + bool dirty = false; + for (auto &entry : players) + { + if (!entry.uuid.empty() || entry.xuid.empty()) continue; + try { + uint64_t xuid = std::stoull(entry.xuid, nullptr, 0); + if (xuid) { entry.uuid = GameUUID::fromXuid(xuid).toString(); dirty = true; } + } catch (...) {} + } m_bannedPlayers.swap(players); m_bannedIps.swap(ips); + if (dirty) Save(); return true; } diff --git a/Minecraft.Server/Access/WhitelistManager.cpp b/Minecraft.Server/Access/WhitelistManager.cpp index ec0104b751..9b671445f5 100644 --- a/Minecraft.Server/Access/WhitelistManager.cpp +++ b/Minecraft.Server/Access/WhitelistManager.cpp @@ -7,6 +7,7 @@ #include "..\Common\StringUtils.h" #include "..\ServerLogger.h" #include "Common/vendor/nlohmann/json.hpp" +#include "..\..\Minecraft.World\UUID.h" #include @@ -44,8 +45,18 @@ namespace ServerRuntime { return false; } + bool dirty = false; + for (auto &entry : players) + { + if (!entry.uuid.empty() || entry.xuid.empty()) continue; + try { + uint64_t xuid = std::stoull(entry.xuid, nullptr, 0); + if (xuid) { entry.uuid = GameUUID::fromXuid(xuid).toString(); dirty = true; } + } catch (...) {} + } m_whitelistedPlayers.swap(players); + if (dirty) Save(); return true; } diff --git a/Minecraft.World/DirectoryLevelStorage.cpp b/Minecraft.World/DirectoryLevelStorage.cpp index c557e37af1..892ccae5b5 100644 --- a/Minecraft.World/DirectoryLevelStorage.cpp +++ b/Minecraft.World/DirectoryLevelStorage.cpp @@ -11,6 +11,8 @@ #include "LevelData.h" #include "DirectoryLevelStorage.h" #include "ConsoleSaveFileIO.h" +#include "UUID.h" +#include "StringHelpers.h" const wstring DirectoryLevelStorage::sc_szPlayerDir(L"players/"); @@ -168,6 +170,8 @@ DirectoryLevelStorage::DirectoryLevelStorage(ConsoleSaveFile *saveFile, const Fi #ifdef _LARGE_WORLDS m_usedMappings = byteArray(MAXIMUM_MAP_SAVE_DATA/8); #endif + + migratePlayerXuidsToUuids(); } DirectoryLevelStorage::~DirectoryLevelStorage() @@ -398,7 +402,7 @@ void DirectoryLevelStorage::save(shared_ptr player) #elif defined(_DURANGO) ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + player->getXuid().toString() + L".dat" ); #else - const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + std::to_wstring( player->getXuid() ) + L".dat" ); + const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + player->getGameUUID().toWString() + L".dat" ); #endif // If saves are disabled (e.g. because we are writing the save buffer to disk) then cache this player data if(StorageManager.GetSaveDisabled()) @@ -447,7 +451,7 @@ CompoundTag *DirectoryLevelStorage::loadPlayerDataTag(PlayerUID xuid) #elif defined(_DURANGO) ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + xuid.toString() + L".dat" ); #else - const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + std::to_wstring( xuid ) + L".dat" ); + const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + GameUUID::fromXuid(xuid).toWString() + L".dat" ); #endif const auto it = m_cachedSaveData.find(realFile.getName()); if(it != m_cachedSaveData.end() ) @@ -818,3 +822,42 @@ void DirectoryLevelStorage::saveAllCachedData() } m_mapFilesToDelete.clear(); } +void DirectoryLevelStorage::migratePlayerXuidsToUuids() +{ + if (!m_saveFile) return; + + const ConsoleSavePath marker(L"migration_v1.dat"); + if (m_saveFile->doesFileExist(marker)) return; + + if (vector *playerFiles = m_saveFile->getFilesWithPrefix(playerDir.getName())) + { + for (FileEntry *file : *playerFiles) + { + wstring stem = replaceAll(replaceAll(file->data.filename, playerDir.getName(), L""), L".dat", L""); + + if (stem.empty() || !std::all_of(stem.begin(), stem.end(), [](wchar_t c) { return c >= L'0' && c <= L'9'; })) continue; + + const PlayerUID xuid = _fromString(stem); + if (xuid == INVALID_XUID) continue; + + ConsoleSaveFileInputStream fis(m_saveFile, file); + CompoundTag *tag = NbtIo::readCompressed(&fis); + if (!tag) continue; + + const wstring uuidStr = GameUUID::fromXuid(xuid).toWString(); + tag->putString(L"UUID", uuidStr); + + ConsoleSaveFileOutputStream fos(m_saveFile, ConsoleSavePath(playerDir.getName() + uuidStr + L".dat")); + NbtIo::writeCompressed(tag, &fos); + delete tag; + + m_saveFile->deleteFile(file); + app.DebugPrintf("migrated player %ls -> %ls\n", stem.c_str(), uuidStr.c_str()); + } + delete playerFiles; + } + + DWORD written = 0; + const uint8_t ver = 1; + m_saveFile->writeFile(m_saveFile->createFile(marker), &ver, 1, &written); +} diff --git a/Minecraft.World/DirectoryLevelStorage.h b/Minecraft.World/DirectoryLevelStorage.h index 6d305ba3f4..b0edff1e8f 100644 --- a/Minecraft.World/DirectoryLevelStorage.h +++ b/Minecraft.World/DirectoryLevelStorage.h @@ -139,6 +139,7 @@ class DirectoryLevelStorage : public LevelStorage, public PlayerIO static wstring getPlayerDir() { return sc_szPlayerDir; } private: + void migratePlayerXuidsToUuids(); void dontSaveMapMappingForPlayer(PlayerUID xuid); void deleteMapFilesForPlayer(PlayerUID xuid); }; From 14afabda0eff279a737b32302df320ddeba7ac4f Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sat, 4 Apr 2026 15:35:40 -0400 Subject: [PATCH 12/18] fix: fixing an oopsie where password mode would briefly show the real text in the buffer before switching to asterisks when sent to exchange for token. (elyby) --- Minecraft.Client/Common/UI/UIScene_Keyboard.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp b/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp index e21cf85b5f..f57b21e13b 100644 --- a/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp +++ b/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp @@ -173,7 +173,7 @@ void UIScene_Keyboard::tick() // Sync our buffer from Flash so we pick up changes made via controller/on-screen buttons. // Without this, switching between controller and keyboard would use stale text. // In PC mode we own the buffer — skip sync to preserve cursor position. - if (!m_bPCMode) + if (!m_bPCMode && m_eKeyboardMode != C_4JInput::EKeyboardMode_Password) { const wchar_t* flashText = m_KeyboardTextInput.getLabel(); if (flashText) @@ -417,8 +417,12 @@ void UIScene_Keyboard::KeyboardDonePressed() // Use getLabel() here — this is a timer callback (not an Iggy callback) so it's safe. // getLabel() reflects both physical keyboard input (pushed via setLabel) and // on-screen button input (set directly by Flash ActionScript). - const wchar_t* finalText = m_KeyboardTextInput.getLabel(); - app.DebugPrintf("UI Keyboard - DONE - [%ls]\n", finalText); + // in password mode the label is masked with asterisks, so use the real buffer instead + const wchar_t* finalText = (m_eKeyboardMode == C_4JInput::EKeyboardMode_Password) + ? m_win64TextBuffer.c_str() + : m_KeyboardTextInput.getLabel(); + app.DebugPrintf("UI Keyboard - DONE - [%ls]\n", + (m_eKeyboardMode == C_4JInput::EKeyboardMode_Password) ? L"" : finalText); // Store the typed text so callbacks can retrieve it via Win64_GetKeyboardText() wcsncpy_s(g_Win64KeyboardResult, 256, finalText, _TRUNCATE); From f238e682b398b16d331ea2066fe4af4006b242ab Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sat, 4 Apr 2026 19:56:53 -0400 Subject: [PATCH 13/18] add: authentication failure handling and ingame error messages --- Minecraft.Client/ClientConnection.cpp | 6 ++++++ Minecraft.Client/Common/Consoles_App.cpp | 7 +++++++ Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp | 8 ++++++++ .../Common/UI/UIScene_ConnectingProgress.cpp | 6 ++++++ Minecraft.Client/MinecraftServer.cpp | 14 ++++++++++++++ Minecraft.Client/MinecraftServer.h | 1 + Minecraft.Client/PendingConnection.cpp | 12 +++++++----- .../Windows64Media/loc/stringsGeneric.xml | 4 ++++ Minecraft.Client/Windows64Media/strings.h | 1 + Minecraft.Server/ServerLogManager.cpp | 1 + Minecraft.Server/ServerProperties.cpp | 4 ++++ Minecraft.Server/ServerProperties.h | 2 +- Minecraft.World/DisconnectPacket.h | 1 + 13 files changed, 61 insertions(+), 6 deletions(-) diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index 5a33545b56..b584ffb5cf 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -4173,6 +4173,12 @@ void ClientConnection::handleAuth(const shared_ptr &packet) if (handshakeManager->isComplete()) { authComplete = true; + const wstring &authName = handshakeManager->finalUsername; + minecraft->user->name = authName; + extern char g_Win64Username[17]; + extern wchar_t g_Win64UsernameW[17]; + wcsncpy_s(g_Win64UsernameW, authName.c_str(), 16); + wcstombs_s(nullptr, g_Win64Username, g_Win64UsernameW, 16); } else if (handshakeManager->isFailed()) { diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index 0a2fd159a4..39c2807e8a 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -4589,6 +4589,9 @@ int CMinecraftApp::SignoutExitWorldThreadProc( void* lpParameter ) case DisconnectPacket::eDisconnect_OutdatedClient: exitReasonStringId = IDS_DISCONNECTED_CLIENT_OLD; break; + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + break; default: exitReasonStringId = IDS_DISCONNECTED; } @@ -4649,6 +4652,10 @@ int CMinecraftApp::SignoutExitWorldThreadProc( void* lpParameter ) break; case DisconnectPacket::eDisconnect_OutdatedClient: exitReasonStringId = IDS_DISCONNECTED_CLIENT_OLD; + break; + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + break; default: exitReasonStringId = IDS_DISCONNECTED; } diff --git a/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp index e88ed08cdb..036e5bec2a 100644 --- a/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp @@ -508,6 +508,10 @@ void IUIScene_PauseMenu::_ExitWorld(LPVOID lpParameter) exitReasonTitleId = IDS_CONNECTION_FAILED; break; #endif + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + exitReasonTitleId = IDS_CONNECTION_FAILED; + break; default: exitReasonStringId = IDS_CONNECTION_LOST_SERVER; } @@ -609,6 +613,10 @@ void IUIScene_PauseMenu::_ExitWorld(LPVOID lpParameter) exitReasonTitleId = IDS_CONNECTION_FAILED; break; #endif + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + exitReasonTitleId = IDS_CONNECTION_FAILED; + break; default: exitReasonStringId = IDS_DISCONNECTED; } diff --git a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp index 40557cd5c2..ae75ec03b2 100644 --- a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp +++ b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp @@ -133,6 +133,9 @@ void UIScene_ConnectingProgress::tick() case DisconnectPacket::eDisconnect_Banned: exitReasonStringId = IDS_DISCONNECTED_KICKED; break; + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + break; default: exitReasonStringId = IDS_CONNECTION_LOST_SERVER; break; @@ -277,6 +280,9 @@ void UIScene_ConnectingProgress::handleTimerComplete(int id) exitReasonStringId = IDS_DISCONNECTED_NAT_TYPE_MISMATCH; break; #endif + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + break; default: exitReasonStringId = IDS_CONNECTION_LOST_SERVER; break; diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 27ee68b65f..8304007f3b 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -59,6 +59,7 @@ #include "PlayerChunkMap.h" #include "Common\Telemetry\TelemetryManager.h" #include "PlayerConnection.h" +#include "AuthScreen.h" #ifdef _XBOX_ONE #include "Durango\Network\NetworkPlayerDurango.h" #endif @@ -648,6 +649,19 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW //motd = settings->getString(L"motd", L"A Minecraft Server"); //motd.replace('�', '$'); + if (ShouldUseDedicatedServerProperties()) + { + wstring am = GetDedicatedServerString(settings, L"auth-mode", L"session"); + authMode = (am == L"offline") ? "offline" : "session"; + } + else + { + int idx = AuthProfileManager::getSelectedIndex(); + const auto &profiles = AuthProfileManager::getProfiles(); + authMode = (idx >= 0 && idx < static_cast(profiles.size()) && + profiles[idx].type != AuthProfile::OFFLINE) ? "session" : "offline"; + } + setAnimals(GetDedicatedServerBool(settings, L"spawn-animals", true)); setNpcsEnabled(GetDedicatedServerBool(settings, L"spawn-npcs", true)); setPvpAllowed(app.GetGameHostOption( eGameHostOption_PvP )>0?true:false); diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index 1ed5db9d7c..4db4e10162 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -113,6 +113,7 @@ class MinecraftServer : public ConsoleInputSource CRITICAL_SECTION m_consoleInputCS; public: bool onlineMode; + std::string authMode; bool animals; bool npcs; bool pvp; diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index 88fdb96960..6ff5cac1c8 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -125,7 +125,8 @@ void PendingConnection::handlePreLogin(shared_ptr packet) return; } // printf("Server: handlePreLogin\n"); - name = packet->loginKey; // 4J Stu - Change from the login packet as we know better on client end during the pre-login packet + if (!authComplete) + name = packet->loginKey; sendPreLoginResponse(); } @@ -411,9 +412,10 @@ bool PendingConnection::isDisconnected() void PendingConnection::initAuth() { handshakeManager = new HandshakeManager(true); - handshakeManager->registerModule(new SessionAuthModule()); - handshakeManager->registerModule(new KeypairOfflineAuthModule()); - handshakeManager->registerModule(new OfflineAuthModule()); + if (server->authMode == "session") + handshakeManager->registerModule(new SessionAuthModule()); + else + handshakeManager->registerModule(new OfflineAuthModule()); } void PendingConnection::handleAuth(const shared_ptr &packet) @@ -433,6 +435,6 @@ void PendingConnection::handleAuth(const shared_ptr &packet) } else if (handshakeManager->isFailed()) { - disconnect(DisconnectPacket::eDisconnect_Closed); + disconnect(DisconnectPacket::eDisconnect_AuthFailed); } } diff --git a/Minecraft.Client/Windows64Media/loc/stringsGeneric.xml b/Minecraft.Client/Windows64Media/loc/stringsGeneric.xml index b5e58a9b6c..2b41f1daf7 100644 --- a/Minecraft.Client/Windows64Media/loc/stringsGeneric.xml +++ b/Minecraft.Client/Windows64Media/loc/stringsGeneric.xml @@ -5867,6 +5867,10 @@ Press{*CONTROLLER_VK_B*} if you already know about Fireworks. You cannot join this game as the player you are trying to join is running a newer version of the game. + + Authentication required. This server requires you to be signed in. + + New World diff --git a/Minecraft.Client/Windows64Media/strings.h b/Minecraft.Client/Windows64Media/strings.h index 19a50eeebd..b7812b0fe3 100644 --- a/Minecraft.Client/Windows64Media/strings.h +++ b/Minecraft.Client/Windows64Media/strings.h @@ -2290,3 +2290,4 @@ #define IDS_RICHPRESENCESTATE_BREWING 2284 #define IDS_RICHPRESENCESTATE_ANVIL 2285 #define IDS_RICHPRESENCESTATE_TRADING 2286 +#define IDS_DISCONNECTED_AUTH_FAILED 2287 diff --git a/Minecraft.Server/ServerLogManager.cpp b/Minecraft.Server/ServerLogManager.cpp index 84805f7e4e..53f83f7404 100644 --- a/Minecraft.Server/ServerLogManager.cpp +++ b/Minecraft.Server/ServerLogManager.cpp @@ -196,6 +196,7 @@ namespace ServerRuntime case DisconnectPacket::eDisconnect_Banned: return "banned"; case DisconnectPacket::eDisconnect_NotFriendsWithHost: return "not-friends-with-host"; case DisconnectPacket::eDisconnect_NATMismatch: return "nat-mismatch"; + case DisconnectPacket::eDisconnect_AuthFailed: return "auth-failed"; default: return "unknown"; } } diff --git a/Minecraft.Server/ServerProperties.cpp b/Minecraft.Server/ServerProperties.cpp index d6ba64e7e2..da50955705 100644 --- a/Minecraft.Server/ServerProperties.cpp +++ b/Minecraft.Server/ServerProperties.cpp @@ -40,6 +40,7 @@ static const ServerPropertyDefault kServerPropertyDefaults[] = { { "allow-flight", "true" }, { "allow-nether", "true" }, + { "auth-mode", "session" }, { "autosave-interval", "60" }, { "bedrock-fog", "true" }, { "bonus-chest", "false" }, @@ -864,6 +865,9 @@ ServerPropertiesConfig LoadServerPropertiesConfig() config.maxBuildHeight = ReadNormalizedIntProperty(&merged, "max-build-height", 256, 64, 256, &shouldWrite); config.motd = ReadNormalizedStringProperty(&merged, "motd", "A Minecraft Server", 255, &shouldWrite); + config.authMode = ReadNormalizedStringProperty(&merged, "auth-mode", "session", 16, &shouldWrite); + if (config.authMode != "session" && config.authMode != "offline") + config.authMode = "session"; if (shouldWrite) { diff --git a/Minecraft.Server/ServerProperties.h b/Minecraft.Server/ServerProperties.h index 3bb5aca802..03073ab922 100644 --- a/Minecraft.Server/ServerProperties.h +++ b/Minecraft.Server/ServerProperties.h @@ -73,7 +73,7 @@ namespace ServerRuntime bool doTileDrops; bool naturalRegeneration; bool doDaylightCycle; - + std::string authMode; /** other MinecraftServer runtime settings */ int maxBuildHeight; std::string levelType; diff --git a/Minecraft.World/DisconnectPacket.h b/Minecraft.World/DisconnectPacket.h index 3c96a429b1..22036a59a8 100644 --- a/Minecraft.World/DisconnectPacket.h +++ b/Minecraft.World/DisconnectPacket.h @@ -44,6 +44,7 @@ class DisconnectPacket : public Packet, public enable_shared_from_this Date: Sat, 4 Apr 2026 20:49:05 -0400 Subject: [PATCH 14/18] re: replaced raw pointers in favor of unique_ptr in [HandshakeManager] and related classes. --- Minecraft.Client/AuthScreen.cpp | 3 ++- Minecraft.Client/ClientConnection.cpp | 6 +++--- Minecraft.Client/PendingConnection.cpp | 4 ++-- Minecraft.World/HandshakeManager.cpp | 15 +++++---------- Minecraft.World/HandshakeManager.h | 6 +++--- Minecraft.World/UUID.cpp | 12 +++++------- 6 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Minecraft.Client/AuthScreen.cpp b/Minecraft.Client/AuthScreen.cpp index 9376b9efb9..c640f0da99 100644 --- a/Minecraft.Client/AuthScreen.cpp +++ b/Minecraft.Client/AuthScreen.cpp @@ -6,6 +6,7 @@ #include "..\Minecraft.World\HttpClient.h" #include "..\Minecraft.World\StringHelpers.h" #include "Common/vendor/nlohmann/json.hpp" +#include #include #include @@ -351,7 +352,7 @@ void AuthFlow::microsoftFlowThread() for (int ms = 0; ms < interval * 1000; ms += 250) { if (cancelRequested) return; - Sleep(250); + std::this_thread::sleep_for(std::chrono::milliseconds(250)); } auto pollResp = HttpClient::post( diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index b584ffb5cf..c85e61b2c1 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -4141,9 +4141,9 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr< void ClientConnection::beginAuth() { handshakeManager = new HandshakeManager(false); - handshakeManager->registerModule(new SessionAuthModule()); - handshakeManager->registerModule(new KeypairOfflineAuthModule()); - handshakeManager->registerModule(new OfflineAuthModule()); + handshakeManager->registerModule(std::make_unique()); + handshakeManager->registerModule(std::make_unique()); + handshakeManager->registerModule(std::make_unique()); const auto &profiles = AuthProfileManager::getProfiles(); int idx = AuthProfileManager::getSelectedIndex(); diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index 6ff5cac1c8..090decd389 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -413,9 +413,9 @@ void PendingConnection::initAuth() { handshakeManager = new HandshakeManager(true); if (server->authMode == "session") - handshakeManager->registerModule(new SessionAuthModule()); + handshakeManager->registerModule(std::make_unique()); else - handshakeManager->registerModule(new OfflineAuthModule()); + handshakeManager->registerModule(std::make_unique()); } void PendingConnection::handleAuth(const shared_ptr &packet) diff --git a/Minecraft.World/HandshakeManager.cpp b/Minecraft.World/HandshakeManager.cpp index 969c1a53c8..931a011673 100644 --- a/Minecraft.World/HandshakeManager.cpp +++ b/Minecraft.World/HandshakeManager.cpp @@ -19,15 +19,10 @@ HandshakeManager::HandshakeManager(bool isServer) { } -HandshakeManager::~HandshakeManager() +void HandshakeManager::registerModule(unique_ptr module) { - for (auto &[name, module] : modules) - delete module; -} - -void HandshakeManager::registerModule(AuthModule *module) -{ - modules[module->schemeName()] = module; + wstring name = module->schemeName(); + modules[std::move(name)] = std::move(module); } void HandshakeManager::setCredentials(const wstring &token, const wstring &uid, const wstring &username, const wstring &variation) @@ -70,7 +65,7 @@ shared_ptr HandshakeManager::handleServer(const shared_ptrsecond; + activeModule = modules.begin()->second.get(); state = HandshakeState::SCHEME_DECLARED; return makePacket(AuthStage::DECLARE_SCHEME, { {L"version", PROTOCOL_VERSION}, @@ -145,7 +140,7 @@ shared_ptr HandshakeManager::handleClient(const shared_ptrsecond; + activeModule = it->second.get(); auto variations = activeModule->supportedVariations(); if (!preferredVariation.empty() && diff --git a/Minecraft.World/HandshakeManager.h b/Minecraft.World/HandshakeManager.h index 360f53eae8..716f40a3ad 100644 --- a/Minecraft.World/HandshakeManager.h +++ b/Minecraft.World/HandshakeManager.h @@ -28,7 +28,7 @@ class HandshakeManager private: bool isServer; HandshakeState state; - unordered_map modules; + unordered_map> modules; AuthModule *activeModule; wstring activeVariation; wstring protocolVersion; @@ -45,9 +45,9 @@ class HandshakeManager wstring finalUsername; HandshakeManager(bool isServer); - ~HandshakeManager(); + ~HandshakeManager() = default; - void registerModule(AuthModule *module); + void registerModule(unique_ptr module); void setCredentials(const wstring &token, const wstring &uid, const wstring &username, const wstring &variation = L""); shared_ptr handlePacket(const shared_ptr &packet); shared_ptr createInitialPacket(); diff --git a/Minecraft.World/UUID.cpp b/Minecraft.World/UUID.cpp index 381349cbe0..4ca8173a06 100644 --- a/Minecraft.World/UUID.cpp +++ b/Minecraft.World/UUID.cpp @@ -2,6 +2,7 @@ #include "UUID.h" #include "Random.h" #include +#include static void sha1_block(uint32_t h[5], const uint8_t block[64]) { uint32_t w[80]; @@ -119,15 +120,12 @@ GameUUID GameUUID::v4(uint64_t high, uint64_t low) GameUUID GameUUID::v5(const GameUUID& ns, const std::string& name) { - uint8_t input[256]; - ns.toBytes(input); - size_t total = 16 + name.size(); - // names over 240 chars would be insane but just in case - if (name.size() <= sizeof(input) - 16) - memcpy(input + 16, name.data(), name.size()); + std::vector input(16 + name.size()); + ns.toBytes(input.data()); + memcpy(input.data() + 16, name.data(), name.size()); uint8_t hash[20]; - sha1(input, total, hash); + sha1(input.data(), input.size(), hash); GameUUID u = fromBytes(hash); u.msb = (u.msb & ~0xF000ULL) | 0x5000ULL; u.lsb = (u.lsb & ~0xC000000000000000ULL) | 0x8000000000000000ULL; From 7b6342591c323929160af45a5353a596679060d0 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sat, 4 Apr 2026 22:35:07 -0400 Subject: [PATCH 15/18] minor change: update authentication messagebox options to include 'Remove' and handle profile removal --- Minecraft.Client/Common/UI/UIScene_MainMenu.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index 139f6b7789..3d1febc33d 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -524,7 +524,7 @@ void UIScene_MainMenu::ShowAuthMenu(int iPad, void *pClass) s_authPad = iPad; s_authParam = pClass; - static const wchar_t *authOptions[] = { L"Next", L"Use", L"Add", L"Back" }; + static const wchar_t *authOptions[] = { L"Next", L"Use", L"Add", L"Remove" }; ShowAuthMessageBox(iPad, L"Authentication", BuildAuthProfileText(), authOptions, 4, &UIScene_MainMenu::AuthMenuReturned, pClass, true); } @@ -565,6 +565,16 @@ int UIScene_MainMenu::AuthMenuReturned(LPVOID lpParam, int iPad, const C4JStorag ShowAuthAddMenu(iPad, lpParam); break; } + case C4JStorage::EMessage_ResultFourthOption: + { + if (!profiles.empty()) + { + AuthProfileManager::removeSelectedProfile(); + if (auto *scene = ui.FindScene(eUIScene_MessageBox)) + static_cast(scene)->updateContent(BuildAuthProfileText()); + } + return 0; + } default: { ui.NavigateBack(iPad); From a257d7899e471756ed91626b6fdd495b9e0d7185 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Sun, 5 Apr 2026 18:04:53 -0400 Subject: [PATCH 16/18] patch: refactor auth handling. seperate each auth method into their own module. --- Minecraft.Client/AuthScreen.cpp | 3 +- Minecraft.Client/ClientConnection.cpp | 3 +- .../Common/UI/UIScene_MainMenu.cpp | 12 +- Minecraft.Client/PendingConnection.cpp | 3 +- Minecraft.World/AuthModule.cpp | 110 ------------------ Minecraft.World/AuthModule.h | 41 ------- Minecraft.World/HandshakeManager.cpp | 4 +- Minecraft.World/OfflineAuthModule.cpp | 36 ++++++ Minecraft.World/OfflineAuthModule.h | 22 ++++ Minecraft.World/SessionAuthModule.cpp | 78 +++++++++++++ Minecraft.World/SessionAuthModule.h | 27 +++++ Minecraft.World/cmake/sources/Common.cmake | 4 + 12 files changed, 186 insertions(+), 157 deletions(-) create mode 100644 Minecraft.World/OfflineAuthModule.cpp create mode 100644 Minecraft.World/OfflineAuthModule.h create mode 100644 Minecraft.World/SessionAuthModule.cpp create mode 100644 Minecraft.World/SessionAuthModule.h diff --git a/Minecraft.Client/AuthScreen.cpp b/Minecraft.Client/AuthScreen.cpp index c640f0da99..0f0d33307a 100644 --- a/Minecraft.Client/AuthScreen.cpp +++ b/Minecraft.Client/AuthScreen.cpp @@ -346,6 +346,7 @@ void AuthFlow::microsoftFlowThread() state = AuthFlowState::POLLING; string msAccessToken; string msRefreshToken; + const string pollBody = "client_id=" + string(MS_CLIENT_ID) + "&device_code=" + deviceCode + "&grant_type=urn:ietf:params:oauth:grant-type:device_code"; for (int attempt = 0; attempt < 180; attempt++) { @@ -357,7 +358,7 @@ void AuthFlow::microsoftFlowThread() auto pollResp = HttpClient::post( "https://login.live.com/oauth20_token.srf", - "client_id=" + string(MS_CLIENT_ID) + "&device_code=" + deviceCode + "&grant_type=urn:ietf:params:oauth:grant-type:device_code", + pollBody, "application/x-www-form-urlencoded" ); diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index c85e61b2c1..2765e9214f 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -55,7 +55,8 @@ #endif #include "DLCTexturePack.h" #include "..\Minecraft.World\HandshakeManager.h" -#include "..\Minecraft.World\AuthModule.h" +#include "..\Minecraft.World\SessionAuthModule.h" +#include "..\Minecraft.World\OfflineAuthModule.h" #include "AuthScreen.h" #ifdef _WINDOWS64 diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index 139f6b7789..3d1febc33d 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -524,7 +524,7 @@ void UIScene_MainMenu::ShowAuthMenu(int iPad, void *pClass) s_authPad = iPad; s_authParam = pClass; - static const wchar_t *authOptions[] = { L"Next", L"Use", L"Add", L"Back" }; + static const wchar_t *authOptions[] = { L"Next", L"Use", L"Add", L"Remove" }; ShowAuthMessageBox(iPad, L"Authentication", BuildAuthProfileText(), authOptions, 4, &UIScene_MainMenu::AuthMenuReturned, pClass, true); } @@ -565,6 +565,16 @@ int UIScene_MainMenu::AuthMenuReturned(LPVOID lpParam, int iPad, const C4JStorag ShowAuthAddMenu(iPad, lpParam); break; } + case C4JStorage::EMessage_ResultFourthOption: + { + if (!profiles.empty()) + { + AuthProfileManager::removeSelectedProfile(); + if (auto *scene = ui.FindScene(eUIScene_MessageBox)) + static_cast(scene)->updateContent(BuildAuthProfileText()); + } + return 0; + } default: { ui.NavigateBack(iPad); diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index 090decd389..a85e3378b8 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -15,7 +15,8 @@ #include "..\Minecraft.World\SharedConstants.h" #include "Settings.h" #include "..\Minecraft.World\HandshakeManager.h" -#include "..\Minecraft.World\AuthModule.h" +#include "..\Minecraft.World\SessionAuthModule.h" +#include "..\Minecraft.World\OfflineAuthModule.h" #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "..\Minecraft.Server\ServerLogManager.h" #include "..\Minecraft.Server\Access\Access.h" diff --git a/Minecraft.World/AuthModule.cpp b/Minecraft.World/AuthModule.cpp index 45b11734d1..091a14db52 100644 --- a/Minecraft.World/AuthModule.cpp +++ b/Minecraft.World/AuthModule.cpp @@ -1,18 +1,5 @@ #include "stdafx.h" #include "AuthModule.h" -#include "HttpClient.h" -#include "StringHelpers.h" -#include "Common/vendor/nlohmann/json.hpp" -#include - -static wstring generateServerId() -{ - static constexpr wchar_t hex[] = L"0123456789abcdef"; - static std::mt19937 rng(std::random_device{}()); - wstring id(16, L'0'); - for (auto &c : id) c = hex[rng() & 0xF]; - return id; -} bool AuthModule::validate(const wstring &uid, const wstring &username) { @@ -28,100 +15,3 @@ bool AuthModule::extractIdentity(const vector> &fields, w } return validate(outUid, outUsername); } - -SessionAuthModule::SessionAuthModule() -{ - endpoints[L"mojang"] = {L"https://authserver.mojang.com", L"https://sessionserver.mojang.com"}; - endpoints[L"elyby"] = {L"https://authserver.ely.by", L"https://authserver.ely.by"}; -} - -const wchar_t *SessionAuthModule::schemeName() { return L"mcconsoles:session"; } - -vector SessionAuthModule::supportedVariations() -{ - return {L"mojang", L"elyby"}; -} - -vector> SessionAuthModule::getSettings(const wstring &variation) -{ - auto it = endpoints.find(variation); - if (it == endpoints.end()) return {}; - - activeSessionEndpoint = it->second.sessionEndpoint; - activeServerId = generateServerId(); - - return { - {L"authEndpoint", it->second.authEndpoint}, - {L"sessionEndpoint", it->second.sessionEndpoint}, - {L"serverId", activeServerId} - }; -} - -bool SessionAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) -{ - wstring username; - for (const auto &[k, v] : fields) - { - if (k == L"username") username = v; - } - - if (username.empty() || activeServerId.empty() || activeSessionEndpoint.empty()) - return false; - - string url = narrowStr(activeSessionEndpoint) - + "/session/minecraft/hasJoined?username=" + narrowStr(username) - + "&serverId=" + narrowStr(activeServerId); - - auto response = HttpClient::get(url); - if (response.statusCode != 200) - return false; - - auto json = nlohmann::json::parse(response.body, nullptr, false); - if (json.is_discarded()) - return false; - - string id = json.value("id", ""); - string name = json.value("name", ""); - - if (id.empty() || name.empty()) - return false; - - outUid = convStringToWstring(id); - outUsername = convStringToWstring(name); - - return validate(outUid, outUsername); -} - -const wchar_t *KeypairOfflineAuthModule::schemeName() { return L"mcconsoles:keypair_offline"; } - -vector KeypairOfflineAuthModule::supportedVariations() -{ - return {L"rsa2048", L"ed25519"}; -} - -vector> KeypairOfflineAuthModule::getSettings(const wstring &variation) -{ - return {{L"key_type", variation}}; -} - -bool KeypairOfflineAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) -{ - return extractIdentity(fields, outUid, outUsername); -} - -const wchar_t *OfflineAuthModule::schemeName() { return L"mcconsoles:offline"; } - -vector OfflineAuthModule::supportedVariations() -{ - return {}; -} - -vector> OfflineAuthModule::getSettings(const wstring &variation) -{ - return {}; -} - -bool OfflineAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) -{ - return extractIdentity(fields, outUid, outUsername); -} diff --git a/Minecraft.World/AuthModule.h b/Minecraft.World/AuthModule.h index e5f4a6fa4e..ae5875b428 100644 --- a/Minecraft.World/AuthModule.h +++ b/Minecraft.World/AuthModule.h @@ -4,7 +4,6 @@ using namespace std; #include #include #include -#include inline string narrowStr(const wstring &w) { @@ -26,43 +25,3 @@ class AuthModule protected: bool extractIdentity(const vector> &fields, wstring &outUid, wstring &outUsername); }; - -class SessionAuthModule : public AuthModule -{ -public: - struct EndpointPair { - wstring authEndpoint; - wstring sessionEndpoint; - }; - -private: - unordered_map endpoints; - wstring activeSessionEndpoint; - wstring activeServerId; - -public: - SessionAuthModule(); - - const wchar_t *schemeName() override; - vector supportedVariations() override; - vector> getSettings(const wstring &variation) override; - bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; -}; - -class KeypairOfflineAuthModule : public AuthModule -{ -public: - const wchar_t *schemeName() override; - vector supportedVariations() override; - vector> getSettings(const wstring &variation) override; - bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; -}; - -class OfflineAuthModule : public AuthModule -{ -public: - const wchar_t *schemeName() override; - vector supportedVariations() override; - vector> getSettings(const wstring &variation) override; - bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; -}; diff --git a/Minecraft.World/HandshakeManager.cpp b/Minecraft.World/HandshakeManager.cpp index 931a011673..96d83b26b5 100644 --- a/Minecraft.World/HandshakeManager.cpp +++ b/Minecraft.World/HandshakeManager.cpp @@ -35,8 +35,8 @@ void HandshakeManager::setCredentials(const wstring &token, const wstring &uid, vector> HandshakeManager::drainPendingPackets() { - vector> out; - out.swap(pendingPackets); + auto out = std::move(pendingPackets); + pendingPackets.clear(); return out; } diff --git a/Minecraft.World/OfflineAuthModule.cpp b/Minecraft.World/OfflineAuthModule.cpp new file mode 100644 index 0000000000..5369ceacc6 --- /dev/null +++ b/Minecraft.World/OfflineAuthModule.cpp @@ -0,0 +1,36 @@ +#include "stdafx.h" +#include "OfflineAuthModule.h" + +const wchar_t *KeypairOfflineAuthModule::schemeName() { return L"mcconsoles:keypair_offline"; } + +vector KeypairOfflineAuthModule::supportedVariations() +{ + return {L"rsa2048", L"ed25519"}; +} + +vector> KeypairOfflineAuthModule::getSettings(const wstring &variation) +{ + return {{L"key_type", variation}}; +} + +bool KeypairOfflineAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) +{ + return extractIdentity(fields, outUid, outUsername); +} + +const wchar_t *OfflineAuthModule::schemeName() { return L"mcconsoles:offline"; } + +vector OfflineAuthModule::supportedVariations() +{ + return {}; +} + +vector> OfflineAuthModule::getSettings(const wstring &variation) +{ + return {}; +} + +bool OfflineAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) +{ + return extractIdentity(fields, outUid, outUsername); +} diff --git a/Minecraft.World/OfflineAuthModule.h b/Minecraft.World/OfflineAuthModule.h new file mode 100644 index 0000000000..b2b4454d9c --- /dev/null +++ b/Minecraft.World/OfflineAuthModule.h @@ -0,0 +1,22 @@ +#pragma once +using namespace std; + +#include "AuthModule.h" + +class KeypairOfflineAuthModule : public AuthModule +{ +public: + const wchar_t *schemeName() override; + vector supportedVariations() override; + vector> getSettings(const wstring &variation) override; + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; +}; + +class OfflineAuthModule : public AuthModule +{ +public: + const wchar_t *schemeName() override; + vector supportedVariations() override; + vector> getSettings(const wstring &variation) override; + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; +}; diff --git a/Minecraft.World/SessionAuthModule.cpp b/Minecraft.World/SessionAuthModule.cpp new file mode 100644 index 0000000000..50844b39b6 --- /dev/null +++ b/Minecraft.World/SessionAuthModule.cpp @@ -0,0 +1,78 @@ +#include "stdafx.h" +#include "SessionAuthModule.h" +#include "HttpClient.h" +#include "StringHelpers.h" +#include "Common/vendor/nlohmann/json.hpp" +#include + +static wstring generateServerId() +{ + static constexpr wchar_t hex[] = L"0123456789abcdef"; + static std::mt19937 rng(std::random_device{}()); + wstring id(16, L'0'); + for (auto &c : id) c = hex[rng() & 0xF]; + return id; +} + +SessionAuthModule::SessionAuthModule() +{ + endpoints[L"mojang"] = {L"https://authserver.mojang.com", L"https://sessionserver.mojang.com"}; + endpoints[L"elyby"] = {L"https://authserver.ely.by", L"https://authserver.ely.by"}; +} + +const wchar_t *SessionAuthModule::schemeName() { return L"mcconsoles:session"; } + +vector SessionAuthModule::supportedVariations() +{ + return {L"mojang", L"elyby"}; +} + +vector> SessionAuthModule::getSettings(const wstring &variation) +{ + auto it = endpoints.find(variation); + if (it == endpoints.end()) return {}; + + activeSessionEndpoint = it->second.sessionEndpoint; + activeServerId = generateServerId(); + + return { + {L"authEndpoint", it->second.authEndpoint}, + {L"sessionEndpoint", it->second.sessionEndpoint}, + {L"serverId", activeServerId} + }; +} + +bool SessionAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) +{ + wstring username; + for (const auto &[k, v] : fields) + { + if (k == L"username") username = v; + } + + if (username.empty() || activeServerId.empty() || activeSessionEndpoint.empty()) + return false; + + string url = narrowStr(activeSessionEndpoint) + + "/session/minecraft/hasJoined?username=" + narrowStr(username) + + "&serverId=" + narrowStr(activeServerId); + + auto response = HttpClient::get(url); + if (response.statusCode != 200) + return false; + + auto json = nlohmann::json::parse(response.body, nullptr, false); + if (json.is_discarded()) + return false; + + string id = json.value("id", ""); + string name = json.value("name", ""); + + if (id.empty() || name.empty()) + return false; + + outUid = convStringToWstring(id); + outUsername = convStringToWstring(name); + + return validate(outUid, outUsername); +} diff --git a/Minecraft.World/SessionAuthModule.h b/Minecraft.World/SessionAuthModule.h new file mode 100644 index 0000000000..ecb60c06fa --- /dev/null +++ b/Minecraft.World/SessionAuthModule.h @@ -0,0 +1,27 @@ +#pragma once +using namespace std; + +#include "AuthModule.h" +#include + +class SessionAuthModule : public AuthModule +{ +public: + struct EndpointPair { + wstring authEndpoint; + wstring sessionEndpoint; + }; + +private: + unordered_map endpoints; + wstring activeSessionEndpoint; + wstring activeServerId; + +public: + SessionAuthModule(); + + const wchar_t *schemeName() override; + vector supportedVariations() override; + vector> getSettings(const wstring &variation) override; + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; +}; diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index ae1fad9d0b..8bf351e962 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -261,6 +261,10 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/AnimatePacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/AuthModule.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AuthModule.h" + "${CMAKE_CURRENT_SOURCE_DIR}/SessionAuthModule.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/SessionAuthModule.h" + "${CMAKE_CURRENT_SOURCE_DIR}/OfflineAuthModule.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/OfflineAuthModule.h" "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.h" "${CMAKE_CURRENT_SOURCE_DIR}/UUID.cpp" From 179d21d7b6c9af7beec1a5c4da5526a113a6abcb Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Mon, 6 Apr 2026 02:33:39 -0400 Subject: [PATCH 17/18] major refactor: auth v3 --- Minecraft.Client/AuthScreen.cpp | 74 +++++++----- Minecraft.Client/AuthScreen.h | 13 ++- Minecraft.Client/CMakeLists.txt | 2 + Minecraft.Client/ClientConnection.cpp | 21 +++- .../Common/Network/GameNetworkManager.cpp | 29 +++++ .../Common/UI/UIScene_MainMenu.cpp | 48 +++++--- Minecraft.Client/Common/UI/UIScene_MainMenu.h | 4 +- Minecraft.Client/PendingConnection.cpp | 17 ++- .../Windows64/Windows64_Minecraft.cpp | 30 +++++ Minecraft.World/AuthModule.h | 18 +++ Minecraft.World/HandshakeManager.cpp | 61 ++++++++-- Minecraft.World/MapItem.cpp | 9 +- Minecraft.World/OfflineAuthModule.cpp | 36 ------ Minecraft.World/OfflineAuthModule.h | 22 ---- Minecraft.World/SessionAuthModule.cpp | 109 ++++++++++++++---- Minecraft.World/SessionAuthModule.h | 37 ++++-- Minecraft.World/cmake/sources/Common.cmake | 2 - yggdrasil.json | 8 ++ 18 files changed, 375 insertions(+), 165 deletions(-) delete mode 100644 Minecraft.World/OfflineAuthModule.cpp delete mode 100644 Minecraft.World/OfflineAuthModule.h create mode 100644 yggdrasil.json diff --git a/Minecraft.Client/AuthScreen.cpp b/Minecraft.Client/AuthScreen.cpp index 0f0d33307a..bdaa2b9b7b 100644 --- a/Minecraft.Client/AuthScreen.cpp +++ b/Minecraft.Client/AuthScreen.cpp @@ -3,6 +3,7 @@ #include "Minecraft.h" #include "User.h" #include "..\Minecraft.World\AuthModule.h" +#include "..\Minecraft.World\SessionAuthModule.h" #include "..\Minecraft.World\HttpClient.h" #include "..\Minecraft.World\StringHelpers.h" #include "Common/vendor/nlohmann/json.hpp" @@ -12,24 +13,33 @@ using json = nlohmann::json; static constexpr auto PROFILES_FILE = L"auth_profiles.dat"; -static constexpr auto MS_CLIENT_ID = "00000000441cc96b"; +static constexpr auto MS_CLIENT_ID = "00000000402b5328"; static json parseResponse(const HttpResponse &resp, int expectedStatus = 200); static bool msTokenExchange(const string &msAccessToken, string &mcToken, string &profId, string &profName); static bool msRefreshOAuth(const string &refreshToken, string &newAccessToken, string &newRefreshToken); -static bool elybyValidate(const string &accessToken, const string &clientToken); -static bool elybyRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken); +static bool yggdrasilValidate(const string &accessToken, const string &clientToken, const string &validateUrl); +static bool yggdrasilRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken, const string &refreshUrl); vector AuthProfileManager::profiles; int AuthProfileManager::selectedProfile = -1; +static constexpr uint32_t PROFILE_MAGIC = 0x4D435032; + void AuthProfileManager::load() { profiles.clear(); std::ifstream file(PROFILES_FILE, std::ios::binary); if (!file) return; + uint32_t header = 0; + file.read(reinterpret_cast(&header), sizeof(header)); + bool hasVariation = (header == PROFILE_MAGIC); + uint32_t count = 0; - file.read(reinterpret_cast(&count), sizeof(count)); + if (hasVariation) + file.read(reinterpret_cast(&count), sizeof(count)); + else + count = header; for (uint32_t i = 0; i < count && file.good(); i++) { @@ -52,6 +62,10 @@ void AuthProfileManager::load() p.username = readWstr(); p.token = readWstr(); p.clientToken = readWstr(); + if (hasVariation) + p.variation = readWstr(); + if (p.variation.empty() && p.type == AuthProfile::YGGDRASIL) + p.variation = L"elyby"; profiles.push_back(std::move(p)); } @@ -66,6 +80,9 @@ void AuthProfileManager::save() std::ofstream file(PROFILES_FILE, std::ios::binary | std::ios::trunc); if (!file) return; + uint32_t magic = PROFILE_MAGIC; + file.write(reinterpret_cast(&magic), sizeof(magic)); + uint32_t count = static_cast(profiles.size()); file.write(reinterpret_cast(&count), sizeof(count)); @@ -83,16 +100,17 @@ void AuthProfileManager::save() writeWstr(p.username); writeWstr(p.token); writeWstr(p.clientToken); + writeWstr(p.variation); } int32_t idx = static_cast(selectedProfile); file.write(reinterpret_cast(&idx), sizeof(idx)); } -void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid, const wstring &token, const wstring &clientToken) +void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid, const wstring &token, const wstring &clientToken, const wstring &variation) { wstring finalUid = uid.empty() ? L"offline_" + username : uid; - profiles.push_back({type, finalUid, username, token, clientToken}); + profiles.push_back({type, finalUid, username, token, clientToken, variation}); selectedProfile = static_cast(profiles.size()) - 1; save(); } @@ -137,12 +155,14 @@ bool AuthProfileManager::applySelectedProfile() } } } - else if (p.type == AuthProfile::ELYBY && !p.token.empty()) + else if (p.type == AuthProfile::YGGDRASIL && !p.token.empty()) { - if (!elybyValidate(narrowStr(p.token), narrowStr(p.clientToken))) + auto *provider = YggdrasilRegistry::find(p.variation); + if (!provider) provider = &YggdrasilRegistry::defaultProvider(); + if (!yggdrasilValidate(narrowStr(p.token), narrowStr(p.clientToken), provider->validateUrl())) { string newAccess, newClient; - if (elybyRefresh(narrowStr(p.token), narrowStr(p.clientToken), newAccess, newClient)) + if (yggdrasilRefresh(narrowStr(p.token), narrowStr(p.clientToken), newAccess, newClient, provider->refreshUrl())) { p.token = convStringToWstring(newAccess); if (!newClient.empty()) p.clientToken = convStringToWstring(newClient); @@ -169,7 +189,6 @@ bool AuthProfileManager::applySelectedProfile() return true; } -// --- AuthFlow --- std::thread AuthFlow::workerThread; std::atomic AuthFlow::state{AuthFlowState::IDLE}; @@ -177,6 +196,7 @@ std::atomic AuthFlow::cancelRequested{false}; AuthResult AuthFlow::result; wstring AuthFlow::userCode; wstring AuthFlow::verificationUri; +wstring AuthFlow::activeVariation; void AuthFlow::reset() { @@ -187,6 +207,7 @@ void AuthFlow::reset() result = {}; userCode.clear(); verificationUri.clear(); + activeVariation.clear(); cancelRequested = false; } @@ -197,11 +218,15 @@ void AuthFlow::startMicrosoft() workerThread = std::thread(microsoftFlowThread); } -void AuthFlow::startElyBy(const wstring &username, const wstring &password) +void AuthFlow::startYggdrasil(const wstring &username, const wstring &password, const wstring &providerName) { reset(); state = AuthFlowState::EXCHANGING; - workerThread = std::thread(elybyFlowThread, narrowStr(username), narrowStr(password)); + wstring resolvedName = providerName.empty() ? YggdrasilRegistry::defaultProvider().name : providerName; + activeVariation = resolvedName; + auto *provider = YggdrasilRegistry::find(resolvedName); + string authUrl = provider ? provider->authenticateUrl() : YggdrasilRegistry::defaultProvider().authenticateUrl(); + workerThread = std::thread(yggdrasilFlowThread, narrowStr(username), narrowStr(password), authUrl); } static void authFail(AuthResult &result, std::atomic &state, const wchar_t *msg) @@ -210,7 +235,6 @@ static void authFail(AuthResult &result, std::atomic &state, cons state = AuthFlowState::FAILED; } -// parse json response body, return discarded json on bad status static json parseResponse(const HttpResponse &resp, int expectedStatus) { if (resp.statusCode != expectedStatus) return json::value_t::discarded; @@ -273,19 +297,15 @@ static bool msRefreshOAuth(const string &refreshToken, string &newAccessToken, s newRefreshToken = j.value("refresh_token", ""); return !newAccessToken.empty(); } - -// validate ely.by token via yggdrasil /validate endpoint -static bool elybyValidate(const string &accessToken, const string &clientToken) +static bool yggdrasilValidate(const string &accessToken, const string &clientToken, const string &validateUrl) { - auto resp = HttpClient::post("https://authserver.ely.by/auth/validate", + auto resp = HttpClient::post(validateUrl, json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump()); return resp.statusCode == 200; } - -// refresh ely.by token via yggdrasil /refresh endpoint -static bool elybyRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken) +static bool yggdrasilRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken, const string &refreshUrl) { - auto resp = HttpClient::post("https://authserver.ely.by/auth/refresh", + auto resp = HttpClient::post(refreshUrl, json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump()); auto j = parseResponse(resp); @@ -403,9 +423,9 @@ void AuthFlow::microsoftFlowThread() state = AuthFlowState::COMPLETE; } -void AuthFlow::elybyFlowThread(const string &username, const string &password) +void AuthFlow::yggdrasilFlowThread(const string &username, const string &password, const string &authenticateUrl) { - auto resp = HttpClient::post("https://authserver.ely.by/auth/authenticate", json({ + auto resp = HttpClient::post(authenticateUrl, json({ {"username", username}, {"password", password}, {"clientToken", "mcconsoles"}, @@ -416,7 +436,7 @@ void AuthFlow::elybyFlowThread(const string &username, const string &password) if (resp.statusCode != 200 || respJson.is_discarded()) { - string msg = "Ely.by auth failed"; + string msg = "Yggdrasil auth failed"; if (!respJson.is_discarded()) msg = respJson.value("errorMessage", msg); result = {false, {}, {}, {}, {}, convStringToWstring(msg)}; state = AuthFlowState::FAILED; @@ -424,16 +444,16 @@ void AuthFlow::elybyFlowThread(const string &username, const string &password) } string accessToken = respJson.value("accessToken", ""); - string elyClientToken = respJson.value("clientToken", ""); + string yggClientToken = respJson.value("clientToken", ""); string uuid, name; try { uuid = respJson["selectedProfile"].value("id", ""); name = respJson["selectedProfile"].value("name", ""); } catch (...) {} if (accessToken.empty() || uuid.empty() || name.empty()) { - authFail(result, state, L"Ely.by response missing profile"); + authFail(result, state, L"Yggdrasil response missing profile"); return; } - result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), convStringToWstring(elyClientToken), {}}; + result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), convStringToWstring(yggClientToken), {}}; state = AuthFlowState::COMPLETE; } diff --git a/Minecraft.Client/AuthScreen.h b/Minecraft.Client/AuthScreen.h index c24afc262d..38425f0c10 100644 --- a/Minecraft.Client/AuthScreen.h +++ b/Minecraft.Client/AuthScreen.h @@ -5,13 +5,14 @@ using namespace std; struct AuthProfile { - enum Type : uint8_t { MICROSOFT, ELYBY, OFFLINE }; + enum Type : uint8_t { MICROSOFT, YGGDRASIL, OFFLINE }; Type type; wstring uid; wstring username; wstring token; wstring clientToken; + wstring variation; }; class AuthProfileManager @@ -23,7 +24,7 @@ class AuthProfileManager public: static void load(); static void save(); - static void addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid = L"", const wstring &token = L"", const wstring &clientToken = L""); + static void addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid = L"", const wstring &token = L"", const wstring &clientToken = L"", const wstring &variation = L""); static void removeSelectedProfile(); static bool applySelectedProfile(); @@ -60,17 +61,17 @@ class AuthFlow static AuthResult result; static wstring userCode; static wstring verificationUri; - + static wstring activeVariation; static void microsoftFlowThread(); - static void elybyFlowThread(const string &username, const string &password); + static void yggdrasilFlowThread(const string &username, const string &password, const string &authenticateUrl); public: static void startMicrosoft(); - static void startElyBy(const wstring &username, const wstring &password); - + static void startYggdrasil(const wstring &username, const wstring &password, const wstring &providerName = L""); static AuthFlowState getState() { return state.load(); } static const AuthResult &getResult() { return result; } static const wstring &getUserCode() { return userCode; } static const wstring &getVerificationUri() { return verificationUri; } + static const wstring &getActiveVariation() { return activeVariation; } static void reset(); }; diff --git a/Minecraft.Client/CMakeLists.txt b/Minecraft.Client/CMakeLists.txt index 9f75efd219..9f52cd53bd 100644 --- a/Minecraft.Client/CMakeLists.txt +++ b/Minecraft.Client/CMakeLists.txt @@ -44,6 +44,8 @@ set_source_files_properties(compat_shims.cpp PROPERTIES SKIP_PRECOMPILE_HEADERS configure_compiler_target(Minecraft.Client) +target_link_options(Minecraft.Client PRIVATE "/MAP") + set_target_properties(Minecraft.Client PROPERTIES OUTPUT_NAME "Minecraft.Client" VS_DEBUGGER_WORKING_DIRECTORY "$" diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index 2765e9214f..23585e8ead 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -56,7 +56,6 @@ #include "DLCTexturePack.h" #include "..\Minecraft.World\HandshakeManager.h" #include "..\Minecraft.World\SessionAuthModule.h" -#include "..\Minecraft.World\OfflineAuthModule.h" #include "AuthScreen.h" #ifdef _WINDOWS64 @@ -4141,21 +4140,33 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr< void ClientConnection::beginAuth() { + app.DebugPrintf("AUTH: beginAuth() starting\n"); handshakeManager = new HandshakeManager(false); - handshakeManager->registerModule(std::make_unique()); - handshakeManager->registerModule(std::make_unique()); - handshakeManager->registerModule(std::make_unique()); const auto &profiles = AuthProfileManager::getProfiles(); int idx = AuthProfileManager::getSelectedIndex(); + bool isOffline = true; if (idx >= 0 && idx < static_cast(profiles.size())) { const auto &p = profiles[idx]; + isOffline = (p.type == AuthProfile::OFFLINE); wstring variation; if (p.type == AuthProfile::MICROSOFT) variation = L"mojang"; - else if (p.type == AuthProfile::ELYBY) variation = L"elyby"; + else if (p.type == AuthProfile::YGGDRASIL) variation = p.variation; + app.DebugPrintf("AUTH: profile type=%d variation=%ls username=%ls\n", + (int)p.type, variation.c_str(), p.username.c_str()); handshakeManager->setCredentials(p.token, p.uid, p.username, variation); } + else + { + app.DebugPrintf("AUTH: no valid profile selected (idx=%d, count=%d)\n", + idx, (int)profiles.size()); + } + + if (!isOffline) + handshakeManager->registerModule(std::make_unique()); + handshakeManager->registerModule(std::make_unique()); + handshakeManager->registerModule(std::make_unique()); auto initial = handshakeManager->createInitialPacket(); if (initial) send(initial); diff --git a/Minecraft.Client/Common/Network/GameNetworkManager.cpp b/Minecraft.Client/Common/Network/GameNetworkManager.cpp index 50aeae689e..934a910580 100644 --- a/Minecraft.Client/Common/Network/GameNetworkManager.cpp +++ b/Minecraft.Client/Common/Network/GameNetworkManager.cpp @@ -413,6 +413,35 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame return false; } + if (!g_NetworkManager.IsHost()) + { + try { + connection->beginAuth(); + while (!connection->authComplete && !connection->isClosed() && IsInSession() && !g_NetworkManager.IsLeavingGame()) + { + connection->tick(); + Sleep(50); + } + } catch (...) { + app.DebugPrintf("AUTH: exception during auth handshake\n"); + connection->close(); + delete connection; + connection = nullptr; + MinecraftServer::HaltServer(); + return false; + } + + if (!connection->authComplete) + { + app.DebugPrintf("Auth handshake did not complete, aborting connection\n"); + connection->close(); + delete connection; + connection = nullptr; + MinecraftServer::HaltServer(); + return false; + } + } + connection->send(std::make_shared(minecraft->user->name)); // Tick connection until we're ready to go. The stages involved in this are: diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index 3d1febc33d..4070de8b71 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -2,6 +2,7 @@ #include "..\..\..\Minecraft.World\Mth.h" #include "..\..\..\Minecraft.World\StringHelpers.h" #include "..\..\..\Minecraft.World\Random.h" +#include "..\..\..\Minecraft.World\SessionAuthModule.h" #include "..\..\User.h" #include "..\..\MinecraftServer.h" #include "..\..\AuthScreen.h" @@ -459,7 +460,7 @@ void UIScene_MainMenu::RunAction(int iPad) static int s_authPad = 0; static void *s_authParam = nullptr; -static wstring s_elybyUsername; +static wstring s_yggdrasilUsername; static bool s_authFlowActive = false; static wstring ReadKeyboardText(int maxLen) @@ -509,11 +510,17 @@ static const wchar_t *BuildAuthProfileText() for (int i = 0; i < static_cast(profiles.size()); i++) { const auto &p = profiles[i]; - const wchar_t *type = (p.type == AuthProfile::MICROSOFT) ? L"[MS]" - : (p.type == AuthProfile::ELYBY) ? L"[Ely]" : L"[Off]"; + wstring label; + if (p.type == AuthProfile::MICROSOFT) label = L"[MS]"; + else if (p.type == AuthProfile::YGGDRASIL) + { + auto *provider = YggdrasilRegistry::find(p.variation); + label = provider ? L"[" + provider->displayName + L"]" : L"[Ygg]"; + } + else label = L"[Off]"; if (i > 0) text += L"\n"; text += (i == sel) ? L"> " : L" "; - text += wstring(type) + L" " + p.username; + text += label + L" " + p.username; } } return text.c_str(); @@ -531,7 +538,9 @@ void UIScene_MainMenu::ShowAuthMenu(int iPad, void *pClass) void UIScene_MainMenu::ShowAuthAddMenu(int iPad, void *pClass) { - static const wchar_t *addOptions[] = { L"Microsoft Auth", L"Ely.by Auth", L"Add Offline User", L"Back" }; + static wstring yggLabel; + yggLabel = YggdrasilRegistry::defaultProvider().displayName + L" Auth"; + const wchar_t *addOptions[] = { L"Microsoft Auth", yggLabel.c_str(), L"Add Offline User", L"Back" }; ShowAuthMessageBox(iPad, L"Add Profile", L"Select an auth type", addOptions, 4, &UIScene_MainMenu::AuthAddMenuReturned, pClass); } @@ -602,15 +611,15 @@ int UIScene_MainMenu::AuthAddMenuReturned(LPVOID lpParam, int iPad, const C4JSto { #ifdef _WINDOWS64 UIKeyboardInitData kbData; - kbData.title = L"Ely.by Username"; + kbData.title = L"Username"; kbData.defaultText = L""; kbData.maxChars = 64; - kbData.callback = &UIScene_MainMenu::ElyByUsernameReturned; + kbData.callback = &UIScene_MainMenu::YggdrasilUsernameReturned; kbData.lpParam = lpParam; kbData.pcMode = g_KBMInput.IsKBMActive(); ui.NavigateToScene(iPad, eUIScene_Keyboard, &kbData); #else - InputManager.RequestKeyboard(L"Ely.by Username", L"", (DWORD)0, 64, &UIScene_MainMenu::ElyByUsernameReturned, lpParam, C_4JInput::EKeyboardMode_Default); + InputManager.RequestKeyboard(L"Username", L"", (DWORD)0, 64, &UIScene_MainMenu::YggdrasilUsernameReturned, lpParam, C_4JInput::EKeyboardMode_Default); #endif break; } @@ -646,26 +655,26 @@ int UIScene_MainMenu::AuthMsFlowReturned(LPVOID lpParam, int iPad, const C4JStor return 0; } -int UIScene_MainMenu::ElyByUsernameReturned(LPVOID lpParam, const bool bRes) +int UIScene_MainMenu::YggdrasilUsernameReturned(LPVOID lpParam, const bool bRes) { if (bRes) { wstring name = ReadKeyboardText(128); if (!name.empty()) { - s_elybyUsername = std::move(name); + s_yggdrasilUsername = std::move(name); #ifdef _WINDOWS64 UIKeyboardInitData kbData; - kbData.title = L"Ely.by Password"; + kbData.title = L"Password"; kbData.defaultText = L""; kbData.maxChars = 128; - kbData.callback = &UIScene_MainMenu::ElyByPasswordReturned; + kbData.callback = &UIScene_MainMenu::YggdrasilPasswordReturned; kbData.lpParam = lpParam; kbData.pcMode = g_KBMInput.IsKBMActive(); kbData.keyboardMode = C_4JInput::EKeyboardMode_Password; ui.NavigateToScene(s_authPad, eUIScene_Keyboard, &kbData); #else - InputManager.RequestKeyboard(L"Ely.by Password", L"", (DWORD)0, 128, &UIScene_MainMenu::ElyByPasswordReturned, lpParam, C_4JInput::EKeyboardMode_Password); + InputManager.RequestKeyboard(L"Password", L"", (DWORD)0, 128, &UIScene_MainMenu::YggdrasilPasswordReturned, lpParam, C_4JInput::EKeyboardMode_Password); #endif return 0; } @@ -674,18 +683,20 @@ int UIScene_MainMenu::ElyByUsernameReturned(LPVOID lpParam, const bool bRes) return 0; } -int UIScene_MainMenu::ElyByPasswordReturned(LPVOID lpParam, const bool bRes) +int UIScene_MainMenu::YggdrasilPasswordReturned(LPVOID lpParam, const bool bRes) { if (bRes) { wstring pass = ReadKeyboardText(256); if (!pass.empty()) { - AuthFlow::startElyBy(s_elybyUsername, pass); + AuthFlow::startYggdrasil(s_yggdrasilUsername, pass); s_authFlowActive = true; + static wstring loginTitle; + loginTitle = YggdrasilRegistry::defaultProvider().displayName + L" Login"; static const wchar_t *waitOptions[] = { L"Cancel" }; - ShowAuthMessageBox(s_authPad, L"Ely.by Login", L"Authenticating...", + ShowAuthMessageBox(s_authPad, loginTitle.c_str(), L"Authenticating...", waitOptions, 1, &UIScene_MainMenu::AuthMsFlowReturned, lpParam, true); SecureZeroMemory(pass.data(), pass.size() * sizeof(wchar_t)); @@ -2144,8 +2155,9 @@ void UIScene_MainMenu::tick() { s_authFlowActive = false; const auto &r = AuthFlow::getResult(); - auto type = AuthFlow::getUserCode().empty() ? AuthProfile::ELYBY : AuthProfile::MICROSOFT; - AuthProfileManager::addProfile(type, r.username, r.uuid, r.accessToken, r.clientToken); + auto type = AuthFlow::getUserCode().empty() ? AuthProfile::YGGDRASIL : AuthProfile::MICROSOFT; + wstring variation = (type == AuthProfile::YGGDRASIL) ? AuthFlow::getActiveVariation() : L""; + AuthProfileManager::addProfile(type, r.username, r.uuid, r.accessToken, r.clientToken, variation); AuthFlow::reset(); if (scene) ui.NavigateBack(s_authPad); ShowAuthMenu(s_authPad, s_authParam); diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.h b/Minecraft.Client/Common/UI/UIScene_MainMenu.h index 87de602c69..0794566804 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.h @@ -151,8 +151,8 @@ class UIScene_MainMenu : public UIScene static int AuthAddMenuReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); static int AuthKeyboardReturned(LPVOID lpParam, const bool bRes); static int AuthMsFlowReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); - static int ElyByUsernameReturned(LPVOID lpParam, const bool bRes); - static int ElyByPasswordReturned(LPVOID lpParam, const bool bRes); + static int YggdrasilUsernameReturned(LPVOID lpParam, const bool bRes); + static int YggdrasilPasswordReturned(LPVOID lpParam, const bool bRes); static void LoadTrial(); diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index a85e3378b8..32d59bfa16 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -16,7 +16,6 @@ #include "Settings.h" #include "..\Minecraft.World\HandshakeManager.h" #include "..\Minecraft.World\SessionAuthModule.h" -#include "..\Minecraft.World\OfflineAuthModule.h" #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "..\Minecraft.Server\ServerLogManager.h" #include "..\Minecraft.Server\Access\Access.h" @@ -105,8 +104,14 @@ void PendingConnection::disconnect(DisconnectPacket::eDisconnectReason reason) void PendingConnection::handlePreLogin(shared_ptr packet) { - if (handshakeManager && !authComplete) + if (!authComplete) { + if (!handshakeManager) + { + app.DebugPrintf("PendingConnection: PreLogin received without auth handshake, disconnecting\n"); + disconnect(DisconnectPacket::eDisconnect_AuthFailed); + return; + } app.DebugPrintf("PendingConnection: PreLogin received before auth complete, disconnecting\n"); disconnect(DisconnectPacket::eDisconnect_Closed); return; @@ -413,10 +418,12 @@ bool PendingConnection::isDisconnected() void PendingConnection::initAuth() { handshakeManager = new HandshakeManager(true); - if (server->authMode == "session") - handshakeManager->registerModule(std::make_unique()); - else + handshakeManager->registerModule(std::make_unique()); + if (server->authMode != "session") + { + handshakeManager->registerModule(std::make_unique()); handshakeManager->registerModule(std::make_unique()); + } } void PendingConnection::handleAuth(const shared_ptr &packet) diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index fa5f4cccbd..2abaf7c8cb 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -1296,6 +1296,34 @@ static Minecraft* InitialiseMinecraftRuntime() return pMinecraft; } +static LONG WINAPI CrashHandler(EXCEPTION_POINTERS *ep) +{ + auto *record = ep->ExceptionRecord; + auto *ctx = ep->ContextRecord; + HMODULE hMod = GetModuleHandleA(nullptr); + + wchar_t buf[1024]; + swprintf(buf, 1024, + L"Unhandled exception!\n\nCode: 0x%08X\nAddress: 0x%p\nBase: 0x%p\nRVA: 0x%llX\n\nPlease report this crash info.", + record->ExceptionCode, record->ExceptionAddress, hMod, + (unsigned long long)((char *)record->ExceptionAddress - (char *)hMod)); + + FILE *clog = nullptr; + if (fopen_s(&clog, "crash.log", "w") == 0 && clog) + { + fprintf(clog, "[CRASH] Code=0x%08X Addr=0x%p Base=0x%p RVA=0x%llX\n", + record->ExceptionCode, record->ExceptionAddress, hMod, + (unsigned long long)((char *)record->ExceptionAddress - (char *)hMod)); + fprintf(clog, " RIP=0x%llX RSP=0x%llX RBP=0x%llX\n", ctx->Rip, ctx->Rsp, ctx->Rbp); + fprintf(clog, " RAX=0x%llX RBX=0x%llX RCX=0x%llX\n", ctx->Rax, ctx->Rbx, ctx->Rcx); + fprintf(clog, " RDX=0x%llX RSI=0x%llX RDI=0x%llX\n", ctx->Rdx, ctx->Rsi, ctx->Rdi); + fclose(clog); + } + + MessageBoxW(nullptr, buf, L"MinecraftConsoles Crash", MB_OK | MB_ICONERROR); + return EXCEPTION_EXECUTE_HANDLER; +} + int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, @@ -1304,6 +1332,8 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); + SetUnhandledExceptionFilter(CrashHandler); + // 4J-Win64: set CWD to exe dir so asset paths resolve correctly { char szExeDir[MAX_PATH] = {}; diff --git a/Minecraft.World/AuthModule.h b/Minecraft.World/AuthModule.h index ae5875b428..308ddcbe1b 100644 --- a/Minecraft.World/AuthModule.h +++ b/Minecraft.World/AuthModule.h @@ -25,3 +25,21 @@ class AuthModule protected: bool extractIdentity(const vector> &fields, wstring &outUid, wstring &outUsername); }; + +class KeypairOfflineAuthModule : public AuthModule +{ +public: + const wchar_t *schemeName() override { return L"mcconsoles:keypair_offline"; } + vector supportedVariations() override { return {L"rsa2048", L"ed25519"}; } + vector> getSettings(const wstring &variation) override { return {{L"key_type", variation}}; } + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override { return extractIdentity(fields, outUid, outUsername); } +}; + +class OfflineAuthModule : public AuthModule +{ +public: + const wchar_t *schemeName() override { return L"mcconsoles:offline"; } + vector supportedVariations() override { return {}; } + vector> getSettings(const wstring &) override { return {}; } + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override { return extractIdentity(fields, outUid, outUsername); } +}; diff --git a/Minecraft.World/HandshakeManager.cpp b/Minecraft.World/HandshakeManager.cpp index 96d83b26b5..8ccc4b042a 100644 --- a/Minecraft.World/HandshakeManager.cpp +++ b/Minecraft.World/HandshakeManager.cpp @@ -48,7 +48,16 @@ shared_ptr HandshakeManager::handlePacket(const shared_ptr HandshakeManager::createInitialPacket() { state = HandshakeState::VERSION_SENT; - return makePacket(AuthStage::ANNOUNCE_VERSION, {{L"version", PROTOCOL_VERSION}}); + wstring schemes; + for (const auto &[name, mod] : modules) + { + if (!schemes.empty()) schemes += L","; + schemes += name; + } + return makePacket(AuthStage::ANNOUNCE_VERSION, { + {L"version", PROTOCOL_VERSION}, + {L"schemes", schemes} + }); } shared_ptr HandshakeManager::handleServer(const shared_ptr &packet) @@ -61,11 +70,32 @@ shared_ptr HandshakeManager::handleServer(const shared_ptrsecond.get(); + auto splitCsv = [](const wstring &s) { + vector out; + for (size_t p = 0; p < s.size(); ) { + size_t c = s.find(L',', p); + if (c == wstring::npos) c = s.size(); + out.push_back(s.substr(p, c - p)); + p = c + 1; + } + return out; + }; + auto supported = splitCsv(getField(packet->fields, L"schemes")); + + activeModule = nullptr; + for (auto &[name, mod] : modules) + { + if (supported.empty() || std::find(supported.begin(), supported.end(), name) != supported.end()) + { + activeModule = mod.get(); + break; + } + } + if (!activeModule) + return fail(); state = HandshakeState::SCHEME_DECLARED; return makePacket(AuthStage::DECLARE_SCHEME, { {L"version", PROTOCOL_VERSION}, @@ -133,6 +163,8 @@ shared_ptr HandshakeManager::handleClient(const shared_ptrfields, L"version"); wstring scheme = getField(packet->fields, L"scheme"); + app.DebugPrintf("AUTH CLIENT: DECLARE_SCHEME scheme=%ls\n", scheme.c_str()); + if (protocolVersion != PROTOCOL_VERSION) return fail(); @@ -149,24 +181,39 @@ shared_ptr HandshakeManager::handleClient(const shared_ptrfields, L"serverId"); - wstring sessionEndpoint = getField(packet->fields, L"sessionEndpoint"); + wstring joinUrlW = getField(packet->fields, L"joinUrl"); wstring scheme(activeModule->schemeName()); - if (scheme == L"mcconsoles:session" && !accessToken.empty()) + + app.DebugPrintf("AUTH CLIENT: SCHEME_SETTINGS joinUrl=%ls serverId=%ls\n", joinUrlW.c_str(), serverId.c_str()); + + if (scheme == L"mcconsoles:session" && !accessToken.empty() && !joinUrlW.empty()) { nlohmann::json body = { {"accessToken", narrowStr(accessToken)}, {"selectedProfile", narrowStr(clientUid)}, {"serverId", narrowStr(serverId)} }; - auto resp = HttpClient::post(narrowStr(sessionEndpoint) + "/session/minecraft/join", body.dump()); - if (resp.statusCode != 204) + string joinUrl = narrowStr(joinUrlW); + app.DebugPrintf("AUTH CLIENT: POSTing join to %s\n", joinUrl.c_str()); + HttpResponse resp; + try { + resp = HttpClient::post(joinUrl, body.dump()); + } catch (...) { + app.DebugPrintf("AUTH CLIENT: join POST threw exception\n"); + return fail(); + } + app.DebugPrintf("AUTH CLIENT: join POST status=%d\n", resp.statusCode); + if (resp.statusCode < 200 || resp.statusCode >= 300) return fail(); } diff --git a/Minecraft.World/MapItem.cpp b/Minecraft.World/MapItem.cpp index 61c203e3b4..3ff8206c65 100644 --- a/Minecraft.World/MapItem.cpp +++ b/Minecraft.World/MapItem.cpp @@ -21,6 +21,7 @@ MapItem::MapItem(int id) : ComplexItem(id) shared_ptr MapItem::getSavedData(short idNum, Level *level) { + if (!level) return nullptr; std::wstring id = wstring( L"map_" ) + std::to_wstring(idNum); shared_ptr mapItemSavedData = dynamic_pointer_cast(level->getSavedData(typeid(MapItemSavedData), id)); @@ -42,6 +43,7 @@ shared_ptr MapItem::getSavedData(short idNum, Level *level) shared_ptr MapItem::getSavedData(shared_ptr itemInstance, Level *level) { + if (!level) return nullptr; MemSect(31); std::wstring id = wstring( L"map_" ) + std::to_wstring(itemInstance->getAuxValue() ); MemSect(0); @@ -254,6 +256,7 @@ void MapItem::update(Level *level, shared_ptr player, shared_ptr itemInstance, Level *level, shared_ptr owner, int slot, bool selected) { + if (!level) return; if (level->isClientSide) return; shared_ptr data = getSavedData(itemInstance, level); @@ -293,7 +296,10 @@ void MapItem::inventoryTick(shared_ptr itemInstance, Level *level, shared_ptr MapItem::getUpdatePacket(shared_ptr itemInstance, Level *level, shared_ptr player) { - charArray data = MapItem::getSavedData(itemInstance, level)->getUpdatePacket(itemInstance, level, player); + if (!level) return nullptr; + auto savedData = MapItem::getSavedData(itemInstance, level); + if (!savedData) return nullptr; + charArray data = savedData->getUpdatePacket(itemInstance, level, player); if (data.data == nullptr || data.length == 0) return nullptr; @@ -304,6 +310,7 @@ shared_ptr MapItem::getUpdatePacket(shared_ptr itemInstanc void MapItem::onCraftedBy(shared_ptr itemInstance, Level *level, shared_ptr player) { + if (!level) return; wchar_t buf[64]; int mapScale = 3; diff --git a/Minecraft.World/OfflineAuthModule.cpp b/Minecraft.World/OfflineAuthModule.cpp deleted file mode 100644 index 5369ceacc6..0000000000 --- a/Minecraft.World/OfflineAuthModule.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "stdafx.h" -#include "OfflineAuthModule.h" - -const wchar_t *KeypairOfflineAuthModule::schemeName() { return L"mcconsoles:keypair_offline"; } - -vector KeypairOfflineAuthModule::supportedVariations() -{ - return {L"rsa2048", L"ed25519"}; -} - -vector> KeypairOfflineAuthModule::getSettings(const wstring &variation) -{ - return {{L"key_type", variation}}; -} - -bool KeypairOfflineAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) -{ - return extractIdentity(fields, outUid, outUsername); -} - -const wchar_t *OfflineAuthModule::schemeName() { return L"mcconsoles:offline"; } - -vector OfflineAuthModule::supportedVariations() -{ - return {}; -} - -vector> OfflineAuthModule::getSettings(const wstring &variation) -{ - return {}; -} - -bool OfflineAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) -{ - return extractIdentity(fields, outUid, outUsername); -} diff --git a/Minecraft.World/OfflineAuthModule.h b/Minecraft.World/OfflineAuthModule.h deleted file mode 100644 index b2b4454d9c..0000000000 --- a/Minecraft.World/OfflineAuthModule.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -using namespace std; - -#include "AuthModule.h" - -class KeypairOfflineAuthModule : public AuthModule -{ -public: - const wchar_t *schemeName() override; - vector supportedVariations() override; - vector> getSettings(const wstring &variation) override; - bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; -}; - -class OfflineAuthModule : public AuthModule -{ -public: - const wchar_t *schemeName() override; - vector supportedVariations() override; - vector> getSettings(const wstring &variation) override; - bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; -}; diff --git a/Minecraft.World/SessionAuthModule.cpp b/Minecraft.World/SessionAuthModule.cpp index 50844b39b6..5f100aba2b 100644 --- a/Minecraft.World/SessionAuthModule.cpp +++ b/Minecraft.World/SessionAuthModule.cpp @@ -4,6 +4,8 @@ #include "StringHelpers.h" #include "Common/vendor/nlohmann/json.hpp" #include +#include +#include static wstring generateServerId() { @@ -14,50 +16,96 @@ static wstring generateServerId() return id; } -SessionAuthModule::SessionAuthModule() +static vector s_registryProviders; +static bool s_registryLoaded = false; + +void YggdrasilRegistry::load() +{ + s_registryProviders.clear(); + + std::ifstream file("yggdrasil.json"); + if (file.is_open()) + { + auto j = nlohmann::json::parse(file, nullptr, false); + if (j.is_array()) + { + for (const auto &entry : j) + { + if (!entry.is_object()) continue; + YggdrasilProviderConfig cfg; + cfg.name = convStringToWstring(entry.value("name", "")); + cfg.displayName = convStringToWstring(entry.value("displayName", "")); + cfg.authUrl = entry.value("authUrl", ""); + cfg.sessionUrl = entry.value("sessionUrl", ""); + if (!cfg.name.empty() && !cfg.sessionUrl.empty()) + s_registryProviders.push_back(std::move(cfg)); + } + } + } + + if (std::none_of(s_registryProviders.begin(), s_registryProviders.end(), + [](const auto &p) { return p.name == L"elyby"; })) + s_registryProviders.insert(s_registryProviders.begin(), + {L"elyby", L"Ely.by", "https://authserver.ely.by/auth", "https://authserver.ely.by/session"}); + + s_registryProviders.push_back( + {L"mojang", L"Mojang", "", "https://sessionserver.mojang.com/session/minecraft"}); + + s_registryLoaded = true; +} + +const vector &YggdrasilRegistry::providers() { - endpoints[L"mojang"] = {L"https://authserver.mojang.com", L"https://sessionserver.mojang.com"}; - endpoints[L"elyby"] = {L"https://authserver.ely.by", L"https://authserver.ely.by"}; + if (!s_registryLoaded) load(); + return s_registryProviders; +} + +const YggdrasilProviderConfig *YggdrasilRegistry::find(const wstring &name) +{ + for (const auto &p : providers()) + if (p.name == name) return &p; + return nullptr; +} + +const YggdrasilProviderConfig &YggdrasilRegistry::defaultProvider() +{ + const auto &list = providers(); + for (const auto &p : list) + if (p.name != L"mojang") return p; + return list[0]; } const wchar_t *SessionAuthModule::schemeName() { return L"mcconsoles:session"; } vector SessionAuthModule::supportedVariations() { - return {L"mojang", L"elyby"}; + vector result; + for (const auto &p : YggdrasilRegistry::providers()) + result.push_back(p.name); + return result; } vector> SessionAuthModule::getSettings(const wstring &variation) { - auto it = endpoints.find(variation); - if (it == endpoints.end()) return {}; + auto *p = YggdrasilRegistry::find(variation); + if (!p) return {}; - activeSessionEndpoint = it->second.sessionEndpoint; + activeProvider = p; activeServerId = generateServerId(); - return { - {L"authEndpoint", it->second.authEndpoint}, - {L"sessionEndpoint", it->second.sessionEndpoint}, + {L"joinUrl", convStringToWstring(p->joinUrl())}, {L"serverId", activeServerId} }; } -bool SessionAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) +bool SessionAuthModule::serverVerify(const YggdrasilProviderConfig &provider, const wstring &username, const wstring &serverId, wstring &outUid, wstring &outUsername) { - wstring username; - for (const auto &[k, v] : fields) - { - if (k == L"username") username = v; - } - - if (username.empty() || activeServerId.empty() || activeSessionEndpoint.empty()) - return false; - - string url = narrowStr(activeSessionEndpoint) - + "/session/minecraft/hasJoined?username=" + narrowStr(username) - + "&serverId=" + narrowStr(activeServerId); + string url = provider.hasJoinedUrl() + "?username=" + narrowStr(username) + "&serverId=" + narrowStr(serverId); + app.DebugPrintf("AUTH SERVER [%ls]: hasJoined GET %s\n", provider.name.c_str(), url.c_str()); auto response = HttpClient::get(url); + app.DebugPrintf("AUTH SERVER [%ls]: hasJoined status=%d\n", provider.name.c_str(), response.statusCode); + if (response.statusCode != 200) return false; @@ -67,12 +115,25 @@ bool SessionAuthModule::onAuthData(const vector> &fields, string id = json.value("id", ""); string name = json.value("name", ""); - if (id.empty() || name.empty()) return false; outUid = convStringToWstring(id); outUsername = convStringToWstring(name); + return true; +} + +bool SessionAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) +{ + wstring username; + for (const auto &[k, v] : fields) + if (k == L"username") username = v; + + if (username.empty() || activeServerId.empty() || !activeProvider) + return false; + + if (!serverVerify(*activeProvider, username, activeServerId, outUid, outUsername)) + return false; return validate(outUid, outUsername); } diff --git a/Minecraft.World/SessionAuthModule.h b/Minecraft.World/SessionAuthModule.h index ecb60c06fa..2990edbd9b 100644 --- a/Minecraft.World/SessionAuthModule.h +++ b/Minecraft.World/SessionAuthModule.h @@ -1,24 +1,41 @@ #pragma once using namespace std; - #include "AuthModule.h" -#include +#include +#include -class SessionAuthModule : public AuthModule +struct YggdrasilProviderConfig +{ + wstring name; + wstring displayName; + string authUrl; + string sessionUrl; + string authenticateUrl() const { return authUrl + "/authenticate"; } + string validateUrl() const { return authUrl + "/validate"; } + string refreshUrl() const { return authUrl + "/refresh"; } + string joinUrl() const { return sessionUrl + "/join"; } + string hasJoinedUrl() const { return sessionUrl + "/hasJoined"; } +}; + +class YggdrasilRegistry { public: - struct EndpointPair { - wstring authEndpoint; - wstring sessionEndpoint; - }; + static void load(); + static const vector &providers(); + static const YggdrasilProviderConfig *find(const wstring &name); + static const YggdrasilProviderConfig &defaultProvider(); +}; +class SessionAuthModule : public AuthModule +{ private: - unordered_map endpoints; - wstring activeSessionEndpoint; + const YggdrasilProviderConfig *activeProvider = nullptr; wstring activeServerId; + bool serverVerify(const YggdrasilProviderConfig &provider, const wstring &username, const wstring &serverId, wstring &outUid, wstring &outUsername); + public: - SessionAuthModule(); + SessionAuthModule() = default; const wchar_t *schemeName() override; vector supportedVariations() override; diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index 8bf351e962..dc843f1385 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -263,8 +263,6 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/AuthModule.h" "${CMAKE_CURRENT_SOURCE_DIR}/SessionAuthModule.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/SessionAuthModule.h" - "${CMAKE_CURRENT_SOURCE_DIR}/OfflineAuthModule.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/OfflineAuthModule.h" "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.h" "${CMAKE_CURRENT_SOURCE_DIR}/UUID.cpp" diff --git a/yggdrasil.json b/yggdrasil.json new file mode 100644 index 0000000000..4fbd954975 --- /dev/null +++ b/yggdrasil.json @@ -0,0 +1,8 @@ +[ + { + "name": "elyby", + "displayName": "Ely.by", + "authUrl": "https://authserver.ely.by/auth", + "sessionUrl": "https://authserver.ely.by/session" + } +] From df3dd69a0d502cddf9047fcdb7e2cee833bf4287 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Mon, 6 Apr 2026 03:09:52 -0400 Subject: [PATCH 18/18] tweak: change name of main menu button from "Auth'"to "Switch User" --- Minecraft.Client/Common/UI/UIScene_MainMenu.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index 4070de8b71..8b7169515c 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -51,12 +51,12 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye if(ProfileManager.IsFullVersion()) { m_bTrialVersion=false; - m_buttons[static_cast(eControl_UnlockOrDLC)].init(L"Auth",eControl_UnlockOrDLC); + m_buttons[static_cast(eControl_UnlockOrDLC)].init(L"Switch User",eControl_UnlockOrDLC); } else { m_bTrialVersion=true; - m_buttons[static_cast(eControl_UnlockOrDLC)].init(L"Auth",eControl_UnlockOrDLC); + m_buttons[static_cast(eControl_UnlockOrDLC)].init(L"Switch User",eControl_UnlockOrDLC); } #ifndef _DURANGO @@ -187,7 +187,7 @@ void UIScene_MainMenu::handleGainFocus(bool navBack) if(navBack && ProfileManager.IsFullVersion()) { // once again replacing the shop with auth. not a bad thing. - m_buttons[static_cast(eControl_UnlockOrDLC)].setLabel(L"Auth"); + m_buttons[static_cast(eControl_UnlockOrDLC)].setLabel(L"Switch User"); } #if TO_BE_IMPLEMENTED @@ -2264,7 +2264,7 @@ void UIScene_MainMenu::tick() if(ProfileManager.IsFullVersion()) { m_bTrialVersion=false; - m_buttons[(int)eControl_UnlockOrDLC].init(L"Auth",eControl_UnlockOrDLC); + m_buttons[(int)eControl_UnlockOrDLC].init(L"Switch User",eControl_UnlockOrDLC); } } #endif