From 1daae8e2a8b8ebb4543397b3167b1e238a2e93a4 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Fri, 3 Apr 2026 21:39:59 -0400 Subject: [PATCH] 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 | 215 +++++++++++--- 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, 627 insertions(+), 100 deletions(-) diff --git a/Minecraft.Client/AuthScreen.cpp b/Minecraft.Client/AuthScreen.cpp index 463e07f9..c0d1d894 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 f31f294a..d01ab41a 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 @@ private: 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 @@ public: 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 92402784..26615352 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 35edf17f..e21cf85b 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 146934c1..72ada08f 100644 --- a/Minecraft.Client/Common/UI/UIScene_Keyboard.h +++ b/Minecraft.Client/Common/UI/UIScene_Keyboard.h @@ -13,6 +13,7 @@ private: 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 f9dbc6eb..a2095cd3 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,24 +627,72 @@ int UIScene_MainMenu::AuthAddMenuReturned(LPVOID lpParam, int iPad, const C4JSto return 0; } +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) + { + wstring name = ReadKeyboardText(128); + if (!name.empty()) + { + s_elybyUsername = std::move(name); +#ifdef _WINDOWS64 + 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.RequestKeyboard(L"Ely.by Password", L"", (DWORD)0, 128, &UIScene_MainMenu::ElyByPasswordReturned, lpParam, C_4JInput::EKeyboardMode_Password); +#endif + 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()) + { + 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) { - 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); - } + wstring name = ReadKeyboardText(128); + if (!name.empty()) + AuthProfileManager::addProfile(AuthProfile::OFFLINE, name); } ShowAuthMenu(s_authPad, lpParam); return 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 4a059cce..87de602c 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.h @@ -150,6 +150,9 @@ private: 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 9767b83b..08556054 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 28fcbcd1..2c08daff 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 e283e2b3..45b11734 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 851b1260..e5f4a6fa 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 8b6b87d2..969c1a53 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 9e9da1c8..360f53ea 100644 --- a/Minecraft.World/HandshakeManager.h +++ b/Minecraft.World/HandshakeManager.h @@ -33,6 +33,13 @@ private: wstring activeVariation; wstring protocolVersion; + wstring accessToken; + wstring clientUid; + wstring clientUsername; + wstring preferredVariation; + + vector> pendingPackets; + public: wstring finalUid; wstring finalUsername; @@ -41,8 +48,10 @@ public: ~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 b59d1b1f..6e929635 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 317941c0..f08c1e79 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 = {}); };