feat: adds auth, implicit support for 64x64 skins,

This commit is contained in:
Matthew Toro 2026-03-27 01:03:33 -04:00
parent 73d713878c
commit 5bc5e029fd
61 changed files with 9183 additions and 26 deletions

View file

@ -5,6 +5,68 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
function(configure_msvc_system_includes)
if(NOT MSVC)
return()
endif()
get_filename_component(_msvc_bin_dir "${CMAKE_CXX_COMPILER}" DIRECTORY)
get_filename_component(_msvc_root "${_msvc_bin_dir}/../../.." ABSOLUTE)
set(_msvc_include_dir "${_msvc_root}/include")
set(_sdk_root "C:/Program Files (x86)/Windows Kits/10/Include")
set(_sdk_lib_root "C:/Program Files (x86)/Windows Kits/10/Lib")
set(_sdk_include_dirs)
set(_sdk_lib_dirs)
if(EXISTS "${_sdk_root}")
file(GLOB _sdk_versions LIST_DIRECTORIES true "${_sdk_root}/*")
if(_sdk_versions)
list(SORT _sdk_versions COMPARE NATURAL ORDER DESCENDING)
list(GET _sdk_versions 0 _sdk_version_dir)
foreach(_sdk_subdir ucrt shared um winrt cppwinrt)
if(EXISTS "${_sdk_version_dir}/${_sdk_subdir}")
list(APPEND _sdk_include_dirs "${_sdk_version_dir}/${_sdk_subdir}")
endif()
endforeach()
endif()
endif()
if(EXISTS "${_sdk_lib_root}")
file(GLOB _sdk_lib_versions LIST_DIRECTORIES true "${_sdk_lib_root}/*")
if(_sdk_lib_versions)
list(SORT _sdk_lib_versions COMPARE NATURAL ORDER DESCENDING)
list(GET _sdk_lib_versions 0 _sdk_lib_version_dir)
foreach(_sdk_lib_subdir ucrt um)
if(EXISTS "${_sdk_lib_version_dir}/${_sdk_lib_subdir}/x64")
list(APPEND _sdk_lib_dirs "${_sdk_lib_version_dir}/${_sdk_lib_subdir}/x64")
endif()
endforeach()
endif()
endif()
set(_system_include_dirs)
if(EXISTS "${_msvc_include_dir}")
list(APPEND _system_include_dirs "${_msvc_include_dir}")
endif()
list(APPEND _system_include_dirs ${_sdk_include_dirs})
set(_system_lib_dirs)
if(EXISTS "${_msvc_root}/lib/x64")
list(APPEND _system_lib_dirs "${_msvc_root}/lib/x64")
endif()
list(APPEND _system_lib_dirs ${_sdk_lib_dirs})
if(_system_include_dirs)
list(REMOVE_DUPLICATES _system_include_dirs)
include_directories(${_system_include_dirs})
endif()
if(_system_lib_dirs)
list(REMOVE_DUPLICATES _system_lib_dirs)
link_directories(${_system_lib_dirs})
endif()
endfunction()
if(NOT WIN32)
message(FATAL_ERROR "This CMake build currently supports Windows only.")
endif()
@ -13,6 +75,8 @@ if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "Use a 64-bit generator/toolchain (x64).")
endif()
configure_msvc_system_includes()
set(CMAKE_CONFIGURATION_TYPES
"Debug"
"Release"
@ -77,6 +141,7 @@ list(APPEND MINECRAFT_SHARED_DEFINES ${PLATFORM_DEFINES})
# Sources
# ---
add_subdirectory(Minecraft.World)
add_subdirectory(newauth)
add_subdirectory(Minecraft.Client)
if(PLATFORM_NAME STREQUAL "Windows64") # Server is only supported on Windows for now
add_subdirectory(Minecraft.Server)

View file

@ -51,6 +51,7 @@ set_target_properties(Minecraft.Client PROPERTIES
target_link_libraries(Minecraft.Client PRIVATE
Minecraft.World
newauth
d3d11
d3dcompiler
XInput9_1_0

View file

@ -58,6 +58,12 @@
#ifdef _WINDOWS64
#include "Xbox\Network\NetworkPlayerXbox.h"
#include "Common\Network\PlatformNetworkManagerStub.h"
#include "..\newauth\include\newauth.h"
#include "..\newauth\include\newauthManager.h"
#include "..\Minecraft.World\AuthSchemePacket.h"
#include "..\Minecraft.World\AuthResponsePacket.h"
#include "..\Minecraft.World\AuthResultPacket.h"
#include "..\Minecraft.World\GameUUID.h"
#endif
@ -368,6 +374,14 @@ void ClientConnection::handleLogin(shared_ptr<LoginPacket> packet)
minecraft->player->setCustomSkin( app.GetPlayerSkinId(m_userIndex) );
minecraft->player->setCustomCape( app.GetPlayerCapeId(m_userIndex) );
#ifdef _WINDOWS64
// gives me a skin.
if (!m_authSkinKey.empty() && app.IsFileInMemoryTextures(m_authSkinKey))
{
minecraft->player->customTextureUrl = m_authSkinKey;
app.DebugPrintf("Auth: Applied MS skin '%ls' to local player\n", m_authSkinKey.c_str());
}
#endif
minecraft->createPrimaryLocalPlayer(ProfileManager.GetPrimaryPad());
@ -751,6 +765,45 @@ void ClientConnection::handleAddEntity(shared_ptr<AddEntityPacket> packet)
}
}
#ifdef _WINDOWS64
bool ClientConnection::ensureRemoteNetworkPlayer(BYTE smallId, const std::wstring *playerName, PlayerUID xuid)
{
if (smallId >= MINECRAFT_NET_MAX_PLAYERS)
return false;
extern CPlatformNetworkManagerStub* g_pPlatformNetworkManager;
if (g_pPlatformNetworkManager == nullptr)
return false;
INetworkPlayer *networkPlayer = g_NetworkManager.GetPlayerBySmallId(smallId);
IQNetPlayer *qnetPlayer = nullptr;
if (networkPlayer != nullptr)
{
qnetPlayer = static_cast<NetworkPlayerXbox *>(networkPlayer)->GetQNetPlayer();
}
if (qnetPlayer == nullptr)
{
qnetPlayer = &IQNet::m_player[smallId];
extern void Win64_SetupRemoteQNetPlayer(IQNetPlayer * player, BYTE smallId, bool isHost, bool isLocal);
Win64_SetupRemoteQNetPlayer(qnetPlayer, smallId, false, false);
g_pPlatformNetworkManager->NotifyPlayerJoined(qnetPlayer);
}
if (playerName != nullptr && !playerName->empty())
{
wcsncpy_s(qnetPlayer->m_gamertag, 32, playerName->c_str(), _TRUNCATE);
}
if (xuid != INVALID_XUID)
{
qnetPlayer->m_resolvedXuid = xuid;
}
return true;
}
#endif
void ClientConnection::handleAddExperienceOrb(shared_ptr<AddExperienceOrbPacket> packet)
{
shared_ptr<Entity> e = std::make_shared<ExperienceOrb>(level, packet->x / 32.0, packet->y / 32.0, packet->z / 32.0, packet->value);
@ -882,6 +935,15 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
#ifdef _WINDOWS64
{
BYTE pendingSmallId = 0xFF;
auto pendingInfo = m_pendingRemotePlayerSmallIds.find(packet->id);
if (pendingInfo != m_pendingRemotePlayerSmallIds.end())
{
pendingSmallId = pendingInfo->second;
ensureRemoteNetworkPlayer(pendingSmallId, &packet->name, player->getXuid());
m_pendingRemotePlayerSmallIds.erase(pendingInfo);
}
IQNetPlayer* matchedQNetPlayer = nullptr;
PlayerUID pktXuid = player->getXuid();
const PlayerUID WIN64_XUID_BASE = (PlayerUID)0xe000d45248242f2e;
@ -897,6 +959,16 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
}
}
if (matchedQNetPlayer == nullptr && pendingSmallId != 0xFF)
{
INetworkPlayer* np = g_NetworkManager.GetPlayerBySmallId(pendingSmallId);
if (np != nullptr)
{
NetworkPlayerXbox* npx = (NetworkPlayerXbox*)np;
matchedQNetPlayer = npx->GetQNetPlayer();
}
}
// Current Win64 path: identify QNet player by name and attach packet XUID.
if (matchedQNetPlayer == nullptr)
{
@ -1087,6 +1159,8 @@ void ClientConnection::handleRemoveEntity(shared_ptr<RemoveEntitiesPacket> packe
{
for (int i = 0; i < packet->ids.length; i++)
{
m_pendingRemotePlayerSmallIds.erase(packet->ids[i]);
shared_ptr<Entity> entity = getEntity(packet->ids[i]);
if (entity != nullptr && entity->GetType() == eTYPE_PLAYER)
{
@ -2004,8 +2078,15 @@ void ClientConnection::handlePreLogin(shared_ptr<PreLoginPacket> packet)
// 4J - Check that we can play with all the players already in the game who have Friends-Only UGC set
BOOL canPlay = TRUE;
BOOL canPlayLocal = TRUE;
#ifdef _WINDOWS64
// yes, this looks wrong, but on Win64 we don't have the API to check friends of friends, so just assume that there -
// - is at least one friend and that we are friends with the host. fuck xbl. we can remedy this with discord in the future or some shit -- mattsumi
BOOL isAtLeastOneFriend = TRUE;
BOOL isFriendsWithHost = TRUE;
#else
BOOL isAtLeastOneFriend = g_NetworkManager.IsHost();
BOOL isFriendsWithHost = TRUE;
#endif
BOOL cantPlayContentRestricted = FALSE;
if(!g_NetworkManager.IsHost())
@ -2443,8 +2524,14 @@ void ClientConnection::handlePreLogin(shared_ptr<PreLoginPacket> packet)
}
BOOL allAllowed, friendsAllowed;
ProfileManager.AllowedPlayerCreatedContent(m_userIndex,true,&allAllowed,&friendsAllowed);
#ifdef _WINDOWS64
// Save ugcPlayersVersion for the deferred LoginPacket sent after auth handshake
m_preLoginUgcPlayersVersion = packet->m_ugcPlayersVersion;
#else
send(std::make_shared<LoginPacket>(minecraft->user->name, SharedConstants::NETWORK_PROTOCOL_VERSION, offlineXUID, onlineXUID, (allAllowed != TRUE && friendsAllowed == TRUE),
packet->m_ugcPlayersVersion, app.GetPlayerSkinId(m_userIndex), app.GetPlayerCapeId(m_userIndex), ProfileManager.IsGuest(m_userIndex)));
#endif
if(!g_NetworkManager.IsHost() )
{
@ -3556,9 +3643,31 @@ bool ClientConnection::isServerPacketListener()
void ClientConnection::handlePlayerInfo(shared_ptr<PlayerInfoPacket> packet)
{
unsigned int startingPrivileges = app.GetPlayerPrivileges(packet->m_networkSmallId);
BYTE networkSmallId = static_cast<BYTE>(packet->m_networkSmallId);
unsigned int startingPrivileges = app.GetPlayerPrivileges(networkSmallId);
INetworkPlayer *networkPlayer = g_NetworkManager.GetPlayerBySmallId(packet->m_networkSmallId);
INetworkPlayer *networkPlayer = g_NetworkManager.GetPlayerBySmallId(networkSmallId);
#ifdef _WINDOWS64
if (networkPlayer == nullptr && packet->m_entityId >= 0)
{
shared_ptr<Entity> entity = getEntity(packet->m_entityId);
if (entity != nullptr && entity->instanceof(eTYPE_PLAYER))
{
shared_ptr<Player> player = dynamic_pointer_cast<Player>(entity);
if (player != nullptr)
{
std::wstring playerName = player->getName();
ensureRemoteNetworkPlayer(networkSmallId, &playerName, player->getXuid());
networkPlayer = g_NetworkManager.GetPlayerBySmallId(networkSmallId);
}
}
else
{
m_pendingRemotePlayerSmallIds[packet->m_entityId] = networkSmallId;
}
}
#endif
if(networkPlayer != nullptr && networkPlayer->IsHost())
{
@ -3568,7 +3677,7 @@ void ClientConnection::handlePlayerInfo(shared_ptr<PlayerInfoPacket> packet)
}
// 4J Stu - Repurposed this packet for player info that we want
app.UpdatePlayerInfo(packet->m_networkSmallId, packet->m_playerColourIndex, packet->m_playerPrivileges);
app.UpdatePlayerInfo(networkSmallId, packet->m_playerColourIndex, packet->m_playerPrivileges);
shared_ptr<Entity> entity = getEntity(packet->m_entityId);
if(entity != nullptr && entity->instanceof(eTYPE_PLAYER))
@ -4129,3 +4238,301 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr<
m_recievedTick = GetTickCount();
m_packet = packet;
}
#ifdef _WINDOWS64
void ClientConnection::handleAuthScheme(shared_ptr<AuthSchemePacket> packet)
{
app.DebugPrintf("Auth: Received AuthSchemePacket with %d scheme(s)\n", (int)packet->schemes.size());
newauthManager& authMgr = newauthManager::Get();
std::string serverId(packet->serverId.begin(), packet->serverId.end());
wstring chosenScheme;
bool isElyby = false;
bool offlineAvailable = false;
bool wantsOnlineAuth = false;
for (const auto& scheme : packet->schemes)
{
if (scheme == L"offline")
offlineAvailable = true;
}
if (!g_NetworkManager.IsHost())
{
auto accounts = authMgr.GetJavaAccounts();
int activeIdx = authMgr.GetActiveJavaAccountIndex();
if (activeIdx >= 0 && activeIdx < (int)accounts.size())
{
bool acctIsOffline = accounts[activeIdx].isOffline || accounts[activeIdx].authProvider == "offline";
bool acctIsElyby = (accounts[activeIdx].authProvider == "elyby");
for (const auto& scheme : packet->schemes)
{
if (scheme == L"mojang" && !acctIsOffline && !acctIsElyby)
{
chosenScheme = L"mojang";
wantsOnlineAuth = true;
break;
}
else if (scheme == L"elyby" && !acctIsOffline && acctIsElyby)
{
chosenScheme = L"elyby";
isElyby = true;
wantsOnlineAuth = true;
break;
}
}
}
}
if (chosenScheme.empty() && offlineAvailable)
{
chosenScheme = L"offline";
}
if (chosenScheme.empty())
{
app.DebugPrintf("Auth: No compatible scheme found, disconnecting\n");
app.SetDisconnectReason(DisconnectPacket::eDisconnect_AuthFailed);
app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ExitWorld, (void*)TRUE);
return;
}
std::string chosenStr(chosenScheme.begin(), chosenScheme.end());
app.DebugPrintf("Auth: Chose scheme '%s'\n", chosenStr.c_str());
if (chosenScheme == L"offline")
{
newauth::JavaSession session = authMgr.GetSlotSession((int)m_userIndex);
wstring uuid, username;
if (!session.uuid.empty())
{
uuid = wstring(session.uuid.begin(), session.uuid.end());
username = wstring(session.username.begin(), session.username.end());
}
else
{
auto accounts = authMgr.GetJavaAccounts();
int activeIdx = authMgr.GetActiveJavaAccountIndex();
if (activeIdx >= 0 && activeIdx < (int)accounts.size() && !accounts[activeIdx].uuid.empty())
{
const auto& acct = accounts[activeIdx];
uuid = wstring(acct.uuid.begin(), acct.uuid.end());
username = wstring(acct.username.begin(), acct.username.end());
}
else
{
username = minecraft->user->name;
std::string narrowName(username.begin(), username.end());
std::string offUuid = GameUUID::generateOffline(narrowName).toDashed();
uuid = wstring(offUuid.begin(), offUuid.end());
}
}
send(std::make_shared<AuthResponsePacket>(L"offline", uuid, username));
return;
}
newauth::JavaSession session = authMgr.GetSlotSession((int)m_userIndex);
if (wantsOnlineAuth && (session.accessToken.empty() || session.uuid.empty() || session.username.empty()))
{
app.DebugPrintf("Auth: Waiting for slot %d online session to become ready\n", (int)m_userIndex);
authMgr.WaitForSlotReady((int)m_userIndex, 5000);
session = authMgr.GetSlotSession((int)m_userIndex);
if ((session.accessToken.empty() || session.uuid.empty() || session.username.empty()) && m_userIndex != 0)
{
app.DebugPrintf("Auth: Slot %d session still empty, retrying with primary slot session\n", (int)m_userIndex);
authMgr.WaitForSlotReady(0, 5000);
session = authMgr.GetSlotSession(0);
}
}
if (session.accessToken.empty() || session.uuid.empty() || session.username.empty())
{
app.DebugPrintf("Auth: Online session unavailable for chosen scheme '%s'\n", chosenStr.c_str());
if (offlineAvailable)
{
app.DebugPrintf("Auth: Falling back to offline scheme because no online session is available\n");
wstring fbUsername = minecraft->user->name;
std::string narrowName(fbUsername.begin(), fbUsername.end());
std::string offUuid = GameUUID::generateOffline(narrowName).toDashed();
wstring fbUuid(offUuid.begin(), offUuid.end());
send(std::make_shared<AuthResponsePacket>(L"offline", fbUuid, fbUsername));
return;
}
app.SetDisconnectReason(DisconnectPacket::eDisconnect_AuthFailed);
app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ExitWorld, (void*)TRUE);
return;
}
std::string token = session.accessToken;
std::string uuid = session.uuid;
std::string undashedUuid = newauth::UndashUuid(uuid);
std::string joinError;
bool joinOk = false;
if (isElyby)
joinOk = newauth::ElybyJoinServer(token, undashedUuid, serverId, joinError);
else
joinOk = newauth::JoinServer(token, undashedUuid, serverId, joinError);
if (!joinOk)
{
app.DebugPrintf("Auth: JoinServer failed: %s\n", joinError.c_str());
if (offlineAvailable)
{
app.DebugPrintf("Auth: Falling back to offline scheme\n");
newauth::JavaSession fallbackSession = authMgr.GetSlotSession((int)m_userIndex);
wstring fbUuid, fbUsername;
if (!fallbackSession.uuid.empty())
{
fbUuid = wstring(fallbackSession.uuid.begin(), fallbackSession.uuid.end());
fbUsername = wstring(fallbackSession.username.begin(), fallbackSession.username.end());
}
else
{
fbUsername = minecraft->user->name;
std::string narrowName(fbUsername.begin(), fbUsername.end());
std::string offUuid = GameUUID::generateOffline(narrowName).toDashed();
fbUuid = wstring(offUuid.begin(), offUuid.end());
}
send(std::make_shared<AuthResponsePacket>(L"offline", fbUuid, fbUsername));
return;
}
app.SetDisconnectReason(DisconnectPacket::eDisconnect_AuthFailed);
app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ExitWorld, (void*)TRUE);
return;
}
std::string dashedUuid = newauth::DashUuid(uuid);
wstring wUuid(dashedUuid.begin(), dashedUuid.end());
wstring wUsername(session.username.begin(), session.username.end());
send(std::make_shared<AuthResponsePacket>(chosenScheme, wUuid, wUsername));
app.DebugPrintf("Auth: Sent AuthResponsePacket\n");
}
void ClientConnection::handleAuthResult(shared_ptr<AuthResultPacket> packet)
{
if (!packet->success)
{
std::wstring errMsg = packet->errorMessage;
app.DebugPrintf("Auth: Server rejected authentication: %ls\n", errMsg.c_str());
app.SetDisconnectReason(DisconnectPacket::eDisconnect_AuthFailed);
app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ExitWorld, (void*)TRUE);
return;
}
m_authAssignedUuid = packet->assignedUuid;
m_authAssignedUsername = packet->assignedUsername;
if (!m_authAssignedUsername.empty())
{
minecraft->user->name = m_authAssignedUsername;
extern char g_Win64Username[17];
extern wchar_t g_Win64UsernameW[17];
wcsncpy_s(g_Win64UsernameW, 17, m_authAssignedUsername.c_str(), _TRUNCATE);
WideCharToMultiByte(CP_ACP, 0, g_Win64UsernameW, -1, g_Win64Username, 17, nullptr, nullptr);
IQNetPlayer* localQNetPlayer = nullptr;
INetworkPlayer* localNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(m_userIndex);
if (localNetworkPlayer != nullptr)
{
localQNetPlayer = static_cast<NetworkPlayerXbox*>(localNetworkPlayer)->GetQNetPlayer();
}
if (localQNetPlayer == nullptr && m_userIndex >= 0 && m_userIndex < MINECRAFT_NET_MAX_PLAYERS)
{
localQNetPlayer = &IQNet::m_player[m_userIndex];
}
if (localQNetPlayer != nullptr)
{
wcsncpy_s(localQNetPlayer->m_gamertag, 32, m_authAssignedUsername.c_str(), _TRUNCATE);
}
}
app.DebugPrintf("Auth: Success! uuid=%ls username=%ls\n",
m_authAssignedUuid.c_str(), m_authAssignedUsername.c_str());
if (!packet->skinKey.empty() && !packet->skinData.empty())
{
if (newauth::ValidateSkinPng(packet->skinData.data(), packet->skinData.size()))
{
DWORD skinBytes = (DWORD)packet->skinData.size();
PBYTE skinCopy = new BYTE[skinBytes];
memcpy(skinCopy, packet->skinData.data(), skinBytes);
app.AddMemoryTextureFile(packet->skinKey,
skinCopy,
skinBytes);
m_authSkinKey = packet->skinKey;
app.DebugPrintf("Auth: Registered skin texture '%ls' (%d bytes)\n",
packet->skinKey.c_str(), (int)packet->skinData.size());
}
}
else if (!m_authAssignedUuid.empty())
{
std::string uuid(m_authAssignedUuid.begin(), m_authAssignedUuid.end());
newauthManager& authMgr = newauthManager::Get();
auto accounts = authMgr.GetJavaAccounts();
int activeIdx = authMgr.GetActiveJavaAccountIndex();
bool isElyby = (activeIdx >= 0 && activeIdx < (int)accounts.size()
&& accounts[activeIdx].authProvider == "elyby");
std::string skinKey = isElyby
? newauth::MakeElybySkinKey(uuid)
: newauth::MakeSkinKey(uuid);
std::string skinErr;
std::string skinUrl = isElyby
? newauth::ElybyFetchProfileSkinUrl(uuid, skinErr)
// honbestlky, this makes me reconsider elyby considering it could just be used to generate alts but whatever
: newauth::FetchProfileSkinUrl(uuid, skinErr);
if (!skinUrl.empty())
{
std::string fetchErr;
auto skinPng = newauth::FetchSkinPng(skinUrl, fetchErr);
if (!skinPng.empty() && newauth::ValidateSkinPng(skinPng.data(), skinPng.size()))
{
DWORD skinBytes = (DWORD)skinPng.size();
PBYTE skinCopy = new BYTE[skinBytes];
memcpy(skinCopy, skinPng.data(), skinBytes);
std::wstring wSkinKey(skinKey.begin(), skinKey.end());
app.AddMemoryTextureFile(wSkinKey, skinCopy, skinBytes);
m_authSkinKey = wSkinKey;
app.DebugPrintf("Auth: Client-side skin fetch for uuid=%s (%d bytes)\n",
uuid.c_str(), (int)skinPng.size());
}
}
}
sendLoginPacketAfterAuth();
}
void ClientConnection::sendLoginPacketAfterAuth()
{
PlayerUID offlineXUID = INVALID_XUID;
PlayerUID onlineXUID = INVALID_XUID;
if (ProfileManager.IsSignedInLive(m_userIndex))
{
ProfileManager.GetXUID(m_userIndex, &onlineXUID, true);
}
if (!ProfileManager.IsGuest(m_userIndex))
{
ProfileManager.GetXUID(m_userIndex, &offlineXUID, false);
}
BOOL allAllowed, friendsAllowed;
ProfileManager.AllowedPlayerCreatedContent(m_userIndex, true, &allAllowed, &friendsAllowed);
send(std::make_shared<LoginPacket>(
minecraft->user->name,
SharedConstants::NETWORK_PROTOCOL_VERSION,
offlineXUID, onlineXUID,
(allAllowed != TRUE && friendsAllowed == TRUE),
m_preLoginUgcPlayersVersion,
app.GetPlayerSkinId(m_userIndex),
app.GetPlayerCapeId(m_userIndex),
ProfileManager.IsGuest(m_userIndex)));
if (!g_NetworkManager.IsHost())
{
Minecraft::GetInstance()->progressRenderer->progressStagePercentage((eCCLoginSent * 100) / (eCCConnected));
}
app.DebugPrintf("Auth: Sent LoginPacket after auth\n");
}
#endif

View file

@ -1,4 +1,5 @@
#pragma once
#include <unordered_map>
#include <unordered_set>
#include "..\Minecraft.World\net.minecraft.network.h"
class Minecraft;
@ -6,6 +7,8 @@ class MultiPlayerLevel;
class SavedDataStorage;
class Socket;
class MultiplayerLocalPlayer;
class AuthSchemePacket;
class AuthResultPacket;
class ClientConnection : public PacketListener
{
@ -14,6 +17,7 @@ private:
{
eCCPreLoginSent = 0,
eCCPreLoginReceived,
eCCAuthWaiting,
eCCLoginSent,
eCCLoginReceived,
eCCConnected
@ -48,6 +52,7 @@ private:
std::unordered_set<int> m_trackedEntityIds;
std::unordered_set<int64_t> m_visibleChunks;
std::unordered_map<int, BYTE> m_pendingRemotePlayerSmallIds;
static int64_t chunkKey(int x, int z) { return ((int64_t)x << 32) | ((int64_t)z & 0xFFFFFFFF); }
@ -55,6 +60,9 @@ private:
bool shouldProcessForEntity(int entityId) const;
bool shouldProcessForPosition(int blockX, int blockZ) const;
bool anyOtherConnectionHasChunk(int x, int z) const;
#ifdef _WINDOWS64
bool ensureRemoteNetworkPlayer(BYTE smallId, const std::wstring *playerName, PlayerUID xuid);
#endif
public:
bool isTrackingEntity(int entityId) const { return m_trackedEntityIds.count(entityId) > 0; }
@ -164,6 +172,16 @@ public:
virtual void handleParticleEvent(shared_ptr<LevelParticlesPacket> packet);
virtual void handleUpdateAttributes(shared_ptr<UpdateAttributesPacket> packet);
// fuck ass shit to make the skins work waaah waaah waah
virtual void handleAuthScheme(shared_ptr<AuthSchemePacket> packet);
virtual void handleAuthResult(shared_ptr<AuthResultPacket> packet);
private:
void sendLoginPacketAfterAuth();
std::wstring m_authAssignedUuid;
std::wstring m_authAssignedUsername;
std::wstring m_authSkinKey;
DWORD m_preLoginUgcPlayersVersion = 0;
private:
// 4J: Entity link packet deferred
class DeferredEntityLinkPacket

Binary file not shown.

Binary file not shown.

View file

@ -19,6 +19,7 @@ CPlatformNetworkManagerStub *g_pPlatformNetworkManager;
void CPlatformNetworkManagerStub::NotifyPlayerJoined(IQNetPlayer *pQNetPlayer )
{
const char * pszDescription;
bool exposePlayer = Win64_ShouldExposeSessionPlayer(pQNetPlayer);
// 4J Stu - We create a fake socket for every where that we need an INBOUND queue of game data. Outbound
// is all handled by QNet so we don't need that. Therefore each client player has one, and the host has one
@ -79,7 +80,10 @@ void CPlatformNetworkManagerStub::NotifyPlayerJoined(IQNetPlayer *pQNetPlayer )
m_machineQNetPrimaryPlayers.push_back( pQNetPlayer );
}
}
g_NetworkManager.PlayerJoining( networkPlayer );
if (exposePlayer)
{
g_NetworkManager.PlayerJoining( networkPlayer );
}
if( createFakeSocket == true && !m_bHostChanged )
{
@ -101,10 +105,13 @@ void CPlatformNetworkManagerStub::NotifyPlayerJoined(IQNetPlayer *pQNetPlayer )
SystemFlagAddPlayer( networkPlayer );
}
for( int idx = 0; idx < XUSER_MAX_COUNT; ++idx)
if (exposePlayer)
{
if(playerChangedCallback[idx] != nullptr)
playerChangedCallback[idx]( playerChangedCallbackParam[idx], networkPlayer, false );
for( int idx = 0; idx < XUSER_MAX_COUNT; ++idx)
{
if(playerChangedCallback[idx] != nullptr)
playerChangedCallback[idx]( playerChangedCallbackParam[idx], networkPlayer, false );
}
}
if(m_pIQNet->GetState() == QNET_STATE_GAME_PLAY)
@ -142,12 +149,18 @@ void CPlatformNetworkManagerStub::NotifyPlayerLeaving(IQNetPlayer* pQNetPlayer)
SystemFlagRemovePlayer(networkPlayer);
}
g_NetworkManager.PlayerLeaving(networkPlayer);
for (int idx = 0; idx < XUSER_MAX_COUNT; ++idx)
if (Win64_ShouldExposeSessionPlayer(pQNetPlayer))
{
if (playerChangedCallback[idx] != nullptr)
playerChangedCallback[idx](playerChangedCallbackParam[idx], networkPlayer, true);
g_NetworkManager.PlayerLeaving(networkPlayer);
}
if (Win64_ShouldExposeSessionPlayer(pQNetPlayer))
{
for (int idx = 0; idx < XUSER_MAX_COUNT; ++idx)
{
if (playerChangedCallback[idx] != nullptr)
playerChangedCallback[idx](playerChangedCallbackParam[idx], networkPlayer, true);
}
}
removeNetworkPlayer(pQNetPlayer);
@ -544,6 +557,7 @@ int CPlatformNetworkManagerStub::JoinGame(FriendSessionInfo* searchResult, int l
m_bLeaveGameOnTick = false;
IQNet::s_isHosting = false;
m_pIQNet->ClientJoinGame();
Win64_SetJoinedDedicatedServer(searchResult->data.isDedicatedServer);
IQNet::m_player[0].m_smallId = 0;
IQNet::m_player[0].m_isRemote = true;
@ -865,6 +879,7 @@ void CPlatformNetworkManagerStub::SearchForGames()
info->data.subTexturePackId = lanSessions[i].subTexturePackId;
info->data.isReadyToJoin = lanSessions[i].isJoinable;
info->data.isJoinable = lanSessions[i].isJoinable;
info->data.isDedicatedServer = lanSessions[i].isDedicatedServer;
strncpy_s(info->data.hostIP, sizeof(info->data.hostIP), lanSessions[i].hostIP, _TRUNCATE);
info->data.hostPort = lanSessions[i].hostPort;
wcsncpy_s(info->data.hostName, XUSER_NAME_SIZE, lanSessions[i].hostName, _TRUNCATE);
@ -916,6 +931,7 @@ void CPlatformNetworkManagerStub::SearchForGames()
info->displayLabelViewableStartIndex = 0;
info->data.isReadyToJoin = true;
info->data.isJoinable = true;
info->data.isDedicatedServer = true;
strncpy_s(info->data.hostIP, sizeof(info->data.hostIP), ipBuf, _TRUNCATE);
info->data.hostPort = port;
info->sessionId = static_cast<uint64_t>(inet_addr(ipBuf)) | static_cast<uint64_t>(port) << 32;

View file

@ -70,6 +70,7 @@ typedef struct _GameSessionData
bool isReadyToJoin;
bool isJoinable;
bool isDedicatedServer;
char hostIP[64];
int hostPort;
@ -85,6 +86,7 @@ typedef struct _GameSessionData
subTexturePackId = 0;
isReadyToJoin = false;
isJoinable = true;
isDedicatedServer = false;
memset(hostIP, 0, sizeof(hostIP));
hostPort = 0;
memset(hostName, 0, sizeof(hostName));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,219 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string>
namespace NativeUI
{
enum : uint32_t
{
ALIGN_LEFT = 0x00u,
ALIGN_RIGHT = 0x01u,
ALIGN_CENTER_X = 0x02u,
ALIGN_CENTER_Y = 0x04u,
ALIGN_BOTTOM = 0x08u,
};
void BeginFrame();
void EndFrame();
void Shutdown();
bool GetMouseVirtual(float& outX, float& outY);
void DrawRect(float x, float y, float w, float h, uint32_t color);
void DrawRectFullscreen(uint32_t color);
void DrawRoundedRect(float x, float y, float w, float h,
float radius, uint32_t color);
void DrawRoundedBorder(float x, float y, float w, float h,
float radius, float thickness, uint32_t color);
void DrawBorder(float x, float y, float w, float h,
float thickness, uint32_t color);
void DrawLine(float x, float y, float length, float thickness,
uint32_t color);
void DrawLineV(float x, float y, float length, float thickness,
uint32_t color);
void DrawGradientRect(float x, float y, float w, float h,
uint32_t topColor, uint32_t bottomColor);
void DrawGradientRoundedRect(float x, float y, float w, float h,
float radius,
uint32_t topColor, uint32_t bottomColor);
void DrawDropShadow(float x, float y, float w, float h,
float offset = 4.0f, float spread = 6.0f,
uint32_t color = 0x60000000u);
void DrawPanel(float x, float y, float w, float h,
float radius = 8.0f,
uint32_t bgColor = 0xF0181818u,
uint32_t borderColor = 0xFF333333u,
float borderThick = 1.0f,
bool shadow = true);
void DrawDivider(float x, float y, float w,
uint32_t color = 0xFF333333u,
float thickness = 1.0f);
void DrawText(float x, float y, const wchar_t* text,
uint32_t color, float size = 14.0f,
uint32_t align = ALIGN_LEFT);
void DrawShadowText(float x, float y, const wchar_t* text,
uint32_t color, float size = 14.0f,
uint32_t align = ALIGN_LEFT);
float DrawTextWrapped(float x, float y, const wchar_t* text,
float maxWidth, uint32_t color,
float size = 14.0f, uint32_t align = ALIGN_LEFT);
void MeasureText(const wchar_t* text, float size,
float* outWidth, float* outHeight);
float LineHeight(float size);
void PushClipRect(float x, float y, float w, float h);
void PopClipRect();
void DrawButton(float x, float y, float w, float h,
const wchar_t* label, bool focused,
bool hovered = false, float labelSize = 16.0f);
void DrawTextBox(float x, float y, float w, float h,
uint32_t tint = 0xFFFFFFFFu);
void DrawLink(float x, float y, const wchar_t* text,
const char* url, bool focused, bool hovered = false,
float size = 14.0f, uint32_t align = ALIGN_LEFT,
float* outX = nullptr, float* outY = nullptr,
float* outW = nullptr, float* outH = nullptr);
void DrawProgressBar(float x, float y, float w, float h,
float progress,
uint32_t fillColor = 0xFF1A71D1u,
uint32_t trackColor = 0xFF333333u);
void DrawSpinner(float cx, float cy, float radius, int tick,
uint32_t color = 0xFFFFFFFFu);
void DrawCheckbox(float x, float y, float size,
bool checked, bool focused,
bool hovered = false);
void DrawSlider(float x, float y, float w, float h,
float value, bool focused,
bool hovered = false,
uint32_t fillColor = 0xFF1A71D1u,
uint32_t trackColor = 0xFF333333u);
void DrawTooltip(float x, float y, const wchar_t* text,
float size = 12.0f);
void OpenURL(const char* url);
int LoadTexture(const wchar_t* path);
int LoadTextureByName(int textureName);
int LoadTextureFromFileDirect(const char* filePath,
int* outWidth = nullptr, int* outHeight = nullptr);
int LoadTextureFromFile(const char* filePath,
int* outWidth = nullptr, int* outHeight = nullptr);
struct NineSlice
{
int tl, tm, tr;
int ml, mm, mr;
int bl, bm, br;
int cornerW, cornerH;
bool valid;
};
NineSlice LoadNineSlice(const char* basePath);
void DrawNineSlice(float x, float y, float w, float h,
const NineSlice& ns, uint32_t tint = 0xFFFFFFFFu);
struct ThreeSlice
{
int left, mid, right;
int capW, capH;
bool valid;
};
ThreeSlice LoadThreeSlice(const char* basePath);
void DrawThreeSlice(float x, float y, float w, float h,
const ThreeSlice& ts, uint32_t tint = 0xFFFFFFFFu);
void DrawTexture(float x, float y, float w, float h,
int textureId, uint32_t tint = 0xFFFFFFFFu);
void DrawTextureUV(float x, float y, float w, float h,
int textureId,
float u0, float v0, float u1, float v1,
uint32_t tint = 0xFFFFFFFFu);
void DrawTextureRounded(float x, float y, float w, float h,
int textureId, float radius,
uint32_t tint = 0xFFFFFFFFu);
void DrawTextureFit(float x, float y, float w, float h,
int textureId, int texW, int texH,
uint32_t tint = 0xFFFFFFFFu);
class FocusList
{
public:
struct Entry { int id; float x, y, w, h; };
void Clear() { m_entries.clear(); m_hoveredId = -1; }
void Add(int id, float x, float y, float w, float h);
int GetFocused() const;
int GetHovered() const { return m_hoveredId; }
bool IsFocused(int id) const { return GetFocused() == id; }
bool IsHovered(int id) const { return m_lastDevice == eDevice_Mouse && m_hoveredId == id; }
bool IsActive(int id) const { return IsFocused(id) || IsHovered(id); }
bool ShowFocus(int id) const
{
if (m_lastDevice == eDevice_Mouse)
return m_hoveredId >= 0 && m_hoveredId == id;
return IsFocused(id);
}
void MoveNext();
void MovePrev();
void SetFocus(int id);
bool HitTest(float mx, float my, int& outId) const;
bool UpdateHover(float mx, float my);
void ClearHover() { m_hoveredId = -1; }
int Count() const { return (int)m_entries.size(); }
void TickMouse();
bool IsMouseConsumed() const { return m_mouseConsumed; }
static constexpr int RESULT_NAVIGATED = -1;
static constexpr int RESULT_UNHANDLED = -2;
int HandleMenuKey(int key, int backId = 0,
float panelX = 0, float panelY = 0,
float panelW = 0, float panelH = 0);
enum ELastDevice { eDevice_None, eDevice_Gamepad, eDevice_Mouse };
ELastDevice GetLastDevice() const { return m_lastDevice; }
private:
std::vector<Entry> m_entries;
int m_focusIdx = 0;
int m_hoveredId = -1;
int m_lastHoveredId = -1;
bool m_mouseConsumed = false;
ELastDevice m_lastDevice = eDevice_None;
float m_lastMouseX = -1.0f;
float m_lastMouseY = -1.0f;
};
}

View file

@ -67,6 +67,9 @@
#include "UIScene_Intro.h"
#include "UIScene_SaveMessage.h"
#include "UIScene_MainMenu.h"
#ifndef MINECRAFT_SERVER_BUILD
#include "UIScene_MSAuth.h"
#endif
#include "UIScene_LoadMenu.h"
#include "UIScene_JoinMenu.h"
#include "UIScene_LoadOrJoinMenu.h"

View file

@ -114,6 +114,7 @@ enum EUIScene
#ifndef _XBOX
// Anything non-xbox should be added here. The ordering of scenes above is required for sentient reporting on xbox 360 to continue to be accurate
eUIScene_MSAuth,
eUIComponent_Panorama,
eUIComponent_Logo,
eUIComponent_DebugUIConsole,

View file

@ -367,6 +367,11 @@ bool UILayer::NavigateToScene(int iPad, EUIScene scene, void *initData)
case eUIScene_MainMenu:
newScene = new UIScene_MainMenu(iPad, initData, this);
break;
case eUIScene_MSAuth:
#ifndef MINECRAFT_SERVER_BUILD
newScene = new UIScene_MSAuth(iPad, initData, this);
#endif
break;
case eUIScene_LoadOrJoinMenu:
newScene = new UIScene_LoadOrJoinMenu(iPad, initData, this);
break;

View file

@ -1000,7 +1000,7 @@ void UIScene::gainFocus()
updateTooltips();
updateComponents();
if(!m_bFocussedOnce)
if(swf && !m_bFocussedOnce)
{
IggyDataValue result;
IggyDataValue value[1];

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,154 @@
#pragma once
/*
* account manager ui
* two views: account list and device code flow
* no swf, pure native overlay
*/
#include "UIScene.h"
#include "NativeUIRenderer.h"
#include "../../../newauth/include/newauthManager.h"
#include <atomic>
#include <vector>
#include <string>
#include <unordered_map>
// control ids for the focus list
// file scope so the static render helpers can use them
namespace MSAuthUI
{
enum EControls {
eBtn_Back = 0,
eBtn_AddAccount = 1,
eBtn_AddOffline = 2,
eBtn_OfflineConfirm = 3,
eBtn_ConfirmYes = 4,
eBtn_ConfirmNo = 5,
eBtn_OfflineTextBox = 6,
eBtn_AddElyby = 7,
eBtn_ElybyUsername = 8,
eBtn_ElybyPassword = 9,
eBtn_ElybySignIn = 10,
eBtn_ElybyCancel = 11,
eBtn_Elyby2FACode = 12,
eBtn_Elyby2FASubmit = 13,
eBtn_Elyby2FACancel = 14,
eBtn_RemoveBase = 50, // 50 + idx = remove btn
eAccountBase = 100, // 100 + idx = account row
eLink_URL = 200,
};
}
class UIScene_MSAuth : public UIScene
{
private:
// what were looking at
enum EView { eView_AccountList, eView_DeviceCode, eView_OfflineInput, eView_ElybyInput, eView_Elyby2FA };
EView m_view = eView_AccountList;
// shared flags that survive scene destruction (async callback safety)
struct AuthFlags {
std::atomic<bool> done { false };
std::atomic<bool> success{ false };
std::atomic<bool> need2FA{ false };
};
std::shared_ptr<AuthFlags> m_authFlags = std::make_shared<AuthFlags>();
int m_closeCountdown = 0;
int m_spinnerTick = 0;
ULONGLONG m_lastAnimTime = 0; // throttle animations to 30 FPS
NativeUI::NineSlice m_panel;
NativeUI::NineSlice m_recessPanel;
bool m_panelLoaded = false;
NativeUI::FocusList m_focus;
std::string m_cachedUri;
// accounts snapshot, refreshed every tick
std::vector<newauthManager::JavaAccountInfo> m_accounts;
int m_activeIdx = -1;
// scroll pos for the list
int m_scrollOffset = 0;
void StartAddAccount();
void SwitchToAccountList();
void SwitchToDeviceCode();
void SwitchToOfflineInput();
void SwitchToElybyInput();
void SwitchToElyby2FA();
void ConfirmOfflineAccount();
void SubmitElybyLogin();
void SubmitElyby2FA();
// offline name entry
std::string m_offlineUsername;
int m_offlineCursorBlink = 0;
bool m_textInputActive = false; // typing mode on/off
// ely.by creds
std::string m_elybyUsername;
std::string m_elybyPassword;
std::string m_elyby2FACode;
// (2fa flag in m_authFlags->need2FA cuz async)
int m_elybyActiveField = 0; // 0=user, 1=pass, 2=2fa
// which splitscreen slot this is for (0 = main, 1-3 = controllers)
int m_targetSlot = 0;
// eat input for a few ticks when opening so we dont accidentally
// process the button press that opened this damn scene
int m_inputGuardTicks = 6;
// remove confirm dialog (-1 = hidden, >= 0 = which account to nuke)
int m_pendingRemoveIdx = -1;
std::string m_pendingRemoveUuid; // saved uuid in case the index changes under us
public:
// shared buffer for virtual keyboard (public because the static
// callback func needs it, prevents use-after-free if scene dies
// while keyboard is up on another ui group)
struct PendingKeyboardResult {
std::string value;
std::atomic<bool> ready{false};
std::atomic<bool> valid{true};
};
// skin cache entry (public for static render helpers)
struct SkinEntry {
std::atomic<int> textureId{-2}; // -2=nope, -1=loading/failed, -3=file ready, >=0=loaded
std::string filePath;
};
private:
std::shared_ptr<PendingKeyboardResult> m_pendingKBResult;
// skin texture cache keyed by uuid
std::unordered_map<std::string, std::shared_ptr<SkinEntry>> m_skinCache;
void EnsureSkinLoaded(const std::string& uuid);
public:
UIScene_MSAuth(int iPad, void* initData, UILayer* parentLayer);
~UIScene_MSAuth();
virtual EUIScene getSceneType() override { return eUIScene_MSAuth; }
virtual wstring getMoviePath() override { return L""; }
virtual bool hidesLowerScenes() override { return true; }
virtual bool blocksInput() override { return true; }
virtual bool hasFocus(int iPad) override { return bHasFocus; }
virtual bool needsReloaded() override { return false; }
virtual void updateTooltips() override;
virtual void tick() override;
virtual void render(S32 width, S32 height,
C4JRender::eViewportType viewport) override;
virtual void handleInput(int iPad, int key, bool repeat,
bool pressed, bool released,
bool& handled) override;
virtual void handlePress(F64 controlId, F64 childId) override;
#ifdef _WINDOWS64
virtual bool handleMouseClick(F32 x, F32 y) override;
#endif
};

View file

@ -33,6 +33,9 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye
m_bIgnorePress=false;
#ifdef _WINDOWS64
m_bPendingPlayAfterAuth = false;
#endif
m_buttons[static_cast<int>(eControl_PlayGame)].init(IDS_PLAY_GAME,eControl_PlayGame);
#ifdef _XBOX_ONE
@ -43,6 +46,10 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye
m_buttons[static_cast<int>(eControl_Leaderboards)].init(IDS_LEADERBOARDS,eControl_Leaderboards);
m_buttons[static_cast<int>(eControl_Achievements)].init( (UIString)IDS_ACHIEVEMENTS,eControl_Achievements);
m_buttons[static_cast<int>(eControl_HelpAndOptions)].init(IDS_HELP_AND_OPTIONS,eControl_HelpAndOptions);
#ifdef _WINDOWS64
m_bTrialVersion=false;
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].init(L"Auth / Accounts",eControl_UnlockOrDLC);
#else
if(ProfileManager.IsFullVersion())
{
m_bTrialVersion=false;
@ -53,6 +60,7 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye
m_bTrialVersion=true;
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].init(IDS_UNLOCK_FULL_GAME,eControl_UnlockOrDLC);
}
#endif
#ifndef _DURANGO
m_buttons[static_cast<int>(eControl_Exit)].init(app.GetString(IDS_EXIT_GAME),eControl_Exit);
@ -153,6 +161,15 @@ void UIScene_MainMenu::handleGainFocus(bool navBack)
ui.ShowPlayerDisplayname(false);
m_bIgnorePress=false;
#ifdef _WINDOWS64
if (navBack && m_bPendingPlayAfterAuth)
{
m_bPendingPlayAfterAuth = false;
proceedToScene(ProfileManager.GetPrimaryPad(), eUIScene_LoadOrJoinMenu);
return;
}
#endif
if (eNavigateWhenReady >= 0)
{
return;
@ -181,8 +198,10 @@ void UIScene_MainMenu::handleGainFocus(bool navBack)
if(navBack && ProfileManager.IsFullVersion())
{
#ifndef _WINDOWS64
// Replace the Unlock Full Game with Downloadable Content
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].setLabel(IDS_DOWNLOADABLECONTENT);
#endif
}
#if TO_BE_IMPLEMENTED
@ -356,8 +375,12 @@ void UIScene_MainMenu::handlePress(F64 controlId, F64 childId)
//CD - Added for audio
ui.PlayUISFX(eSFX_Press);
#ifdef _WINDOWS64
ui.NavigateToScene(primaryPad, eUIScene_MSAuth);
#else
m_eAction=eAction_RunUnlockOrDLC;
signInReturnedFunc = &UIScene_MainMenu::UnlockFullGame_SignInReturned;
#endif
break;
case eControl_Exit:
//CD - Added for audio
@ -1419,7 +1442,22 @@ void UIScene_MainMenu::RunPlayGame(int iPad)
#ifdef _XBOX_ONE
ui.ShowPlayerDisplayname(true);
#endif
#ifdef _WINDOWS64
{
const auto& slot = newauthManager::Get().GetSlot(0);
if (slot.accountIndex < 0 && newauthManager::Get().GetJavaAccounts().empty())
{
m_bPendingPlayAfterAuth = true;
ui.NavigateToScene(ProfileManager.GetPrimaryPad(), eUIScene_MSAuth);
}
else
{
proceedToScene(ProfileManager.GetPrimaryPad(), eUIScene_LoadOrJoinMenu);
}
}
#else
proceedToScene(ProfileManager.GetPrimaryPad(), eUIScene_LoadOrJoinMenu);
#endif
#endif
}
else
@ -2129,7 +2167,9 @@ void UIScene_MainMenu::LoadTrial(void)
void UIScene_MainMenu::handleUnlockFullVersion()
{
#ifndef _WINDOWS64
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].setLabel(IDS_DOWNLOADABLECONTENT,true);
#endif
}

View file

@ -1,6 +1,9 @@
#pragma once
#include "UIScene.h"
#ifdef _WINDOWS64
#include "../../../newauth/include/newauthManager.h"
#endif
class UIScene_MainMenu : public UIScene
{
@ -56,6 +59,9 @@ private:
#ifdef _XBOX_ONE
bool m_bWaitingForDLCInfo;
#endif
#ifdef _WINDOWS64
bool m_bPendingPlayAfterAuth;
#endif
float m_fScreenWidth,m_fScreenHeight;
float m_fRawWidth,m_fRawHeight;

View file

@ -238,9 +238,32 @@ ULONG_PTR IQNetPlayer::GetCustomDataValue() {
IQNetPlayer IQNet::m_player[MINECRAFT_NET_MAX_PLAYERS];
DWORD IQNet::s_playerCount = 1;
bool IQNet::s_isHosting = true;
static bool s_joinedDedicatedServer = false;
// tldr checking if its a dedicated server or a singleplayer
QNET_STATE _iQNetStubState = QNET_STATE_IDLE;
void Win64_SetJoinedDedicatedServer(bool isDedicated)
{
s_joinedDedicatedServer = isDedicated;
}
bool Win64_IsJoinedDedicatedServer()
{
return s_joinedDedicatedServer;
}
bool Win64_ShouldExposeSessionPlayer(IQNetPlayer *player)
{
if (player == nullptr)
return false;
if (!IQNet::s_isHosting && s_joinedDedicatedServer && player->m_isRemote && player->m_isHostPlayer)
return false;
return true;
}
void Win64_SetupRemoteQNetPlayer(IQNetPlayer * player, BYTE smallId, bool isHost, bool isLocal)
{
player->m_smallId = smallId;
@ -317,7 +340,7 @@ IQNetPlayer* IQNet::GetPlayerByIndex(DWORD dwPlayerIndex)
DWORD found = 0;
for (DWORD i = 0; i < s_playerCount; i++)
{
if (Win64_IsActivePlayer(&m_player[i], i))
if (Win64_IsActivePlayer(&m_player[i], i) && Win64_ShouldExposeSessionPlayer(&m_player[i]))
{
if (found == dwPlayerIndex) return &m_player[i];
found++;
@ -351,7 +374,7 @@ DWORD IQNet::GetPlayerCount()
DWORD count = 0;
for (DWORD i = 0; i < s_playerCount; i++)
{
if (Win64_IsActivePlayer(&m_player[i], i)) count++;
if (Win64_IsActivePlayer(&m_player[i], i) && Win64_ShouldExposeSessionPlayer(&m_player[i])) count++;
}
return count;
}
@ -362,13 +385,15 @@ void IQNet::HostGame()
{
_iQNetStubState = QNET_STATE_SESSION_STARTING;
s_isHosting = true;
// Host slot keeps legacy XUID so old host player data remains addressable.
s_joinedDedicatedServer = false;
// linking xuid to new uuid, lazy but it works.
m_player[0].m_resolvedXuid = Win64Xuid::GetLegacyEmbeddedHostXuid();
}
void IQNet::ClientJoinGame()
{
_iQNetStubState = QNET_STATE_SESSION_STARTING;
s_isHosting = false;
s_joinedDedicatedServer = false;
for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; i++)
{
@ -384,6 +409,7 @@ void IQNet::EndGame()
{
_iQNetStubState = QNET_STATE_IDLE;
s_isHosting = false;
s_joinedDedicatedServer = false;
s_playerCount = 1;
for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; i++)
{

View file

@ -93,6 +93,15 @@ public:
void setEnforceUnicodeSheet(bool enforceUnicodeSheet);
void setBidirectional(bool bidirectional);
int getCols() const { return m_cols; }
int getRows() const { return m_rows; }
int getCharWidth() const { return m_charWidth; }
int getCharHeight() const { return m_charHeight; }
ResourceLocation* getTextureLocation() const { return m_textureLocation; }
Textures* getTextures() const { return textures; }
wchar_t mapChar(wchar_t c) { return (wchar_t)MapCharacter(c); }
int getCharPixelWidth(wchar_t c) const { return charWidths[(int)(unsigned short)c]; }
// 4J-PB - check for invalid player name - Japanese local name
bool AllCharactersValid(const wstring &str);
};

View file

@ -33,6 +33,7 @@
#include "..\Minecraft.World\net.minecraft.world.damagesource.h"
#ifdef _WINDOWS64
#include "Windows64\Network\WinsockNetLayer.h"
#include "..\newauth\include\newauthManager.h"
#endif
#include <sstream>
#ifdef SPLIT_SAVES
@ -644,10 +645,33 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW
// 4J - Unused
//localIp = settings->getString(L"server-ip", L"");
//onlineMode = settings->getBoolean(L"online-mode", true);
//motd = settings->getString(L"motd", L"A Minecraft Server");
//motd.replace('<27>', '$');
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
wstring wProvider = settings->getString(L"auth-provider", L"mojang");
authProvider = std::string(wProvider.begin(), wProvider.end());
if (authProvider != "elyby" && authProvider != "offline")
authProvider = "mojang";
}
#elif defined(_WINDOWS64)
// // singleplayer worlds should always be in offline mode right?
// // fuck ts makes me upset considering LAN worlds too
// yeah nvm that shit works now
{
newauthManager& authMgr = newauthManager::Get();
auto accounts = authMgr.GetJavaAccounts();
int activeIdx = authMgr.GetActiveJavaAccountIndex();
if (activeIdx >= 0 && activeIdx < (int)accounts.size() && !accounts[activeIdx].authProvider.empty())
authProvider = accounts[activeIdx].authProvider;
else
authProvider = "mojang";
}
#else
authProvider = "mojang";
#endif
setAnimals(GetDedicatedServerBool(settings, L"spawn-animals", true));
setNpcsEnabled(GetDedicatedServerBool(settings, L"spawn-npcs", true));
setPvpAllowed(app.GetGameHostOption( eGameHostOption_PvP )>0?true:false);
@ -680,11 +704,11 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW
return false;
}
if (!onlineMode) {
if (authProvider == "offline") {
logger.warning("**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!");
logger.warning("The server will make no attempt to authenticate usernames. Beware.");
logger.warning("While this makes the game possible to play without internet access, it also opens up the ability for hackers to connect with any username they choose.");
logger.warning("To change this, set \"online-mode\" to \"true\" in the server.settings file.");
logger.warning("To change this, set \"auth-provider\" to \"mojang\" or \"elyby\" in server.properties.");
}
#endif
setPlayers(new PlayerList(this));

View file

@ -112,7 +112,7 @@ private:
vector<ConsoleInput *> consoleInput; // 4J - was synchronizedList - TODO - investigate
CRITICAL_SECTION m_consoleInputCS;
public:
bool onlineMode;
std::string authProvider; // mojang, elyby, or offline. not recommended.
bool animals;
bool npcs;
bool pvp;

View file

@ -13,7 +13,15 @@
#include "..\Minecraft.World\net.minecraft.world.level.storage.h"
#include "..\Minecraft.World\net.minecraft.world.item.h"
#include "..\Minecraft.World\SharedConstants.h"
#include "..\Minecraft.World\GameUUID.h"
#include "..\Minecraft.World\AuthSchemePacket.h"
#include "..\Minecraft.World\AuthResponsePacket.h"
#include "..\Minecraft.World\AuthResultPacket.h"
#include "Settings.h"
#include <thread>
#ifdef _WINDOWS64
#include "..\newauth\include\newauth.h"
#endif
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\ServerLogManager.h"
#include "..\Minecraft.Server\Access\Access.h"
@ -63,6 +71,8 @@ PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, co
PendingConnection::~PendingConnection()
{
if (m_authVerifyResult)
m_authVerifyResult->cancelled.store(true, std::memory_order_release);
delete connection;
}
@ -73,6 +83,71 @@ void PendingConnection::tick()
this->handleAcceptedLogin(acceptedLogin);
acceptedLogin = nullptr;
}
if (m_authState == eAuth_Verifying && m_authVerifyResult &&
m_authVerifyResult->ready.load(std::memory_order_acquire))
{
auto verifyResult = m_authVerifyResult;
bool authSuccess = false;
std::string uuid;
std::string uname;
std::vector<uint8_t> skinData;
std::string skinUuid;
std::string errMsg;
{
std::lock_guard<std::mutex> lock(verifyResult->mutex);
authSuccess = verifyResult->success;
if (authSuccess)
{
uuid = verifyResult->uuid;
uname = verifyResult->username;
skinData = std::move(verifyResult->skinData);
skinUuid = verifyResult->skinUuid;
}
else
{
errMsg = verifyResult->errorMessage;
}
}
m_authVerifyResult.reset();
if (authSuccess)
{
if (m_authAssignedUuid.empty())
{
m_authAssignedUuid = std::wstring(uuid.begin(), uuid.end());
m_authAssignedUsername = std::wstring(uname.begin(), uname.end());
}
std::string skinKeyUuid = skinUuid.empty() ? uuid : skinUuid;
std::string skinKey;
#ifdef _WINDOWS64
if (server->authProvider == "elyby")
skinKey = newauth::MakeElybySkinKey(skinKeyUuid);
else
skinKey = newauth::MakeSkinKey(skinKeyUuid);
#endif
m_authSkinKey = std::wstring(skinKey.begin(), skinKey.end());
m_authSkinData = std::move(skinData);
name = m_authAssignedUsername;
send(std::make_shared<AuthResultPacket>(true, m_authAssignedUuid,
m_authAssignedUsername, L"", m_authSkinKey, m_authSkinData));
m_authState = eAuth_WaitingAck;
app.DebugPrintf("Auth: Verification succeeded for %ls\n", m_authAssignedUsername.c_str());
}
else
{
std::wstring wErrMsg(errMsg.begin(), errMsg.end());
app.DebugPrintf("Auth: Verification failed: %s\n", errMsg.c_str());
send(std::make_shared<AuthResultPacket>(false, L"", L"", wErrMsg));
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
}
}
if (_tick++ == MAX_TICKS_BEFORE_LOGIN)
{
disconnect(DisconnectPacket::eDisconnect_LoginTooLong);
@ -150,8 +225,8 @@ void PendingConnection::sendPreLoginResponse()
}
}
#if 0
if (false)// server->onlineMode) // 4J - removed
#if 0
if (false) // 4J - removed
{
loginKey = L"TOIMPLEMENT"; // 4J - todo Long.toHexString(random.nextLong());
connection->send( shared_ptr<PreLoginPacket>( new PreLoginPacket(loginKey, ugcXuids, ugcXuidCount, ugcFriendsOnlyBits, server->m_ugcPlayersVersion, szUniqueMapName,app.GetGameHostOption(eGameHostOption_All),hostIndex) ) );
@ -163,10 +238,259 @@ void PendingConnection::sendPreLoginResponse()
BYTE cappedHostIndex = (hostIndex >= 255u) ? 254 : static_cast<BYTE>(hostIndex);
connection->send(std::make_shared<PreLoginPacket>(L"-", ugcXuids, cappedCount, ugcFriendsOnlyBits, server->m_ugcPlayersVersion, szUniqueMapName, app.GetGameHostOption(eGameHostOption_All), cappedHostIndex, server->m_texturePackId));
}
#ifdef _WINDOWS64
sendAuthScheme();
#endif
}
void PendingConnection::sendAuthScheme()
{
std::string hexId = GameUUID::generateV4().toUndashed();
if (hexId.size() > 20) hexId.resize(20);
m_authServerId = hexId;
vector<wstring> schemes;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (server->authProvider == "elyby")
{
schemes.push_back(L"elyby");
}
else if (server->authProvider == "offline")
{
schemes.push_back(L"offline");
}
else
{
schemes.push_back(L"mojang");
}
#else
{
schemes.push_back(L"mojang");
schemes.push_back(L"elyby");
schemes.push_back(L"offline");
}
#endif
wstring wServerId(m_authServerId.begin(), m_authServerId.end());
send(std::make_shared<AuthSchemePacket>(schemes, wServerId));
m_authState = eAuth_WaitingResponse;
app.DebugPrintf("Auth: Sent AuthSchemePacket with %d scheme(s), serverId=%s\n",
(int)schemes.size(), m_authServerId.c_str());
}
void PendingConnection::handleAuthResponse(shared_ptr<AuthResponsePacket> packet)
{
if (m_authState != eAuth_WaitingResponse)
{
app.DebugPrintf("Auth: Received AuthResponsePacket in wrong state %d\n", m_authState);
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
std::string chosenScheme(packet->chosenScheme.begin(), packet->chosenScheme.end());
std::string clientUsername(packet->username.begin(), packet->username.end());
std::string clientUuid(packet->mojangUuid.begin(), packet->mojangUuid.end());
app.DebugPrintf("Auth: Client chose scheme '%s', username='%s'\n",
chosenScheme.c_str(), clientUsername.c_str());
if (chosenScheme == "offline")
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
bool offlineAllowed = (server->authProvider == "offline");
#else
bool offlineAllowed = true;
#endif
if (!offlineAllowed)
{
send(std::make_shared<AuthResultPacket>(false, L"", L"",
L"Server does not allow offline authentication"));
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
#if !defined(MINECRAFT_SERVER_BUILD)
// assumed that offline/singleplayer or lan worlds will auto-trust uuids.
std::string uuid = clientUuid;
if (uuid.empty())
{
GameUUID offlineUuid = GameUUID::generateOffline(clientUsername);
uuid = offlineUuid.toDashed();
}
m_authAssignedUuid = std::wstring(uuid.begin(), uuid.end());
m_authAssignedUsername = std::wstring(clientUsername.begin(), clientUsername.end());
name = m_authAssignedUsername;
send(std::make_shared<AuthResultPacket>(true, m_authAssignedUuid,
m_authAssignedUsername, L""));
m_authState = eAuth_WaitingAck;
app.DebugPrintf("Auth: Offline auth accepted (single-player), uuid=%s\n", uuid.c_str());
return;
#else
// for ded server to regenerate uuid serverside to prevent uuid forgery
GameUUID offlineUuid = GameUUID::generateOffline(clientUsername);
std::string dashedUuid = offlineUuid.toDashed();
m_authAssignedUuid = std::wstring(dashedUuid.begin(), dashedUuid.end());
m_authAssignedUsername = std::wstring(clientUsername.begin(), clientUsername.end());
name = m_authAssignedUsername;
if (!clientUuid.empty())
{
m_authState = eAuth_Verifying;
m_authVerifyResult = std::make_shared<AuthVerifyResult>();
m_authVerifyResult->success = true;
m_authVerifyResult->username = clientUsername;
m_authVerifyResult->uuid = dashedUuid;
m_authVerifyResult->skinUuid = clientUuid; // just use the user's uuid if auth'd with mojang or ely.by. ez.
std::shared_ptr<AuthVerifyResult> result = m_authVerifyResult;
std::string fetchUuid = clientUuid;
std::thread([result, fetchUuid]() {
try {
if (result->cancelled.load(std::memory_order_acquire))
return;
std::string fetchErr;
std::string skinUrl = newauth::FetchProfileSkinUrl(fetchUuid, fetchErr);
if (!skinUrl.empty())
{
auto skinPng = newauth::FetchSkinPng(skinUrl, fetchErr);
if (!skinPng.empty() && newauth::ValidateSkinPng(skinPng.data(), skinPng.size()))
{
std::lock_guard<std::mutex> lock(result->mutex);
result->skinUrl = skinUrl;
result->skinData = std::move(skinPng);
}
}
result->ready.store(true, std::memory_order_release);
} catch (...) {
result->ready.store(true, std::memory_order_release);
}
}).detach();
return;
}
send(std::make_shared<AuthResultPacket>(true, m_authAssignedUuid,
m_authAssignedUsername, L""));
m_authState = eAuth_WaitingAck;
app.DebugPrintf("Auth: Offline auth success, uuid=%s\n", dashedUuid.c_str());
return;
#endif
}
if (chosenScheme == "mojang" || chosenScheme == "elyby")
{
bool isElyby = (chosenScheme == "elyby");
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (server->authProvider == "offline")
{
send(std::make_shared<AuthResultPacket>(false, L"", L"",
L"Server does not allow online authentication"));
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
bool wantElyby = (server->authProvider == "elyby");
if (isElyby != wantElyby)
{
app.DebugPrintf("Auth: Client chose '%s' but server expects '%s'\n",
chosenScheme.c_str(), server->authProvider.c_str());
send(std::make_shared<AuthResultPacket>(false, L"", L"",
L"Auth scheme mismatch"));
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
#endif
m_authState = eAuth_Verifying;
m_authVerifyResult = std::make_shared<AuthVerifyResult>();
std::string serverId = m_authServerId;
std::string username = clientUsername;
std::shared_ptr<AuthVerifyResult> result = m_authVerifyResult;
#ifdef _WINDOWS64
std::thread([result, username, serverId, isElyby]() {
try {
if (result->cancelled.load(std::memory_order_acquire))
return;
std::string error;
newauth::HasJoinedResult hjResult;
if (isElyby)
hjResult = newauth::ElybyHasJoined(username, serverId, error);
else
hjResult = newauth::HasJoined(username, serverId, error);
if (result->cancelled.load(std::memory_order_acquire))
return;
std::vector<uint8_t> skin;
if (hjResult.success && !hjResult.skinUrl.empty())
{
std::string skinErr;
skin = newauth::FetchSkinPng(hjResult.skinUrl, skinErr);
if (!skin.empty() && !newauth::ValidateSkinPng(skin.data(), skin.size()))
skin.clear();
}
{
std::lock_guard<std::mutex> lock(result->mutex);
if (hjResult.success)
{
result->success = true;
result->username = hjResult.username;
result->uuid = hjResult.uuid;
result->skinUrl = hjResult.skinUrl;
if (!skin.empty())
result->skinData = std::move(skin);
}
else
{
result->success = false;
result->errorMessage = error.empty() ? "Authentication failed" : error;
}
}
result->ready.store(true, std::memory_order_release);
} catch (...) {
{
std::lock_guard<std::mutex> lock(result->mutex);
result->success = false;
result->errorMessage = "Internal auth error";
}
result->ready.store(true, std::memory_order_release);
}
}).detach();
#endif
return;
}
// only really a problem if the client is modified. You Know Who You
app.DebugPrintf("Auth: Unknown scheme '%s'\n", chosenScheme.c_str());
send(std::make_shared<AuthResultPacket>(false, L"", L"", L"Unknown auth scheme"));
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
}
void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
{
#ifdef _WINDOWS64
if (m_authState != eAuth_WaitingAck && m_authState != eAuth_Done)
{
app.DebugPrintf("Auth: LoginPacket received in state %d, rejecting\n", m_authState);
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
if (m_authState == eAuth_WaitingAck)
{
m_authState = eAuth_Done;
}
#endif
// printf("Server: handleLogin\n");
//name = packet->userName;
if (packet->clientVersion != SharedConstants::NETWORK_PROTOCOL_VERSION)
@ -183,11 +507,8 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
return;
}
//if (true)// 4J removed !server->onlineMode)
bool sentDisconnect = false;
// Use the same Xuid choice as handleAcceptedLogin (offline first, online fallback).
//
PlayerUID loginXuid = packet->m_offlineXuid;
if (loginXuid == INVALID_XUID) loginXuid = packet->m_onlineXuid;
@ -336,6 +657,22 @@ void PendingConnection::handleAcceptedLogin(shared_ptr<LoginPacket> packet)
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name);
#endif
#ifdef _WINDOWS64
// Apply auth-verified skin to the player so that placeNewPlayer can
// distribute it to all clients via the existing texture mechanism.
if (!m_authSkinKey.empty() && !m_authSkinData.empty())
{
DWORD skinBytes = (DWORD)m_authSkinData.size();
PBYTE skinCopy = new BYTE[skinBytes];
memcpy(skinCopy, m_authSkinData.data(), skinBytes);
app.AddMemoryTextureFile(m_authSkinKey, skinCopy, skinBytes);
playerEntity->customTextureUrl = m_authSkinKey;
app.DebugPrintf("Auth: Registered server-side skin '%ls' (%d bytes) for %ls\n",
m_authSkinKey.c_str(), (int)skinBytes, name.c_str());
}
#endif
server->getPlayers()->placeNewPlayer(connection, playerEntity, packet);
connection = nullptr; // We've moved responsibility for this over to the new PlayerConnection, nullptr so we don't delete our reference to it here in our dtor
}

View file

@ -1,14 +1,43 @@
#pragma once
#include <atomic>
#include <mutex>
#include <memory>
#include "..\Minecraft.World\PacketListener.h"
class MinecraftServer;
class Socket;
class LoginPacket;
class AuthSchemePacket;
class AuthResponsePacket;
class Connection;
class Random;
using namespace std;
class PendingConnection : public PacketListener
{
public:
enum eAuthState
{
eAuth_None = 0,
eAuth_WaitingResponse,
eAuth_Verifying,
eAuth_WaitingAck,
eAuth_Done
};
struct AuthVerifyResult
{
std::atomic<bool> ready{false};
std::atomic<bool> cancelled{false};
std::mutex mutex;
bool success = false;
std::string username;
std::string uuid;
std::string skinUuid; // UUID to use for skin key (may differ from uuid for offline auth)
std::string skinUrl;
std::vector<uint8_t> skinData;
std::string errorMessage;
};
private:
static const int FAKE_LAG = 0;
static const int MAX_TICKS_BEFORE_LOGIN = 20 * 30;
@ -27,6 +56,14 @@ private:
shared_ptr<LoginPacket> acceptedLogin;
wstring loginKey;
eAuthState m_authState = eAuth_None;
std::string m_authServerId;
std::wstring m_authAssignedUuid;
std::wstring m_authAssignedUsername;
std::wstring m_authSkinKey;
std::vector<uint8_t> m_authSkinData;
std::shared_ptr<AuthVerifyResult> m_authVerifyResult;
public:
PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id);
~PendingConnection();
@ -35,6 +72,7 @@ public:
virtual void handlePreLogin(shared_ptr<PreLoginPacket> packet);
virtual void handleLogin(shared_ptr<LoginPacket> packet);
virtual void handleAcceptedLogin(shared_ptr<LoginPacket> packet);
virtual void handleAuthResponse(shared_ptr<AuthResponsePacket> packet);
virtual void onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects);
virtual void handleGetInfo(shared_ptr<GetInfoPacket> packet);
virtual void handleKeepAlive(shared_ptr<KeepAlivePacket> packet);
@ -46,4 +84,5 @@ public:
private:
void sendPreLoginResponse();
void sendAuthScheme();
};

View file

@ -290,6 +290,9 @@ public:
public:
void clearLastBoundId();
// i guess the nativeui wasn't a terrible idea. it just needed to be written right.
int loadTextureByPath(const wstring& path) { return loadTexture(TN_COUNT, path); }
private:
int loadTexture(TEXTURE_NAME texId, const wstring& resourceName);
public:

View file

@ -1317,6 +1317,7 @@ bool WinsockNetLayer::StartAdvertising(int gamePort, const wchar_t* hostName, un
s_advertiseData.texturePackParentId = texPackId;
s_advertiseData.subTexturePackId = subTexId;
s_advertiseData.isJoinable = 0;
s_advertiseData.isDedicatedServer = g_Win64DedicatedServer ? 1 : 0;
s_hostGamePort = gamePort;
LeaveCriticalSection(&s_advertiseLock);
@ -1519,6 +1520,7 @@ DWORD WINAPI WinsockNetLayer::DiscoveryThreadProc(LPVOID param)
s_discoveredSessions[i].texturePackParentId = broadcast->texturePackParentId;
s_discoveredSessions[i].subTexturePackId = broadcast->subTexturePackId;
s_discoveredSessions[i].isJoinable = (broadcast->isJoinable != 0);
s_discoveredSessions[i].isDedicatedServer = (broadcast->isDedicatedServer != 0);
s_discoveredSessions[i].lastSeenTick = now;
found = true;
break;
@ -1539,6 +1541,7 @@ DWORD WINAPI WinsockNetLayer::DiscoveryThreadProc(LPVOID param)
session.texturePackParentId = broadcast->texturePackParentId;
session.subTexturePackId = broadcast->subTexturePackId;
session.isJoinable = (broadcast->isJoinable != 0);
session.isDedicatedServer = (broadcast->isDedicatedServer != 0);
session.lastSeenTick = now;
s_discoveredSessions.push_back(session);

View file

@ -36,6 +36,7 @@ struct Win64LANBroadcast
DWORD texturePackParentId;
BYTE subTexturePackId;
BYTE isJoinable;
BYTE isDedicatedServer;
};
#pragma pack(pop)
@ -51,6 +52,7 @@ struct Win64LANSession
unsigned int texturePackParentId;
unsigned char subTexturePackId;
bool isJoinable;
bool isDedicatedServer;
DWORD lastSeenTick;
};

View file

@ -49,6 +49,7 @@
#include "Network\WinsockNetLayer.h"
#include "Windows64_Xuid.h"
#include "Common/UI/UI.h"
#include "..\\..\\newauth\\include\\newauthManager.h"
// Forward-declare the internal Renderer class and its global instance from 4J_Render_PC_d.lib.
// C4JRender (RenderManager) is a stateless wrapper — all D3D state lives in InternalRenderManager.
@ -1276,6 +1277,8 @@ static Minecraft* InitialiseMinecraftRuntime()
ProfileManager.SetDebugFullOverride(true);
newauthManager::Get().TryRestoreActiveJavaAccount();
Tesselator::CreateNewThreadStorage(1024 * 1024);
AABB::CreateNewThreadStorage();
Vec3::CreateNewThreadStorage();

View file

@ -28,6 +28,8 @@ set(_MINECRAFT_CLIENT_WINDOWS_COMMON_UI
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIGroup.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UILayer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UILayer.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/NativeUIRenderer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/NativeUIRenderer.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UITTFFont.cpp"
@ -167,6 +169,8 @@ set(_MINECRAFT_CLIENT_WINDOWS_COMMON_UI_SCENES_FRONTEND_MENU_SCREENS
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LoadOrJoinMenu.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_MainMenu.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_MainMenu.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_MSAuth.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_MSAuth.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_NewUpdateMessage.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_NewUpdateMessage.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_SaveMessage.cpp"

View file

@ -37,6 +37,7 @@ set_target_properties(Minecraft.Server PROPERTIES
target_link_libraries(Minecraft.Server PRIVATE
Minecraft.World
newauth
d3d11
d3dcompiler
XInput9_1_0

View file

@ -80,7 +80,8 @@ static const ServerPropertyDefault kServerPropertyDefaults[] =
{ "spawn-monsters", "true" },
{ "spawn-npcs", "true" },
{ "tnt", "true" },
{ "trust-players", "true" }
{ "trust-players", "true" },
{ "auth-provider", "mojang" }
};
static std::string BoolToString(bool value)
@ -783,6 +784,11 @@ ServerPropertiesConfig LoadServerPropertiesConfig()
merged[it->first] = it->second;
}
if (merged.erase("online-mode") > 0)
{
shouldWrite = true;
}
std::string worldName = TrimAscii(merged["level-name"]);
if (worldName.empty())
{
@ -904,6 +910,8 @@ bool SaveServerPropertiesConfig(const ServerPropertiesConfig &config)
}
}
merged.erase("online-mode");
std::string worldName = TrimAscii(WideToUtf8(config.worldName));
if (worldName.empty())
{

View file

@ -0,0 +1,54 @@
#include "stdafx.h"
#include "InputOutputStream.h"
#include "PacketListener.h"
#include "AuthResponsePacket.h"
AuthResponsePacket::AuthResponsePacket()
{
}
AuthResponsePacket::AuthResponsePacket(const wstring& chosenScheme, const wstring& mojangUuid, const wstring& username)
{
this->chosenScheme = chosenScheme;
this->mojangUuid = mojangUuid;
this->username = username;
}
void AuthResponsePacket::read(DataInputStream *dis)
{
chosenScheme = readUtf(dis, 32);
mojangUuid = readUtf(dis, 64);
username = readUtf(dis, 16);
if (chosenScheme != L"mojang" && chosenScheme != L"offline" && chosenScheme != L"elyby")
chosenScheme = L"";
// only allow normal mc username characters
for (wchar_t c : username)
{
if (!((c >= L'a' && c <= L'z') || (c >= L'A' && c <= L'Z') ||
(c >= L'0' && c <= L'9') || c == L'_'))
{
username = L"";
break;
}
}
}
void AuthResponsePacket::write(DataOutputStream *dos)
{
writeUtf(chosenScheme, dos);
writeUtf(mojangUuid, dos);
writeUtf(username, dos);
}
void AuthResponsePacket::handle(PacketListener *listener)
{
listener->handleAuthResponse(shared_from_this());
}
int AuthResponsePacket::getEstimatedSize()
{
return static_cast<int>(3 * sizeof(short) +
(chosenScheme.length() + mojangUuid.length() + username.length()) * sizeof(wchar_t));
}

View file

@ -0,0 +1,22 @@
#pragma once
#include "Packet.h"
using namespace std;
class AuthResponsePacket : public Packet, public enable_shared_from_this<AuthResponsePacket>
{
public:
wstring chosenScheme;
wstring mojangUuid;
wstring username;
AuthResponsePacket();
AuthResponsePacket(const wstring& chosenScheme, const wstring& mojangUuid, const wstring& username);
virtual void read(DataInputStream *dis);
virtual void write(DataOutputStream *dos);
virtual void handle(PacketListener *listener);
virtual int getEstimatedSize();
static shared_ptr<Packet> create() { return make_shared<AuthResponsePacket>(); }
virtual int getId() { return 171; }
};

View file

@ -0,0 +1,76 @@
#include "stdafx.h"
#include "InputOutputStream.h"
#include "PacketListener.h"
#include "AuthResultPacket.h"
AuthResultPacket::AuthResultPacket()
{
success = false;
}
AuthResultPacket::AuthResultPacket(bool success, const wstring& assignedUuid, const wstring& assignedUsername,
const wstring& errorMessage, const wstring& skinKey,
std::vector<uint8_t> skinData)
{
this->success = success;
this->assignedUuid = assignedUuid;
this->assignedUsername = assignedUsername;
this->errorMessage = errorMessage;
this->skinKey = skinKey;
this->skinData = std::move(skinData);
}
void AuthResultPacket::read(DataInputStream *dis)
{
success = dis->readBoolean();
assignedUuid = readUtf(dis, 64);
assignedUsername = readUtf(dis, 64);
errorMessage = readUtf(dis, 256);
skinKey = readUtf(dis, 256);
// read the skin blob (length + bytes)
// cap at 32kb, a real skin png is like 4kb tops
int skinSize = dis->readInt();
if (skinSize > 0 && skinSize <= 32768)
{
skinData.resize(static_cast<size_t>(skinSize));
for (int i = 0; i < skinSize; i++)
skinData[i] = dis->readByte();
}
else
{
skinData.clear();
// eat the bytes anyway so the stream doesnt get fucked up
if (skinSize > 0)
{
for (int i = 0; i < skinSize; i++)
dis->readByte();
}
}
}
void AuthResultPacket::write(DataOutputStream *dos)
{
dos->writeBoolean(success);
writeUtf(assignedUuid, dos);
writeUtf(assignedUsername, dos);
writeUtf(errorMessage, dos);
writeUtf(skinKey, dos);
int skinSize = static_cast<int>(skinData.size());
dos->writeInt(skinSize);
for (int i = 0; i < skinSize; i++)
dos->writeByte(skinData[i]);
}
void AuthResultPacket::handle(PacketListener *listener)
{
listener->handleAuthResult(shared_from_this());
}
int AuthResultPacket::getEstimatedSize()
{
return static_cast<int>(sizeof(bool) + 4 * sizeof(short) +
(assignedUuid.length() + assignedUsername.length() + errorMessage.length() + skinKey.length()) * sizeof(wchar_t)
+ sizeof(int) + skinData.size());
}

View file

@ -0,0 +1,28 @@
#pragma once
#include "Packet.h"
#include <vector>
using namespace std;
class AuthResultPacket : public Packet, public enable_shared_from_this<AuthResultPacket>
{
public:
bool success;
wstring assignedUuid;
wstring assignedUsername;
wstring errorMessage;
wstring skinKey;
std::vector<uint8_t> skinData;
AuthResultPacket();
AuthResultPacket(bool success, const wstring& assignedUuid, const wstring& assignedUsername,
const wstring& errorMessage, const wstring& skinKey = L"",
std::vector<uint8_t> skinData = {});
virtual void read(DataInputStream *dis);
virtual void write(DataOutputStream *dos);
virtual void handle(PacketListener *listener);
virtual int getEstimatedSize();
static shared_ptr<Packet> create() { return make_shared<AuthResultPacket>(); }
virtual int getId() { return 172; }
};

View file

@ -0,0 +1,56 @@
#include "stdafx.h"
#include "InputOutputStream.h"
#include "PacketListener.h"
#include "AuthSchemePacket.h"
AuthSchemePacket::AuthSchemePacket()
{
}
AuthSchemePacket::AuthSchemePacket(const vector<wstring>& schemes, const wstring& serverId)
{
this->schemes = schemes;
this->serverId = serverId;
}
void AuthSchemePacket::read(DataInputStream *dis)
{
int count = dis->readInt();
if (count < 0 || count > 16) count = 0; // dont let someone send us a billion schemes
schemes.clear();
schemes.reserve(static_cast<size_t>(count));
for (int i = 0; i < count; i++)
{
wstring scheme = readUtf(dis, 32);
// only take scheme names we actually know about
if (scheme == L"mojang" || scheme == L"offline" || scheme == L"elyby")
schemes.push_back(std::move(scheme));
}
serverId = readUtf(dis, 64);
}
void AuthSchemePacket::write(DataOutputStream *dos)
{
dos->writeInt(static_cast<int>(schemes.size()));
for (auto& s : schemes)
{
writeUtf(s, dos);
}
writeUtf(serverId, dos);
}
void AuthSchemePacket::handle(PacketListener *listener)
{
listener->handleAuthScheme(shared_from_this());
}
int AuthSchemePacket::getEstimatedSize()
{
int size = sizeof(int);
for (auto& s : schemes)
{
size += sizeof(short) + static_cast<int>(s.length()) * sizeof(wchar_t);
}
size += sizeof(short) + static_cast<int>(serverId.length()) * sizeof(wchar_t);
return size;
}

View file

@ -0,0 +1,22 @@
#pragma once
#include "Packet.h"
using namespace std;
class AuthSchemePacket : public Packet, public enable_shared_from_this<AuthSchemePacket>
{
public:
// "mojang", "offline" etc
vector<wstring> schemes;
wstring serverId; // hex challenge (20 chars), empty for offline
AuthSchemePacket();
AuthSchemePacket(const vector<wstring>& schemes, const wstring& serverId);
virtual void read(DataInputStream *dis);
virtual void write(DataOutputStream *dos);
virtual void handle(PacketListener *listener);
virtual int getEstimatedSize();
static shared_ptr<Packet> create() { return make_shared<AuthSchemePacket>(); }
virtual int getId() { return 170; }
};

View file

@ -50,6 +50,7 @@ public:
#ifdef _XBOX_ONE
eDisconnect_ExitedGame,
#endif
eDisconnect_AuthFailed,
};
// 4J Stu - The reason was a string, but we need to send a non-locale specific reason

View file

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

View file

@ -0,0 +1,45 @@
#pragma once
#include <cstdint>
#include <string>
#include <istream>
#include <functional>
struct GameUUID {
uint64_t hi = 0;
uint64_t lo = 0;
bool isValid() const { return hi != 0 || lo != 0; }
std::string toDashed() const;
std::string toUndashed() const;
std::wstring toWDashed() const;
static GameUUID fromDashed(const std::string& s);
static GameUUID fromUndashed(const std::string& s);
static GameUUID generateV4();
static GameUUID generateOffline(const std::string& playerName);
bool operator==(const GameUUID& o) const { return hi == o.hi && lo == o.lo; }
bool operator!=(const GameUUID& o) const { return !(*this == o); }
bool operator<(const GameUUID& o) const { return hi < o.hi || (hi == o.hi && lo < o.lo); }
struct Hash {
size_t operator()(const GameUUID& u) const {
size_t h = std::hash<uint64_t>{}(u.hi);
h ^= std::hash<uint64_t>{}(u.lo) + 0x9e3779b9 + (h << 6) + (h >> 2);
return h;
}
};
};
inline const GameUUID INVALID_UUID = {};
inline std::wistream& operator>>(std::wistream& is, GameUUID& uuid)
{
std::wstring ws;
is >> ws;
std::string s(ws.begin(), ws.end());
uuid = GameUUID::fromDashed(s);
return is;
}
namespace std {
template<> struct hash<GameUUID> {
size_t operator()(const GameUUID& u) const {
return GameUUID::Hash{}(u);
}
};
}

View file

@ -131,6 +131,10 @@ void Packet::staticCtor()
map(166, true, true, false, false, typeid(XZPacket), XZPacket::create);
map(167, false, true, false, false, typeid(GameCommandPacket), GameCommandPacket::create);
map(170, true, false, true, false, typeid(AuthSchemePacket), AuthSchemePacket::create); // Server -> Client
map(171, false, true, false, false, typeid(AuthResponsePacket), AuthResponsePacket::create); // Client -> Server
map(172, true, false, true, false, typeid(AuthResultPacket), AuthResultPacket::create); // Server -> Client
map(200, true, false, true, false, typeid(AwardStatPacket), AwardStatPacket::create);
map(201, true, true, false, false, typeid(PlayerInfoPacket), PlayerInfoPacket::create); // TODO New for 1.8.2 - Repurposed by 4J
map(202, true, true, true, false, typeid(PlayerAbilitiesPacket), PlayerAbilitiesPacket::create);

View file

@ -491,3 +491,18 @@ void PacketListener::handleGameCommand(shared_ptr<GameCommandPacket> packet)
{
onUnhandledPacket( (shared_ptr<Packet> ) packet);
}
void PacketListener::handleAuthScheme(shared_ptr<AuthSchemePacket> packet)
{
onUnhandledPacket( (shared_ptr<Packet> ) packet);
}
void PacketListener::handleAuthResponse(shared_ptr<AuthResponsePacket> packet)
{
onUnhandledPacket( (shared_ptr<Packet> ) packet);
}
void PacketListener::handleAuthResult(shared_ptr<AuthResultPacket> packet)
{
onUnhandledPacket( (shared_ptr<Packet> ) packet);
}

View file

@ -111,6 +111,9 @@ class KickPlayerPacket;
class AdditionalModelPartsPacket;
class XZPacket;
class GameCommandPacket;
class AuthSchemePacket;
class AuthResponsePacket;
class AuthResultPacket;
class PacketListener
{
@ -227,4 +230,7 @@ public:
virtual void handleKickPlayer(shared_ptr<KickPlayerPacket> packet);
virtual void handleXZ(shared_ptr<XZPacket> packet);
virtual void handleGameCommand(shared_ptr<GameCommandPacket> packet);
virtual void handleAuthScheme(shared_ptr<AuthSchemePacket> packet);
virtual void handleAuthResponse(shared_ptr<AuthResponsePacket> packet);
virtual void handleAuthResult(shared_ptr<AuthResultPacket> packet);
};

View file

@ -151,6 +151,8 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT
"${CMAKE_CURRENT_SOURCE_DIR}/Direction.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Facing.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Facing.h"
"${CMAKE_CURRENT_SOURCE_DIR}/GameUUID.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/GameUUID.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Pos.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Pos.h"
"${CMAKE_CURRENT_SOURCE_DIR}/SharedConstants.cpp"
@ -259,6 +261,12 @@ 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}/AuthResponsePacket.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/AuthResponsePacket.h"
"${CMAKE_CURRENT_SOURCE_DIR}/AuthResultPacket.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/AuthResultPacket.h"
"${CMAKE_CURRENT_SOURCE_DIR}/AuthSchemePacket.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/AuthSchemePacket.h"
"${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.h"
"${CMAKE_CURRENT_SOURCE_DIR}/BlockRegionUpdatePacket.cpp"

View file

@ -108,4 +108,7 @@
#include "KickPlayerPacket.h"
#include "XZPacket.h"
#include "GameCommandPacket.h"
#include "AuthSchemePacket.h"
#include "AuthResponsePacket.h"
#include "AuthResultPacket.h"

View file

@ -132,6 +132,10 @@ void XMemSet(void *a, int t, size_t s);
void XMemSet128(void *a, int t, size_t s);
void *XPhysicalAlloc(SIZE_T a, ULONG_PTR b, ULONG_PTR c, DWORD d);
void XPhysicalFree(void *a);
class IQNetPlayer;
void Win64_SetJoinedDedicatedServer(bool isDedicated);
bool Win64_IsJoinedDedicatedServer();
bool Win64_ShouldExposeSessionPlayer(IQNetPlayer *player);
class DLCManager;

61
newauth/CMakeLists.txt Normal file
View file

@ -0,0 +1,61 @@
include(FetchContent)
FetchContent_Declare(
sqlite3
URL "https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip"
URL_HASH SHA256=aa73d8748095808471deaa8e6f34aa700e37f2f787f4425744f53fdd15a89c40
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(sqlite3)
# build sqlite3 as a static library
add_library(sqlite3_lib STATIC "${sqlite3_SOURCE_DIR}/sqlite3.c")
target_include_directories(sqlite3_lib PUBLIC "${sqlite3_SOURCE_DIR}")
target_compile_definitions(sqlite3_lib PRIVATE SQLITE_THREADSAFE=1)
if(MSVC)
target_compile_options(sqlite3_lib PRIVATE /W0)
endif()
configure_compiler_target(sqlite3_lib)
set(NEWAUTH_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthCrypto.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthCrypto.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthHttp.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthHttp.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthInternal.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthJava.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthManager.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthElyby.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthSession.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthDb.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/newauthDb.h"
)
source_group("src" FILES ${NEWAUTH_SOURCES})
set(NEWAUTH_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/include/newauth.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/newauthManager.h"
)
source_group("include" FILES ${NEWAUTH_HEADERS})
add_library(newauth STATIC ${NEWAUTH_SOURCES} ${NEWAUTH_HEADERS})
target_include_directories(newauth
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include"
PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src"
)
target_compile_definitions(newauth PRIVATE
_LIB
$<$<CONFIG:Debug>:_DEBUG>
_CRT_SECURE_NO_WARNINGS
)
configure_compiler_target(newauth)
target_link_libraries(newauth PRIVATE
winhttp
bcrypt
sqlite3_lib
)

163
newauth/include/newauth.h Normal file
View file

@ -0,0 +1,163 @@
#pragma once
#include <string>
#include <functional>
#include <cstdint>
#include <memory>
#include <vector>
namespace newauth {
enum class AuthErrorCode {
None = 0,
NetworkTimeout,
NetworkError,
HttpError,
InvalidCredentials,
TokenExpired,
ServerUnavailable,
RateLimited,
ProfileNotOwned,
Cancelled,
InternalError,
};
struct AuthError {
AuthErrorCode code = AuthErrorCode::None;
std::string message;
int httpStatus = 0;
bool ok() const { return code == AuthErrorCode::None; }
explicit operator bool() const { return !ok(); }
};
static constexpr const char* JAVA_CLIENT_ID = "00000000402b5328";
static constexpr const char* MSA_SCOPE = "service::user.auth.xboxlive.com::MBI_SSL";
struct DeviceCodeInfo {
std::string userCode;
std::string verificationUri;
std::string directUri;
std::string deviceCode;
int64_t expiresMs;
int64_t intervalMs;
};
using DeviceCodeCallback = std::function<void(const DeviceCodeInfo&)>;
struct JavaSession {
std::string username;
std::string uuid;
std::string accessToken;
int64_t expireMs;
};
class JavaAuthManager {
public:
JavaAuthManager();
~JavaAuthManager();
JavaAuthManager(const JavaAuthManager&) = delete;
JavaAuthManager& operator=(const JavaAuthManager&) = delete;
bool Login(DeviceCodeCallback onDeviceCode,
JavaSession& outSession,
std::string& error,
int timeoutSeconds = 300);
bool Refresh(JavaSession& outSession, std::string& error);
bool IsLoggedIn() const;
void Logout();
void RequestCancel();
bool SaveTokens(const std::string& filePath) const;
bool LoadTokens(const std::string& filePath);
std::string SerializeTokens() const;
bool DeserializeTokens(const std::string& json);
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
struct ElybyTokens {
std::string accessToken;
std::string clientToken;
std::string uuid;
std::string username;
};
bool ElybyLogin(const std::string& username, const std::string& password,
ElybyTokens& outTokens, std::string& error);
bool ElybyRefresh(ElybyTokens& tokens, std::string& error);
bool ElybyValidate(const std::string& accessToken, std::string& error);
bool ElybyLoadTokens(const std::string& path, ElybyTokens& out);
bool ElybySaveTokens(const std::string& path, const ElybyTokens& tokens);
std::string ElybySerializeTokens(const ElybyTokens& tokens);
bool ElybyDeserializeTokens(const std::string& json, ElybyTokens& out);
std::string MakeSkinKey(const std::string& uuid);
std::string UndashUuid(const std::string& dashed);
std::string DashUuid(const std::string& undashed);
std::string GenerateOfflineUuid(const std::string& username);
struct Uuid128 {
uint64_t hi = 0;
uint64_t lo = 0;
bool isValid() const { return hi != 0 || lo != 0; }
};
Uuid128 ParseUuid128(const std::string& dashed);
bool JoinServer(const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
std::string& error);
struct HasJoinedResult {
bool success = false;
AuthError error;
std::string username;
std::string uuid;
std::string skinUrl;
std::string capeUrl;
};
HasJoinedResult HasJoined(const std::string& username,
const std::string& serverId,
std::string& error);
std::vector<uint8_t> FetchSkinPng(const std::string& url, std::string& error);
std::string FetchProfileSkinUrl(const std::string& uuid, std::string& error);
std::vector<uint8_t> FetchSkinPngRaw(const std::string& url, std::string& error);
bool ElybyJoinServer(const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
std::string& error);
HasJoinedResult ElybyHasJoined(const std::string& username,
const std::string& serverId,
std::string& error);
std::string ElybyFetchProfileSkinUrl(const std::string& uuid, std::string& error);
std::string MakeElybySkinKey(const std::string& uuid);
static constexpr size_t kMaxSkinBytes = 32768;
bool ValidateSkinPng(const uint8_t* data, size_t size);
}

View file

@ -0,0 +1,136 @@
#pragma once
#include "newauth.h"
#include <functional>
#include <mutex>
#include <atomic>
#include <string>
#include <vector>
#include <memory>
#include <condition_variable>
#ifndef XUSER_MAX_COUNT
#define XUSER_MAX_COUNT 4
#endif
class newauthManager {
public:
enum class State {
Idle,
WaitingForCode,
Authenticating,
Success,
Failed,
};
struct JavaAccountInfo {
std::string username;
std::string uuid;
std::string tokenFile;
bool isOffline = false;
std::string authProvider;
};
struct AuthSlot {
std::shared_ptr<newauth::JavaAuthManager> auth;
newauth::ElybyTokens elybyTokens;
newauth::JavaSession session;
std::atomic<int> accountIndex{-1};
mutable std::mutex mutex;
std::atomic<uint32_t> generation{0};
std::atomic<State> state{State::Idle};
mutable std::condition_variable cv;
std::string lastError;
bool hasSession() const { return !session.uuid.empty(); }
AuthSlot() : auth(std::make_shared<newauth::JavaAuthManager>()) {}
};
using DeviceCodeCb = newauth::DeviceCodeCallback;
using JavaCompleteCb = std::function<void(bool, const newauth::JavaSession&, const std::string&)>;
static newauthManager& Get();
newauthManager(const newauthManager&) = delete;
newauthManager& operator=(const newauthManager&) = delete;
bool LoadJavaAccountIndex();
bool SaveJavaAccountIndex() const;
std::vector<JavaAccountInfo> GetJavaAccounts() const;
const AuthSlot& GetSlot(int slot) const;
bool SetAccountForSlot(int slot, int accountIndex);
void ClearSlot(int slot);
newauth::JavaSession GetSlotSession(int slot) const;
bool IsSlotLoggedIn(int slot) const;
bool IsTokenExpiringSoon(int slot) const;
void RefreshSlot(int slot);
bool WaitForSlotReady(int slot, int timeoutMs = 15000) const;
bool IsAccountInUseByOtherSlot(int slot, int accountIndex) const;
int GetActiveJavaAccountIndex() const;
bool SetActiveJavaAccount(int index) { return SetAccountForSlot(0, index); }
newauth::JavaSession GetJavaSession() const;
bool IsJavaLoggedIn() const;
State GetState() const { return m_slots[0].state.load(); }
State GetSlotState(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) slot = 0;
return m_slots[slot].state.load();
}
bool WaitForAuthReady(int timeoutMs = 15000) const { return WaitForSlotReady(0, timeoutMs); }
void BeginAddJavaAccount(DeviceCodeCb onDeviceCode, JavaCompleteCb onComplete,
int timeoutSeconds = 300);
using ElybyCompleteCb = std::function<void(bool ok, const newauth::JavaSession& session, const std::string& error)>;
using Elyby2FACb = std::function<void()>;
void BeginAddElybyAccount(const std::string& username, const std::string& password,
ElybyCompleteCb onComplete, Elyby2FACb on2FA = nullptr);
int AddOfflineJavaAccount(const std::string& username);
bool RemoveJavaAccount(int index);
void TryRestoreActiveJavaAccount();
std::string GetJavaDeviceCode() const;
std::string GetJavaDirectUri() const;
std::string GetLastError() const;
static constexpr const char* kJavaAccountsFile = "java_accounts.json";
private:
newauthManager();
~newauthManager();
std::string AllocTokenFile(const std::string& uuid) const;
static newauth::JavaSession SynthesizeOfflineSession(const JavaAccountInfo& acct);
std::shared_ptr<newauth::JavaAuthManager> ResetSlotAuth(AuthSlot& s);
void RunElybyRefresh(int slot, uint32_t gen, const std::string& tokenFile,
bool failOpen, bool alwaysSaveIndex);
AuthSlot m_slots[XUSER_MAX_COUNT];
mutable std::mutex m_accountsMutex;
std::vector<JavaAccountInfo> m_javaAccounts;
mutable std::mutex m_deviceCodeMutex;
std::string m_javaDeviceCode;
std::string m_javaDirectUri;
bool m_javaRestoreAttempted = false;
};

156
newauth/newauth.vcxproj Normal file
View file

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>newauth</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(SolutionDir)bin\$(Configuration)\$(Platform)\</OutDir>
<IntDir>$(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(SolutionDir)bin\$(Configuration)\$(Platform)\</OutDir>
<IntDir>$(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(SolutionDir)bin\$(Configuration)\$(Platform)\</OutDir>
<IntDir>$(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(SolutionDir)bin\$(Configuration)\$(Platform)\</OutDir>
<IntDir>$(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<Optimization>Disabled</Optimization>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Lib>
<OutputFile>$(OutDir)newauth.lib</OutputFile>
</Lib>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Lib>
<OutputFile>$(OutDir)newauth.lib</OutputFile>
</Lib>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<Optimization>Disabled</Optimization>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Lib>
<OutputFile>$(OutDir)newauth.lib</OutputFile>
</Lib>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Lib>
<OutputFile>$(OutDir)newauth.lib</OutputFile>
</Lib>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="include\newauth.h" />
<ClInclude Include="include\newauthManager.h" />
<ClInclude Include="src\newauthCrypto.h" />
<ClInclude Include="src\newauthHttp.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\newauthCrypto.cpp" />
<ClCompile Include="src\newauthHttp.cpp" />
<ClCompile Include="src\newauth.cpp" />
<ClCompile Include="src\newauthJava.cpp" />
<ClCompile Include="src\newauthManager.cpp" />
<ClCompile Include="src\newauthSession.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View file

@ -0,0 +1,347 @@
#include "newauthCrypto.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
#include <stdexcept>
#include <chrono>
#include <cassert>
#include <cstring>
#include <cstdio>
namespace newauth {
static void ThrowIfFailed(NTSTATUS status, const char* ctx) {
if (!BCRYPT_SUCCESS(status)) {
char buf[128];
snprintf(buf, sizeof(buf), "BCrypt error 0x%08X in %s", (unsigned)status, ctx);
throw std::runtime_error(buf);
}
}
std::vector<uint8_t> BuildSubjectPublicKeyInfoDER(
int bitSize,
const std::vector<uint8_t>& x,
const std::vector<uint8_t>& y)
{
static const uint8_t OID_EC_PUBLIC_KEY[] = { 0x2a,0x86,0x48,0xce,0x3d,0x02,0x01 };
static const uint8_t OID_P256[] = { 0x2a,0x86,0x48,0xce,0x3d,0x03,0x01,0x07 };
static const uint8_t OID_P384[] = { 0x2b,0x81,0x04,0x00,0x22 };
size_t coordSize = (bitSize == 256) ? 32 : 48;
const uint8_t* curveOid = (bitSize == 256) ? OID_P256 : OID_P384;
size_t curveOidLen = (bitSize == 256) ? 8 : 5;
size_t algIdInnerLen = 2 + sizeof(OID_EC_PUBLIC_KEY) + 2 + curveOidLen;
size_t bitStringContent = 1 + 1 + coordSize + coordSize;
size_t bitStringLen = bitStringContent;
size_t outerContent = (2 + algIdInnerLen) + (2 + bitStringLen);
std::vector<uint8_t> der;
auto appendLen = [&](size_t len) {
if (len < 128) {
der.push_back((uint8_t)len);
} else {
der.push_back(0x82);
der.push_back((uint8_t)(len >> 8));
der.push_back((uint8_t)(len & 0xFF));
}
};
der.push_back(0x30);
appendLen(outerContent);
der.push_back(0x30);
appendLen(algIdInnerLen);
der.push_back(0x06); der.push_back((uint8_t)sizeof(OID_EC_PUBLIC_KEY));
der.insert(der.end(), OID_EC_PUBLIC_KEY, OID_EC_PUBLIC_KEY + sizeof(OID_EC_PUBLIC_KEY));
der.push_back(0x06); der.push_back((uint8_t)curveOidLen);
der.insert(der.end(), curveOid, curveOid + curveOidLen);
der.push_back(0x03);
appendLen(bitStringLen + 1);
der.push_back(0x00);
der.push_back(0x04);
der.insert(der.end(), x.begin(), x.end());
der.insert(der.end(), y.begin(), y.end());
return der;
}
static ECKeyPair GenerateKeyPair(int bitSize) {
LPCWSTR algId = (bitSize == 256) ? BCRYPT_ECDSA_P256_ALGORITHM
: BCRYPT_ECDSA_P384_ALGORITHM;
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_KEY_HANDLE hKey = nullptr;
try {
ThrowIfFailed(BCryptOpenAlgorithmProvider(&hAlg, algId, nullptr, 0), "OpenAlgProvider");
ThrowIfFailed(BCryptGenerateKeyPair(hAlg, &hKey, bitSize, 0), "GenerateKeyPair");
ThrowIfFailed(BCryptFinalizeKeyPair(hKey, 0), "FinalizeKeyPair");
ULONG privBlobLen = 0;
ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPRIVATE_BLOB, nullptr, 0, &privBlobLen, 0), "ExportPriv(len)");
std::vector<uint8_t> privBlob(privBlobLen);
ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPRIVATE_BLOB, privBlob.data(), privBlobLen, &privBlobLen, 0), "ExportPriv");
ULONG pubBlobLen = 0;
ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPUBLIC_BLOB, nullptr, 0, &pubBlobLen, 0), "ExportPub(len)");
std::vector<uint8_t> pubBlob(pubBlobLen);
ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPUBLIC_BLOB, pubBlob.data(), pubBlobLen, &pubBlobLen, 0), "ExportPub");
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
const BCRYPT_ECCKEY_BLOB* hdr = reinterpret_cast<const BCRYPT_ECCKEY_BLOB*>(pubBlob.data());
ULONG cbKey = hdr->cbKey;
std::vector<uint8_t> x(pubBlob.begin() + 8, pubBlob.begin() + 8 + cbKey);
std::vector<uint8_t> y(pubBlob.begin() + 8 + cbKey, pubBlob.begin() + 8 + cbKey * 2);
ECKeyPair kp;
kp.bitSize = bitSize;
kp.privateBlob = std::move(privBlob);
kp.publicBlob = std::move(pubBlob);
kp.x = x;
kp.y = y;
kp.publicKeyDER = BuildSubjectPublicKeyInfoDER(bitSize, x, y);
return kp;
} catch (...) {
if (hKey) BCryptDestroyKey(hKey);
if (hAlg) BCryptCloseAlgorithmProvider(hAlg, 0);
throw;
}
}
ECKeyPair GenerateP256KeyPair() { return GenerateKeyPair(256); }
ECKeyPair GenerateP384KeyPair() { return GenerateKeyPair(384); }
std::vector<uint8_t> SignSHA256P256(const ECKeyPair& kp,
const uint8_t* data,
size_t len)
{
if (kp.bitSize != 256)
throw std::runtime_error("SignSHA256P256: key is not P-256");
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_KEY_HANDLE hKey = nullptr;
BCRYPT_ALG_HANDLE hHashAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
try {
ThrowIfFailed(BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_ECDSA_P256_ALGORITHM, nullptr, 0), "Sign/OpenAlg");
ThrowIfFailed(BCryptImportKeyPair(hAlg, nullptr, BCRYPT_ECCPRIVATE_BLOB,
&hKey,
const_cast<uint8_t*>(kp.privateBlob.data()),
(ULONG)kp.privateBlob.size(), 0), "Sign/ImportKey");
ThrowIfFailed(BCryptOpenAlgorithmProvider(&hHashAlg, BCRYPT_SHA256_ALGORITHM, nullptr, 0), "Sign/OpenHashAlg");
ThrowIfFailed(BCryptCreateHash(hHashAlg, &hHash, nullptr, 0, nullptr, 0, 0), "Sign/CreateHash");
ThrowIfFailed(BCryptHashData(hHash, const_cast<uint8_t*>(data), (ULONG)len, 0), "Sign/HashData");
uint8_t digest[32];
ThrowIfFailed(BCryptFinishHash(hHash, digest, sizeof(digest), 0), "Sign/FinishHash");
BCryptDestroyHash(hHash); hHash = nullptr;
BCryptCloseAlgorithmProvider(hHashAlg, 0); hHashAlg = nullptr;
ULONG sigLen = 0;
ThrowIfFailed(BCryptSignHash(hKey, nullptr, digest, sizeof(digest), nullptr, 0, &sigLen, 0), "Sign/GetSigLen");
std::vector<uint8_t> sig(sigLen);
ThrowIfFailed(BCryptSignHash(hKey, nullptr, digest, sizeof(digest), sig.data(), sigLen, &sigLen, 0), "Sign/SignHash");
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
sig.resize(sigLen);
return sig;
} catch (...) {
if (hHash) BCryptDestroyHash(hHash);
if (hHashAlg) BCryptCloseAlgorithmProvider(hHashAlg, 0);
if (hKey) BCryptDestroyKey(hKey);
if (hAlg) BCryptCloseAlgorithmProvider(hAlg, 0);
throw;
}
}
static const char kB64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static const char kB64UrlChars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
static std::string Base64EncodeInternal(const uint8_t* data, size_t len, const char* alpha, bool pad) {
std::string out;
out.reserve(((len + 2) / 3) * 4);
for (size_t i = 0; i < len; i += 3) {
uint32_t v = (uint32_t)data[i] << 16;
if (i + 1 < len) v |= (uint32_t)data[i + 1] << 8;
if (i + 2 < len) v |= (uint32_t)data[i + 2];
out += alpha[(v >> 18) & 0x3F];
out += alpha[(v >> 12) & 0x3F];
out += (i + 1 < len) ? alpha[(v >> 6) & 0x3F] : (pad ? '=' : '\0');
out += (i + 2 < len) ? alpha[(v >> 0) & 0x3F] : (pad ? '=' : '\0');
}
while (!out.empty() && out.back() == '\0') out.pop_back();
return out;
}
std::string Base64Encode(const uint8_t* data, size_t len) {
return Base64EncodeInternal(data, len, kB64Chars, true);
}
std::string Base64Encode(const std::vector<uint8_t>& data) {
return Base64Encode(data.data(), data.size());
}
std::string Base64UrlEncode(const uint8_t* data, size_t len) {
return Base64EncodeInternal(data, len, kB64UrlChars, false);
}
std::string Base64UrlEncode(const std::vector<uint8_t>& data) {
return Base64UrlEncode(data.data(), data.size());
}
std::string BuildProofKeyJson(const ECKeyPair& p256) {
std::string xEnc = Base64UrlEncode(p256.x);
std::string yEnc = Base64UrlEncode(p256.y);
return "{\"kty\":\"EC\",\"alg\":\"ES256\",\"crv\":\"P-256\",\"use\":\"sig\","
"\"x\":\"" + xEnc + "\",\"y\":\"" + yEnc + "\"}";
}
static void AppendBE32(std::vector<uint8_t>& buf, uint32_t v) {
buf.push_back((uint8_t)(v >> 24));
buf.push_back((uint8_t)(v >> 16));
buf.push_back((uint8_t)(v >> 8));
buf.push_back((uint8_t)(v ));
}
static void AppendBE64(std::vector<uint8_t>& buf, uint64_t v) {
buf.push_back((uint8_t)(v >> 56));
buf.push_back((uint8_t)(v >> 48));
buf.push_back((uint8_t)(v >> 40));
buf.push_back((uint8_t)(v >> 32));
buf.push_back((uint8_t)(v >> 24));
buf.push_back((uint8_t)(v >> 16));
buf.push_back((uint8_t)(v >> 8));
buf.push_back((uint8_t)(v ));
}
std::string BuildXblSignatureHeader(const ECKeyPair& p256,
const std::string& method,
const std::string& urlPath,
const std::string& body,
const std::string& authHdr)
{
using namespace std::chrono;
int64_t epochSec = duration_cast<seconds>(
system_clock::now().time_since_epoch()).count();
uint64_t winTs = (uint64_t)(epochSec + 11644473600LL) * 10000000ULL;
std::vector<uint8_t> content;
content.reserve(512);
AppendBE32(content, 1);
content.push_back(0x00);
AppendBE64(content, winTs);
content.push_back(0x00);
content.insert(content.end(), method.begin(), method.end());
content.push_back(0x00);
content.insert(content.end(), urlPath.begin(), urlPath.end());
content.push_back(0x00);
content.insert(content.end(), authHdr.begin(), authHdr.end());
content.push_back(0x00);
content.insert(content.end(), body.begin(), body.end());
content.push_back(0x00);
std::vector<uint8_t> sig = SignSHA256P256(p256, content.data(), content.size());
std::vector<uint8_t> hdrBlob;
hdrBlob.reserve(4 + 8 + sig.size());
AppendBE32(hdrBlob, 1);
AppendBE64(hdrBlob, winTs);
hdrBlob.insert(hdrBlob.end(), sig.begin(), sig.end());
return Base64Encode(hdrBlob);
}
std::string GenerateUUID() {
uint8_t bytes[16];
NTSTATUS st = BCryptGenRandom(nullptr, bytes, sizeof(bytes), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
if (!BCRYPT_SUCCESS(st)) {
for (auto& b : bytes) b = (uint8_t)(rand() & 0xFF);
}
bytes[6] = (bytes[6] & 0x0F) | 0x40;
bytes[8] = (bytes[8] & 0x3F) | 0x80;
char buf[37];
snprintf(buf, sizeof(buf),
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5],
bytes[6], bytes[7],
bytes[8], bytes[9],
bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]);
return buf;
}
std::vector<uint8_t> Base64Decode(const std::string& s) {
static const int kInv[256] = {
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
};
std::vector<uint8_t> out;
int buf = 0, bits = 0;
for (unsigned char c : s) {
if (c == '=' || kInv[c] < 0) continue;
buf = (buf << 6) | kInv[c];
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back((uint8_t)(buf >> bits));
buf &= (1 << bits) - 1;
}
}
return out;
}
std::string Base64DecodeStr(const std::string& encoded) {
auto bytes = Base64Decode(encoded);
return std::string(bytes.begin(), bytes.end());
}
std::vector<uint8_t> ComputeMD5(const void* data, size_t len) {
std::vector<uint8_t> result(16, 0);
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
if (BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_MD5_ALGORITHM, nullptr, 0) == 0) {
if (BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0) == 0) {
if (BCryptHashData(hHash, (PUCHAR)data, (ULONG)len, 0) == 0) {
BCryptFinishHash(hHash, result.data(), 16, 0);
}
BCryptDestroyHash(hHash);
}
BCryptCloseAlgorithmProvider(hAlg, 0);
}
return result;
}
}

View file

@ -0,0 +1,50 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
namespace newauth {
struct ECKeyPair {
int bitSize;
std::vector<uint8_t> privateBlob;
std::vector<uint8_t> publicBlob;
std::vector<uint8_t> x;
std::vector<uint8_t> y;
std::vector<uint8_t> publicKeyDER;
};
ECKeyPair GenerateP256KeyPair();
ECKeyPair GenerateP384KeyPair();
std::vector<uint8_t> SignSHA256P256(const ECKeyPair& kp,
const uint8_t* data,
size_t len);
std::string Base64Encode(const uint8_t* data, size_t len);
std::string Base64Encode(const std::vector<uint8_t>& data);
std::string Base64UrlEncode(const uint8_t* data, size_t len);
std::string Base64UrlEncode(const std::vector<uint8_t>& data);
std::string BuildProofKeyJson(const ECKeyPair& p256);
std::string BuildXblSignatureHeader(const ECKeyPair& p256,
const std::string& method,
const std::string& urlPath,
const std::string& body,
const std::string& authHdr = "");
std::string GenerateUUID();
std::vector<uint8_t> Base64Decode(const std::string& b64);
std::string Base64DecodeStr(const std::string& b64);
std::vector<uint8_t> ComputeMD5(const void* data, size_t len);
std::vector<uint8_t> BuildSubjectPublicKeyInfoDER(
int bitSize,
const std::vector<uint8_t>& x,
const std::vector<uint8_t>& y);
}

314
newauth/src/newauthDb.cpp Normal file
View file

@ -0,0 +1,314 @@
#define _CRT_SECURE_NO_WARNINGS
#include "newauthDb.h"
#include "newauthInternal.h"
#include "../include/newauth.h"
#include "sqlite3.h"
#include <cstdio>
#include <fstream>
#include <sstream>
#ifdef _WIN32
#include <Windows.h>
#endif
static void DbLog(const char* fmt, ...) {
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
#ifdef _WIN32
OutputDebugStringA(buf);
#endif
}
#define DB_LOG(msg, ...) DbLog("[authdb] " msg "\n", ##__VA_ARGS__)
namespace newauth {
AuthDb& AuthDb::Get() {
static AuthDb instance;
return instance;
}
AuthDb::AuthDb() = default;
AuthDb::~AuthDb() {
Close();
}
bool AuthDb::Open(const char* dbPath) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_db) return true;
int rc = sqlite3_open(dbPath, &m_db);
if (rc != SQLITE_OK) {
DB_LOG("sqlite3_open('%s') failed: %s", dbPath, sqlite3_errmsg(m_db));
sqlite3_close(m_db);
m_db = nullptr;
return false;
}
sqlite3_exec(m_db, "PRAGMA journal_mode=WAL;", nullptr, nullptr, nullptr);
if (!EnsureTables()) {
DB_LOG("EnsureTables failed");
sqlite3_close(m_db);
m_db = nullptr;
return false;
}
DB_LOG("opened '%s'", dbPath);
return true;
}
void AuthDb::Close() {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_db) {
sqlite3_close(m_db);
m_db = nullptr;
}
}
bool AuthDb::EnsureTables() {
const char* sql =
"CREATE TABLE IF NOT EXISTS accounts ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" username TEXT NOT NULL,"
" uuid TEXT NOT NULL,"
" token_key TEXT,"
" is_offline INTEGER DEFAULT 0,"
" auth_provider TEXT DEFAULT 'mojang'"
");"
"CREATE TABLE IF NOT EXISTS config ("
" key TEXT PRIMARY KEY,"
" value TEXT"
");"
"CREATE TABLE IF NOT EXISTS tokens ("
" key TEXT PRIMARY KEY,"
" blob TEXT NOT NULL"
");";
char* err = nullptr;
int rc = sqlite3_exec(m_db, sql, nullptr, nullptr, &err);
if (rc != SQLITE_OK) {
DB_LOG("EnsureTables error: %s", err ? err : "unknown");
sqlite3_free(err);
return false;
}
return true;
}
bool AuthDb::LoadAccounts(int& activeIndex, std::vector<AccountRow>& accounts) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db) return false;
accounts.clear();
activeIndex = -1;
{
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(m_db,
"SELECT value FROM config WHERE key='activeIndex'", -1, &stmt, nullptr);
if (rc == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW) {
const char* v = (const char*)sqlite3_column_text(stmt, 0);
if (v) activeIndex = atoi(v);
}
sqlite3_finalize(stmt);
}
{
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(m_db,
"SELECT id, username, uuid, token_key, is_offline, auth_provider "
"FROM accounts ORDER BY id", -1, &stmt, nullptr);
if (rc != SQLITE_OK) {
DB_LOG("LoadAccounts prepare failed: %s", sqlite3_errmsg(m_db));
return false;
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
AccountRow row;
row.id = sqlite3_column_int(stmt, 0);
row.username = (const char*)sqlite3_column_text(stmt, 1);
row.uuid = (const char*)sqlite3_column_text(stmt, 2);
const char* tk = (const char*)sqlite3_column_text(stmt, 3);
row.tokenKey = tk ? tk : "";
row.isOffline = sqlite3_column_int(stmt, 4) != 0;
const char* ap = (const char*)sqlite3_column_text(stmt, 5);
row.authProvider = ap ? ap : "mojang";
accounts.push_back(std::move(row));
}
sqlite3_finalize(stmt);
}
DB_LOG("LoadAccounts: %d accounts, activeIndex=%d", (int)accounts.size(), activeIndex);
return true;
}
bool AuthDb::SaveAccounts(int activeIndex, const std::vector<AccountRow>& accounts) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db) return false;
char* err = nullptr;
sqlite3_exec(m_db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);
sqlite3_exec(m_db, "DELETE FROM accounts;", nullptr, nullptr, nullptr);
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(m_db,
"INSERT INTO accounts (username, uuid, token_key, is_offline, auth_provider) "
"VALUES (?, ?, ?, ?, ?)", -1, &stmt, nullptr);
if (rc != SQLITE_OK) {
DB_LOG("SaveAccounts prepare failed: %s", sqlite3_errmsg(m_db));
sqlite3_exec(m_db, "ROLLBACK;", nullptr, nullptr, nullptr);
return false;
}
for (auto& a : accounts) {
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, a.username.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, a.uuid.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, a.tokenKey.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 4, a.isOffline ? 1 : 0);
sqlite3_bind_text(stmt, 5, a.authProvider.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
}
sqlite3_finalize(stmt);
rc = sqlite3_prepare_v2(m_db,
"INSERT OR REPLACE INTO config (key, value) VALUES ('activeIndex', ?)",
-1, &stmt, nullptr);
if (rc == SQLITE_OK) {
char buf[32];
snprintf(buf, sizeof(buf), "%d", activeIndex);
sqlite3_bind_text(stmt, 1, buf, -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
sqlite3_exec(m_db, "COMMIT;", nullptr, nullptr, &err);
if (err) { DB_LOG("SaveAccounts commit: %s", err); sqlite3_free(err); return false; }
DB_LOG("SaveAccounts: saved %d accounts, activeIndex=%d", (int)accounts.size(), activeIndex);
return true;
}
bool AuthDb::SaveTokenBlob(const std::string& key, const std::string& blob) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db) return false;
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(m_db,
"INSERT OR REPLACE INTO tokens (key, blob) VALUES (?, ?)",
-1, &stmt, nullptr);
if (rc != SQLITE_OK) return false;
sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, blob.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return rc == SQLITE_DONE;
}
std::string AuthDb::LoadTokenBlob(const std::string& key) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db) return "";
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(m_db,
"SELECT blob FROM tokens WHERE key=?", -1, &stmt, nullptr);
if (rc != SQLITE_OK) return "";
sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
std::string result;
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char* v = (const char*)sqlite3_column_text(stmt, 0);
if (v) result = v;
}
sqlite3_finalize(stmt);
return result;
}
bool AuthDb::DeleteTokenBlob(const std::string& key) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db) return false;
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(m_db,
"DELETE FROM tokens WHERE key=?", -1, &stmt, nullptr);
if (rc != SQLITE_OK) return false;
sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return rc == SQLITE_DONE;
}
bool AuthDb::MigrateFromJson(const char* jsonPath) {
std::ifstream f(jsonPath);
if (!f) return false;
DB_LOG("MigrateFromJson: found '%s', importing...", jsonPath);
std::string content((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
f.close();
int activeIndex = (int)JsonGetInt(content, "activeIndex");
std::vector<AccountRow> accounts;
std::vector<std::string> objects;
{
size_t pos = content.find("\"accounts\"");
if (pos != std::string::npos) {
pos = content.find('[', pos);
if (pos != std::string::npos) {
int depth = 0; size_t start = pos;
for (; pos < content.size(); ++pos) {
if (content[pos] == '[') ++depth;
else if (content[pos] == ']') { --depth; if (depth == 0) { ++pos; break; } }
}
std::string arr = content.substr(start, pos - start);
objects = JsonArrayObjects(arr);
}
}
}
for (auto& obj : objects) {
AccountRow row;
row.username = JsonGetString(obj, "username");
row.uuid = JsonGetString(obj, "uuid");
row.tokenKey = JsonGetString(obj, "tokenFile");
row.isOffline = (JsonRawValue(obj, "isOffline") == "true");
row.authProvider = JsonGetString(obj, "authProvider");
if (row.authProvider.empty())
row.authProvider = row.isOffline ? "offline" : "mojang";
if (!row.tokenKey.empty() || row.isOffline)
accounts.push_back(std::move(row));
}
if (!SaveAccounts(activeIndex, accounts)) {
DB_LOG("MigrateFromJson: SaveAccounts failed");
return false;
}
for (auto& acct : accounts) {
if (acct.tokenKey.empty() || acct.isOffline) continue;
std::ifstream tf(acct.tokenKey);
if (!tf) continue;
std::string tokenContent((std::istreambuf_iterator<char>(tf)),
std::istreambuf_iterator<char>());
tf.close();
if (!tokenContent.empty()) {
SaveTokenBlob(acct.tokenKey, tokenContent);
DB_LOG("MigrateFromJson: imported token '%s'", acct.tokenKey.c_str());
}
}
std::string backupPath = std::string(jsonPath) + ".migrated";
std::rename(jsonPath, backupPath.c_str());
DB_LOG("MigrateFromJson: done, renamed '%s' -> '%s'", jsonPath, backupPath.c_str());
return true;
}
}

49
newauth/src/newauthDb.h Normal file
View file

@ -0,0 +1,49 @@
#pragma once
#include <string>
#include <vector>
#include <mutex>
struct sqlite3;
namespace newauth {
struct AccountRow {
int id = 0;
std::string username;
std::string uuid;
std::string tokenKey;
bool isOffline = false;
std::string authProvider;
};
class AuthDb {
public:
static AuthDb& Get();
AuthDb(const AuthDb&) = delete;
AuthDb& operator=(const AuthDb&) = delete;
bool Open(const char* dbPath = "newauth.db");
void Close();
bool LoadAccounts(int& activeIndex, std::vector<AccountRow>& accounts);
bool SaveAccounts(int activeIndex, const std::vector<AccountRow>& accounts);
bool SaveTokenBlob(const std::string& key, const std::string& blob);
std::string LoadTokenBlob(const std::string& key);
bool DeleteTokenBlob(const std::string& key);
bool MigrateFromJson(const char* jsonPath = "java_accounts.json");
private:
AuthDb();
~AuthDb();
bool EnsureTables();
sqlite3* m_db = nullptr;
std::mutex m_mutex;
};
}

View file

@ -0,0 +1,160 @@
#include "../include/newauth.h"
#include "newauthCrypto.h"
#include "newauthHttp.h"
#include "newauthInternal.h"
#include <fstream>
#include <sstream>
namespace newauth {
using namespace newauth;
bool ElybyLogin(const std::string& username, const std::string& password,
ElybyTokens& outTokens, std::string& error)
{
std::string clientToken = outTokens.clientToken;
if (clientToken.empty())
clientToken = GenerateUUID();
std::string body = "{\"agent\":{\"name\":\"Minecraft\",\"version\":1}"
",\"username\":" + JsonStr(username) +
",\"password\":" + JsonStr(password) +
",\"clientToken\":" + JsonStr(clientToken) + "}";
try {
auto resp = HttpPost("https://authserver.ely.by/auth/authenticate",
body, "application/json");
if (resp.statusCode == 200 && !resp.body.empty()) {
outTokens.accessToken = JsonGetString(resp.body, "accessToken");
outTokens.clientToken = JsonGetString(resp.body, "clientToken");
std::string profile = JsonRawValue(resp.body, "selectedProfile");
if (!profile.empty()) {
outTokens.uuid = JsonGetString(profile, "id");
outTokens.username = JsonGetString(profile, "name");
}
if (outTokens.accessToken.empty()) {
error = "ElybyLogin: empty accessToken in response";
return false;
}
return true;
}
if (resp.statusCode == 401) {
std::string errMsg = JsonGetString(resp.body, "errorMessage");
if (errMsg.find("two factor auth") != std::string::npos ||
errMsg.find("Account protected with two factor auth") != std::string::npos) {
error = "elyby_2fa_required";
return false;
}
error = "ElybyLogin: invalid credentials";
if (!errMsg.empty()) error += " (" + errMsg + ")";
return false;
}
error = "ElybyLogin HTTP " + std::to_string(resp.statusCode);
if (!resp.body.empty()) {
std::string msg = JsonGetString(resp.body, "errorMessage");
if (!msg.empty()) error += ": " + msg;
}
} catch (const std::exception& e) {
error = std::string("ElybyLogin network error: ") + e.what();
}
return false;
}
bool ElybyRefresh(ElybyTokens& tokens, std::string& error)
{
std::string body = "{\"accessToken\":" + JsonStr(tokens.accessToken) +
",\"clientToken\":" + JsonStr(tokens.clientToken) + "}";
try {
auto resp = HttpPost("https://authserver.ely.by/auth/refresh",
body, "application/json");
if (resp.statusCode == 200 && !resp.body.empty()) {
tokens.accessToken = JsonGetString(resp.body, "accessToken");
tokens.clientToken = JsonGetString(resp.body, "clientToken");
std::string profile = JsonRawValue(resp.body, "selectedProfile");
if (!profile.empty()) {
tokens.uuid = JsonGetString(profile, "id");
tokens.username = JsonGetString(profile, "name");
}
return true;
}
error = "ElybyRefresh HTTP " + std::to_string(resp.statusCode);
if (!resp.body.empty()) {
std::string msg = JsonGetString(resp.body, "errorMessage");
if (!msg.empty()) error += ": " + msg;
}
} catch (const std::exception& e) {
error = std::string("ElybyRefresh network error: ") + e.what();
}
return false;
}
bool ElybyValidate(const std::string& accessToken, std::string& error)
{
std::string body = "{\"accessToken\":" + JsonStr(accessToken) + "}";
try {
auto resp = HttpPost("https://authserver.ely.by/auth/validate",
body, "application/json");
return (resp.statusCode == 204 || resp.statusCode == 200);
} catch (const std::exception& e) {
error = std::string("ElybyValidate network error: ") + e.what();
}
return false;
}
bool ElybyLoadTokens(const std::string& path, ElybyTokens& out)
{
std::ifstream f(path);
if (!f) return false;
std::string content((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
if (content.empty()) return false;
out.accessToken = JsonGetString(content, "accessToken");
out.clientToken = JsonGetString(content, "clientToken");
out.uuid = JsonGetString(content, "uuid");
out.username = JsonGetString(content, "username");
return !out.accessToken.empty();
}
bool ElybySaveTokens(const std::string& path, const ElybyTokens& tokens)
{
std::ofstream f(path);
if (!f) return false;
f << ElybySerializeTokens(tokens);
return f.good();
}
std::string ElybySerializeTokens(const ElybyTokens& tokens)
{
std::ostringstream o;
o << "{\n";
o << "\"accessToken\":" << JsonStr(tokens.accessToken) << ",\n";
o << "\"clientToken\":" << JsonStr(tokens.clientToken) << ",\n";
o << "\"uuid\":" << JsonStr(tokens.uuid) << ",\n";
o << "\"username\":" << JsonStr(tokens.username) << "\n";
o << "}";
return o.str();
}
bool ElybyDeserializeTokens(const std::string& json, ElybyTokens& out)
{
if (json.empty()) return false;
out.accessToken = JsonGetString(json, "accessToken");
out.clientToken = JsonGetString(json, "clientToken");
out.uuid = JsonGetString(json, "uuid");
out.username = JsonGetString(json, "username");
return !out.accessToken.empty();
}
}

152
newauth/src/newauthHttp.cpp Normal file
View file

@ -0,0 +1,152 @@
#include "newauthHttp.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
#include <stdexcept>
#include <string>
namespace newauth {
struct ParsedUrl {
std::wstring scheme;
std::wstring host;
INTERNET_PORT port;
std::wstring path;
};
static std::wstring Utf8ToWide(const std::string& s) {
if (s.empty()) return {};
int len = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0);
std::wstring w(len, L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), w.data(), len);
return w;
}
static std::string WideToUtf8(const std::wstring& w) {
if (w.empty()) return {};
int len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), nullptr, 0, nullptr, nullptr);
std::string s(len, '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), s.data(), len, nullptr, nullptr);
return s;
}
static ParsedUrl ParseUrl(const std::string& url) {
std::wstring wurl = Utf8ToWide(url);
URL_COMPONENTS uc = {};
uc.dwStructSize = sizeof(uc);
wchar_t scheme[16] = {}, host[256] = {}, path[2048] = {};
uc.lpszScheme = scheme; uc.dwSchemeLength = _countof(scheme);
uc.lpszHostName = host; uc.dwHostNameLength = _countof(host);
uc.lpszUrlPath = path; uc.dwUrlPathLength = _countof(path);
if (!WinHttpCrackUrl(wurl.c_str(), (DWORD)wurl.size(), 0, &uc))
throw std::runtime_error("ParseUrl: WinHttpCrackUrl failed for: " + url);
ParsedUrl p;
p.scheme = scheme;
p.host = host;
p.port = uc.nPort;
p.path = path;
if (p.path.empty()) p.path = L"/";
return p;
}
struct WinHttpHandle {
HINTERNET h = nullptr;
WinHttpHandle() = default;
explicit WinHttpHandle(HINTERNET handle) : h(handle) {}
~WinHttpHandle() { if (h) WinHttpCloseHandle(h); }
WinHttpHandle(const WinHttpHandle&) = delete;
WinHttpHandle& operator=(const WinHttpHandle&) = delete;
WinHttpHandle(WinHttpHandle&& o) noexcept : h(o.h) { o.h = nullptr; }
operator HINTERNET() const { return h; }
explicit operator bool() const { return h != nullptr; }
};
static HttpResponse DoRequest(const std::wstring& method,
const ParsedUrl& parsed,
const std::string& body,
const std::string& contentType,
const std::map<std::string, std::string>& headers,
const std::string& urlForErrors)
{
bool isHttps = (parsed.scheme == L"https");
WinHttpHandle session(WinHttpOpen(
L"newauth/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0));
if (!session)
throw std::runtime_error("WinHttpOpen failed");
WinHttpSetTimeouts(session, 10000, 10000, 10000, 15000);
WinHttpHandle connect(WinHttpConnect(session, parsed.host.c_str(), parsed.port, 0));
if (!connect)
throw std::runtime_error("WinHttpConnect failed for host: " + WideToUtf8(parsed.host));
DWORD flags = isHttps ? WINHTTP_FLAG_SECURE : 0;
WinHttpHandle request(WinHttpOpenRequest(
connect, method.c_str(), parsed.path.c_str(),
nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags));
if (!request)
throw std::runtime_error("WinHttpOpenRequest failed");
if (!contentType.empty()) {
std::wstring ct = L"Content-Type: " + Utf8ToWide(contentType);
WinHttpAddRequestHeaders(request, ct.c_str(), (DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD);
}
for (auto& kv : headers) {
std::wstring h = Utf8ToWide(kv.first) + L": " + Utf8ToWide(kv.second);
WinHttpAddRequestHeaders(request, h.c_str(), (DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD);
}
BOOL sent = WinHttpSendRequest(
request,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
body.empty() ? WINHTTP_NO_REQUEST_DATA : const_cast<char*>(body.c_str()),
(DWORD)body.size(), (DWORD)body.size(), 0);
if (!sent || !WinHttpReceiveResponse(request, nullptr))
throw std::runtime_error("request failed for: " + urlForErrors);
DWORD statusCode = 0, statusLen = sizeof(statusCode);
WinHttpQueryHeaders(request,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX,
&statusCode, &statusLen, WINHTTP_NO_HEADER_INDEX);
std::string responseBody;
DWORD bytesAvailable = 0;
while (WinHttpQueryDataAvailable(request, &bytesAvailable) && bytesAvailable > 0) {
std::string chunk(bytesAvailable, '\0');
DWORD bytesRead = 0;
WinHttpReadData(request, chunk.data(), bytesAvailable, &bytesRead);
responseBody.append(chunk.data(), bytesRead);
}
return { (int)statusCode, std::move(responseBody) };
}
HttpResponse HttpPost(const std::string& url,
const std::string& body,
const std::string& contentType,
const std::map<std::string, std::string>& headers)
{
return DoRequest(L"POST", ParseUrl(url), body, contentType, headers, url);
}
HttpResponse HttpGet(const std::string& url,
const std::map<std::string, std::string>& headers)
{
return DoRequest(L"GET", ParseUrl(url), "", "", headers, url);
}
}

20
newauth/src/newauthHttp.h Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <string>
#include <map>
namespace newauth {
struct HttpResponse {
int statusCode;
std::string body;
};
HttpResponse HttpPost(const std::string& url,
const std::string& body,
const std::string& contentType,
const std::map<std::string, std::string>& headers = {});
HttpResponse HttpGet(const std::string& url,
const std::map<std::string, std::string>& headers = {});
}

View file

@ -0,0 +1,205 @@
#pragma once
#include <string>
#include <vector>
#include <map>
#include <cstdint>
#include <chrono>
#include <ctime>
#include <cstdio>
#include <cctype>
namespace newauth {
inline std::string JsonRawValue(const std::string& json, const std::string& key) {
std::string search = "\"" + key + "\"";
size_t pos = 0;
while ((pos = json.find(search, pos)) != std::string::npos) {
size_t colon = json.find_first_not_of(" \t\r\n", pos + search.size());
if (colon == std::string::npos || json[colon] != ':') { pos++; continue; }
size_t vstart = json.find_first_not_of(" \t\r\n", colon + 1);
if (vstart == std::string::npos) return "";
char c = json[vstart];
if (c == '"') {
size_t end = vstart + 1;
while (end < json.size()) {
if (json[end] == '\\') { end += 2; continue; }
if (json[end] == '"') { end++; break; }
end++;
}
return json.substr(vstart, end - vstart);
} else if (c == '{' || c == '[') {
char open = c, close = (c == '{') ? '}' : ']';
int depth = 1;
size_t end = vstart + 1;
while (end < json.size() && depth > 0) {
if (json[end] == open) depth++;
else if (json[end] == close) depth--;
else if (json[end] == '"') {
end++;
while (end < json.size() && json[end] != '"') {
if (json[end] == '\\') end++;
end++;
}
}
end++;
}
return json.substr(vstart, end - vstart);
} else {
size_t end = json.find_first_of(",}\r\n", vstart);
if (end == std::string::npos) end = json.size();
std::string v = json.substr(vstart, end - vstart);
while (!v.empty() && isspace((unsigned char)v.back())) v.pop_back();
return v;
}
}
return "";
}
inline std::string JsonUnquote(const std::string& s) {
if (s.size() >= 2 && s.front() == '"' && s.back() == '"')
return s.substr(1, s.size() - 2);
return s;
}
inline std::string JsonGetString(const std::string& json, const std::string& key) {
return JsonUnquote(JsonRawValue(json, key));
}
inline int64_t JsonGetInt(const std::string& json, const std::string& key) {
auto s = JsonUnquote(JsonRawValue(json, key));
if (s.empty()) return 0;
try { return std::stoll(s); } catch (...) { return 0; }
}
inline std::string JsonFirstArrayObject(const std::string& arrStr) {
size_t pos = arrStr.find('[');
if (pos == std::string::npos) return "";
pos = arrStr.find_first_not_of(" \t\r\n", pos + 1);
if (pos == std::string::npos || arrStr[pos] == ']') return "";
if (arrStr[pos] != '{') return "";
int depth = 1;
size_t end = pos + 1;
while (end < arrStr.size() && depth > 0) {
if (arrStr[end] == '{') depth++;
else if (arrStr[end] == '}') depth--;
else if (arrStr[end] == '"') {
end++;
while (end < arrStr.size() && arrStr[end] != '"') {
if (arrStr[end] == '\\') end++;
end++;
}
}
end++;
}
return arrStr.substr(pos, end - pos);
}
inline std::string JsonNthArrayString(const std::string& arrStr, int n) {
size_t pos = arrStr.find('[');
if (pos == std::string::npos) return "";
pos++;
int idx = 0;
while (pos < arrStr.size()) {
pos = arrStr.find_first_not_of(" \t\r\n,", pos);
if (pos == std::string::npos || arrStr[pos] == ']') break;
if (arrStr[pos] == '"') {
size_t end = pos + 1;
while (end < arrStr.size()) {
if (arrStr[end] == '\\') { end += 2; continue; }
if (arrStr[end] == '"') { end++; break; }
end++;
}
if (idx == n) return arrStr.substr(pos + 1, end - pos - 2);
pos = end;
idx++;
} else {
pos++;
}
}
return "";
}
inline std::vector<std::string> JsonArrayObjects(const std::string& json) {
std::vector<std::string> result;
size_t pos = json.find('[');
if (pos == std::string::npos) return result;
++pos;
while (pos < json.size()) {
pos = json.find('{', pos);
if (pos == std::string::npos) break;
int depth = 0;
size_t start = pos;
for (; pos < json.size(); ++pos) {
if (json[pos] == '{') ++depth;
else if (json[pos] == '}') { --depth; if (depth == 0) { ++pos; break; } }
}
result.push_back(json.substr(start, pos - start));
}
return result;
}
inline std::string UrlEncode(const std::string& s) {
std::string out;
out.reserve(s.size() * 3);
for (unsigned char c : s) {
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
out += c;
} else {
char buf[4];
snprintf(buf, sizeof(buf), "%%%02X", c);
out += buf;
}
}
return out;
}
inline int64_t NowMs() {
using namespace std::chrono;
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}
inline std::string BuildFormBody(const std::map<std::string, std::string>& params) {
std::string body;
for (auto& kv : params) {
if (!body.empty()) body += '&';
body += UrlEncode(kv.first) + '=' + UrlEncode(kv.second);
}
return body;
}
inline std::string JsonEscape(const std::string& s) {
std::string out;
for (char c : s) {
if (c == '"') out += "\\\"";
else if (c == '\\') out += "\\\\";
else if (c == '\n') out += "\\n";
else if (c == '\r') out += "\\r";
else out += c;
}
return out;
}
inline std::string JsonStr(const std::string& s) {
return "\"" + JsonEscape(s) + "\"";
}
inline int64_t ParseIso8601Ms(const std::string& s) {
int Y=0, M=0, D=0, h=0, m=0;
double sec=0.0;
if (sscanf_s(s.c_str(), "%d-%d-%dT%d:%d:%lf", &Y, &M, &D, &h, &m, &sec) < 6)
return 0;
struct tm t = {};
t.tm_year = Y - 1900;
t.tm_mon = M - 1;
t.tm_mday = D;
t.tm_hour = h;
t.tm_min = m;
t.tm_sec = (int)sec;
t.tm_isdst = 0;
time_t epoch = _mkgmtime(&t);
if (epoch == (time_t)-1) return 0;
int ms = (int)((sec - (int)sec) * 1000.0);
return (int64_t)epoch * 1000LL + ms;
}
}

423
newauth/src/newauthJava.cpp Normal file
View file

@ -0,0 +1,423 @@
#include "../include/newauth.h"
#include "newauthCrypto.h"
#include "newauthHttp.h"
#include "newauthInternal.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <bcrypt.h>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <chrono>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <ctime>
#include <map>
namespace newauth {
using namespace newauth;
struct JavaAuthManager::Impl {
struct MsaState {
std::string accessToken, refreshToken;
int64_t expireMs = 0;
bool IsExpired() const { return NowMs() >= expireMs - 60000; }
} msa;
ECKeyPair deviceKeyP256;
std::string deviceId;
struct XblDeviceState {
std::string token, did;
int64_t expireMs = 0;
bool IsExpired() const { return NowMs() >= expireMs - 60000; }
} xblDevice;
struct JavaXstsState {
std::string token, uhs;
int64_t expireMs = 0;
bool IsExpired() const { return NowMs() >= expireMs - 60000; }
} javaXsts;
struct MinecraftTokenState {
std::string tokenType, accessToken;
int64_t expireMs = 0;
std::string AuthHeader() const { return tokenType + " " + accessToken; }
bool IsExpired() const { return NowMs() >= expireMs - 60000; }
} mcToken;
std::atomic<bool> loggedIn{false};
std::atomic<bool> cancelRequested{false};
std::mutex cancelMutex;
std::condition_variable cancelCv;
bool SleepOrCancel(int64_t ms) {
std::unique_lock<std::mutex> lock(cancelMutex);
return cancelCv.wait_for(lock, std::chrono::milliseconds(ms), [this] {
return cancelRequested.load(std::memory_order_relaxed);
});
}
Impl() {
deviceKeyP256 = GenerateP256KeyPair();
deviceId = GenerateUUID();
}
struct DevCodeState {
std::string deviceCode, userCode, verificationUri;
int64_t expiresMs, intervalMs;
};
DevCodeState RequestDeviceCode() {
auto resp = HttpPost(
"https://login.live.com/oauth20_connect.srf",
BuildFormBody({{"client_id", JAVA_CLIENT_ID},
{"scope", MSA_SCOPE},
{"response_type", "device_code"}}),
"application/x-www-form-urlencoded");
if (resp.statusCode != 200)
throw std::runtime_error("Java DeviceCode HTTP " +
std::to_string(resp.statusCode) + ": " + resp.body);
DevCodeState dc;
dc.deviceCode = JsonGetString(resp.body, "device_code");
dc.userCode = JsonGetString(resp.body, "user_code");
dc.verificationUri = JsonGetString(resp.body, "verification_uri");
if (dc.deviceCode.empty() || dc.userCode.empty() || dc.verificationUri.empty())
throw std::runtime_error("Java DeviceCode: missing required fields in response");
int64_t expiresIn = JsonGetInt(resp.body, "expires_in");
int64_t intervalIn = JsonGetInt(resp.body, "interval");
dc.expiresMs = NowMs() + (expiresIn > 0 ? expiresIn : 300) * 1000;
dc.intervalMs = (intervalIn > 0 ? intervalIn : 5) * 1000;
return dc;
}
bool PollMsaToken(const DevCodeState& dc, int timeoutSec, std::string& error) {
int64_t deadline = NowMs() + (int64_t)timeoutSec * 1000;
int64_t intervalMs = dc.intervalMs > 0 ? dc.intervalMs : 5000;
while (NowMs() < deadline && NowMs() < dc.expiresMs) {
if (SleepOrCancel(intervalMs)) {
error = "cancelled";
return false;
}
HttpResponse resp;
try {
resp = HttpPost(
"https://login.live.com/oauth20_token.srf",
BuildFormBody({{"client_id", JAVA_CLIENT_ID},
{"scope", MSA_SCOPE},
{"grant_type", "device_code"},
{"device_code", dc.deviceCode}}),
"application/x-www-form-urlencoded");
} catch (const std::exception& ex) {
error = std::string("MSA poll network error: ") + ex.what();
return false;
}
if (resp.statusCode == 200) {
msa.accessToken = JsonGetString(resp.body, "access_token");
msa.refreshToken = JsonGetString(resp.body, "refresh_token");
msa.expireMs = NowMs() + JsonGetInt(resp.body, "expires_in") * 1000;
return true;
}
std::string err = JsonGetString(resp.body, "error");
if (err == "authorization_pending") continue;
if (err == "slow_down") { intervalMs += 5000; continue; }
error = "MSA poll error: " + err;
return false;
}
error = "MSA device code login timed out";
return false;
}
bool RefreshMsaToken(std::string& error) {
if (msa.refreshToken.empty()) { error = "No refresh token"; return false; }
HttpResponse resp;
try {
resp = HttpPost(
"https://login.live.com/oauth20_token.srf",
BuildFormBody({{"client_id", JAVA_CLIENT_ID},
{"scope", MSA_SCOPE},
{"grant_type", "refresh_token"},
{"refresh_token", msa.refreshToken}}),
"application/x-www-form-urlencoded");
} catch (const std::exception& ex) {
error = std::string("MSA refresh network error: ") + ex.what();
return false;
}
if (resp.statusCode != 200) {
error = "MSA refresh HTTP " + std::to_string(resp.statusCode);
return false;
}
msa.accessToken = JsonGetString(resp.body, "access_token");
std::string nr = JsonGetString(resp.body, "refresh_token");
if (!nr.empty()) msa.refreshToken = nr;
msa.expireMs = NowMs() + JsonGetInt(resp.body, "expires_in") * 1000;
return true;
}
bool AuthXblDevice(std::string& error) {
std::string proofKey = BuildProofKeyJson(deviceKeyP256);
std::string bodyJson =
"{\"Properties\":{"
"\"DeviceType\":\"Win32\","
"\"Id\":\"{" + deviceId + "}\","
"\"AuthMethod\":\"ProofOfPossession\","
"\"ProofKey\":" + proofKey +
"},\"RelyingParty\":\"http://auth.xboxlive.com\","
"\"TokenType\":\"JWT\"}";
std::string sig = BuildXblSignatureHeader(
deviceKeyP256, "POST", "/device/authenticate", bodyJson);
auto resp = HttpPost(
"https://device.auth.xboxlive.com/device/authenticate",
bodyJson, "application/json",
{{"x-xbl-contract-version","1"},{"Signature", sig}});
if (resp.statusCode != 200) {
error = "XBL device auth HTTP " + std::to_string(resp.statusCode);
return false;
}
xblDevice.token = JsonGetString(resp.body, "Token");
xblDevice.expireMs = ParseIso8601Ms(JsonGetString(resp.body, "NotAfter"));
std::string xdi = JsonRawValue(JsonRawValue(resp.body, "DisplayClaims"), "xdi");
xblDevice.did = JsonGetString(xdi, "did");
return true;
}
// god this protocol is convoluted
bool AuthSisu(std::string& error) {
std::string proofKey = BuildProofKeyJson(deviceKeyP256);
std::string bodyJson =
"{\"Sandbox\":\"RETAIL\","
"\"UseModernGamertag\":true,"
"\"AppId\":\"" + std::string(JAVA_CLIENT_ID) + "\","
"\"AccessToken\":\"t=" + msa.accessToken + "\","
"\"DeviceToken\":\"" + xblDevice.token + "\","
"\"ProofKey\":" + proofKey + ","
"\"RelyingParty\":\"rp://api.minecraftservices.com/\"}";
std::string sig = BuildXblSignatureHeader(
deviceKeyP256, "POST", "/authorize", bodyJson);
auto resp = HttpPost(
"https://sisu.xboxlive.com/authorize",
bodyJson, "application/json", {{"Signature", sig}});
if (resp.statusCode != 200) {
error = "SISU authorize HTTP " + std::to_string(resp.statusCode)
+ ": " + resp.body;
return false;
}
std::string authObj = JsonRawValue(resp.body, "AuthorizationToken");
javaXsts.token = JsonGetString(authObj, "Token");
javaXsts.expireMs = ParseIso8601Ms(JsonGetString(authObj, "NotAfter"));
std::string disp = JsonRawValue(authObj, "DisplayClaims");
std::string xui = JsonRawValue(disp, "xui");
std::string xui0 = JsonFirstArrayObject(xui);
javaXsts.uhs = JsonGetString(xui0, "uhs");
if (javaXsts.token.empty() || javaXsts.uhs.empty()) {
error = "SISU response missing XSTS token or uhs";
return false;
}
return true;
}
bool AuthMcLauncherLogin(std::string& error) {
std::string xtoken = "XBL3.0 x=" + javaXsts.uhs + ";" + javaXsts.token;
std::string bodyJson =
"{\"platform\":\"PC_LAUNCHER\","
"\"xtoken\":\"" + xtoken + "\"}";
auto resp = HttpPost(
"https://api.minecraftservices.com/launcher/login",
bodyJson, "application/json");
if (resp.statusCode != 200) {
error = "Launcher login HTTP " + std::to_string(resp.statusCode)
+ ": " + resp.body;
return false;
}
mcToken.tokenType = JsonGetString(resp.body, "token_type");
mcToken.accessToken = JsonGetString(resp.body, "access_token");
mcToken.expireMs = NowMs() + JsonGetInt(resp.body, "expires_in") * 1000;
return true;
}
bool FetchProfile(JavaSession& out, std::string& error) {
auto resp = HttpGet(
"https://api.minecraftservices.com/minecraft/profile",
{{"Authorization", mcToken.AuthHeader()}});
if (resp.statusCode == 404) {
error = "This Microsoft account does not own Minecraft Java Edition";
return false;
}
if (resp.statusCode != 200) {
error = "Profile HTTP " + std::to_string(resp.statusCode)
+ ": " + resp.body;
return false;
}
std::string rawId = JsonGetString(resp.body, "id");
out.uuid = DashUuid(rawId);
out.username = JsonGetString(resp.body, "name");
out.accessToken = mcToken.accessToken;
out.expireMs = mcToken.expireMs;
if (out.uuid.empty() || out.username.empty()) {
error = "Minecraft profile response missing uuid or username";
return false;
}
return true;
}
bool DoFullAuth(DeviceCodeCallback onDeviceCode,
JavaSession& out, std::string& error, int timeoutSec) {
cancelRequested.store(false, std::memory_order_relaxed);
DevCodeState dc;
try { dc = RequestDeviceCode(); }
catch (const std::exception& ex) {
error = std::string("DeviceCode network error: ") + ex.what();
return false;
}
DeviceCodeInfo info;
info.userCode = dc.userCode;
info.verificationUri = dc.verificationUri;
info.directUri = dc.verificationUri + "?otc=" + dc.userCode;
info.deviceCode = dc.deviceCode;
info.expiresMs = dc.expiresMs;
info.intervalMs = dc.intervalMs;
onDeviceCode(info);
if (!PollMsaToken(dc, timeoutSec, error)) return false;
return DoAuthChain(out, error);
}
bool DoAuthChain(JavaSession& out, std::string& error) {
try {
if (!AuthXblDevice(error)) return false;
if (!AuthSisu(error)) return false;
if (!AuthMcLauncherLogin(error)) return false;
if (!FetchProfile(out, error)) return false;
} catch (const std::runtime_error& ex) {
error = ex.what();
return false;
} catch (const std::exception& ex) {
error = std::string("Unexpected auth error: ") + ex.what();
return false;
} catch (...) {
error = "Unknown internal error during auth chain";
return false;
}
loggedIn = true;
return true;
}
std::string Serialise() const {
std::ostringstream o;
o << "{\n"
<< "\"msaAccessToken\":" << newauth::JsonStr(msa.accessToken) << ",\n"
<< "\"msaRefreshToken\":" << newauth::JsonStr(msa.refreshToken) << ",\n"
<< "\"msaExpireMs\":" << msa.expireMs << ",\n"
<< "\"deviceId\":" << newauth::JsonStr(deviceId) << ",\n"
<< "\"deviceKeyPriv\":" << newauth::JsonStr(Base64Encode(deviceKeyP256.privateBlob)) << ",\n"
<< "\"deviceKeyPub\":" << newauth::JsonStr(Base64Encode(deviceKeyP256.publicBlob)) << "\n"
<< "}";
return o.str();
}
static ECKeyPair RebuildP256(const std::vector<uint8_t>& priv,
const std::vector<uint8_t>& pub) {
if (pub.size() < sizeof(BCRYPT_ECCKEY_BLOB)) return {};
const BCRYPT_ECCKEY_BLOB* hdr =
reinterpret_cast<const BCRYPT_ECCKEY_BLOB*>(pub.data());
ULONG cbKey = hdr->cbKey;
if (pub.size() < 8 + (size_t)cbKey * 2) return {};
std::vector<uint8_t> x(pub.begin()+8, pub.begin()+8+cbKey);
std::vector<uint8_t> y(pub.begin()+8+cbKey, pub.begin()+8+cbKey*2);
ECKeyPair kp;
kp.bitSize = 256; kp.privateBlob = priv; kp.publicBlob = pub;
kp.x = x; kp.y = y;
kp.publicKeyDER = BuildSubjectPublicKeyInfoDER(256, x, y);
return kp;
}
bool Deserialise(const std::string& json) {
msa.accessToken = JsonGetString(json, "msaAccessToken");
msa.refreshToken = JsonGetString(json, "msaRefreshToken");
msa.expireMs = JsonGetInt(json, "msaExpireMs");
deviceId = JsonGetString(json, "deviceId");
auto priv = Base64Decode(JsonGetString(json, "deviceKeyPriv"));
auto pub = Base64Decode(JsonGetString(json, "deviceKeyPub"));
if (priv.empty() || pub.empty()) return false;
deviceKeyP256 = RebuildP256(priv, pub);
return true;
}
};
JavaAuthManager::JavaAuthManager() : m_impl(std::make_unique<Impl>()) {}
JavaAuthManager::~JavaAuthManager() = default;
bool JavaAuthManager::Login(DeviceCodeCallback onDeviceCode,
JavaSession& out, std::string& error, int timeoutSec)
{
return m_impl->DoFullAuth(onDeviceCode, out, error, timeoutSec);
}
bool JavaAuthManager::Refresh(JavaSession& out, std::string& error) {
m_impl->cancelRequested.store(false, std::memory_order_relaxed);
if (!m_impl->RefreshMsaToken(error)) return false;
return m_impl->DoAuthChain(out, error);
}
bool JavaAuthManager::IsLoggedIn() const { return m_impl->loggedIn; }
void JavaAuthManager::Logout() {
m_impl->msa = {};
m_impl->javaXsts = {};
m_impl->mcToken = {};
m_impl->loggedIn = false;
}
void JavaAuthManager::RequestCancel() {
m_impl->cancelRequested.store(true, std::memory_order_release);
m_impl->cancelCv.notify_all();
}
bool JavaAuthManager::SaveTokens(const std::string& path) const {
std::ofstream f(path);
if (!f) return false;
f << m_impl->Serialise();
return f.good();
}
bool JavaAuthManager::LoadTokens(const std::string& path) {
std::ifstream f(path);
if (!f) return false;
std::string content((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
return m_impl->Deserialise(content);
}
std::string JavaAuthManager::SerializeTokens() const {
return m_impl->Serialise();
}
bool JavaAuthManager::DeserializeTokens(const std::string& json) {
return m_impl->Deserialise(json);
}
}

View file

@ -0,0 +1,956 @@
#define _CRT_SECURE_NO_WARNINGS
#include "../include/newauthManager.h"
#include "newauthInternal.h"
#include "newauthDb.h"
#include <cstdio>
#include <sstream>
#include <fstream>
#include <chrono>
#include <thread>
#ifdef _WIN32
#include <Windows.h>
#endif
static void AuthLogImpl(const char* fmt, ...) {
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
#ifdef _WIN32
OutputDebugStringA(buf);
#endif
FILE* f = _fsopen("newauth_debug.log", "a", _SH_DENYWR);
if (f) { fputs(buf, f); fclose(f); }
}
#define AUTH_LOG(msg, ...) AuthLogImpl("[newauth] " msg "\n", ##__VA_ARGS__)
using namespace newauth;
newauthManager& newauthManager::Get() {
static newauthManager instance;
return instance;
}
newauthManager::newauthManager() = default;
newauthManager::~newauthManager() {
for (int i = 0; i < XUSER_MAX_COUNT; ++i) {
++m_slots[i].generation;
if (m_slots[i].auth)
m_slots[i].auth->RequestCancel();
}
}
std::shared_ptr<newauth::JavaAuthManager> newauthManager::ResetSlotAuth(AuthSlot& s) {
if (s.auth)
s.auth->RequestCancel();
auto fresh = std::make_shared<newauth::JavaAuthManager>();
s.auth = fresh;
return fresh;
}
newauth::JavaSession newauthManager::SynthesizeOfflineSession(const JavaAccountInfo& acct) {
newauth::JavaSession s;
s.username = acct.username;
s.uuid = acct.uuid;
s.accessToken = "";
s.expireMs = 0;
return s;
}
bool newauthManager::LoadJavaAccountIndex() {
auto& db = AuthDb::Get();
if (!db.Open()) {
AUTH_LOG("LoadJavaAccountIndex: DB open failed");
return false;
}
db.MigrateFromJson(kJavaAccountsFile);
int activeIndex = -1;
std::vector<AccountRow> rows;
if (!db.LoadAccounts(activeIndex, rows)) {
AUTH_LOG("LoadJavaAccountIndex: DB LoadAccounts failed");
return false;
}
std::lock_guard<std::mutex> lock(m_accountsMutex);
m_javaAccounts.clear();
for (auto& row : rows) {
JavaAccountInfo info;
info.username = row.username;
info.uuid = row.uuid;
info.tokenFile = row.tokenKey;
info.isOffline = row.isOffline;
info.authProvider = row.authProvider;
if (info.authProvider.empty())
info.authProvider = info.isOffline ? "offline" : "mojang";
if (!info.tokenFile.empty() || info.isOffline)
m_javaAccounts.push_back(std::move(info));
}
if (activeIndex >= 0 && activeIndex < (int)m_javaAccounts.size())
m_slots[0].accountIndex = activeIndex;
else
m_slots[0].accountIndex = m_javaAccounts.empty() ? -1 : 0;
AUTH_LOG("LoadJavaAccountIndex: loaded %d accounts, active=%d",
(int)m_javaAccounts.size(), m_slots[0].accountIndex.load());
return true;
}
bool newauthManager::SaveJavaAccountIndex() const {
std::lock_guard<std::mutex> lock(m_accountsMutex);
std::vector<AccountRow> rows;
for (auto& a : m_javaAccounts) {
AccountRow row;
row.username = a.username;
row.uuid = a.uuid;
row.tokenKey = a.tokenFile;
row.isOffline = a.isOffline;
row.authProvider = a.authProvider.empty() ? "mojang" : a.authProvider;
rows.push_back(std::move(row));
}
int activeIndex = m_slots[0].accountIndex.load();
if (!AuthDb::Get().SaveAccounts(activeIndex, rows)) {
AUTH_LOG("SaveJavaAccountIndex: DB SaveAccounts failed");
return false;
}
AUTH_LOG("SaveJavaAccountIndex: saved %d accounts, active=%d",
(int)m_javaAccounts.size(), activeIndex);
return true;
}
std::vector<newauthManager::JavaAccountInfo> newauthManager::GetJavaAccounts() const {
std::lock_guard<std::mutex> lock(m_accountsMutex);
return m_javaAccounts;
}
std::string newauthManager::AllocTokenFile(const std::string& uuid) const {
if (!uuid.empty())
return uuid + ".json";
for (int i = 0; ; ++i) {
char buf[64];
snprintf(buf, sizeof(buf), "java_auth_%d.json", i);
std::string name(buf);
bool taken = false;
for (auto& a : m_javaAccounts) {
if (a.tokenFile == name) { taken = true; break; }
}
if (!taken) return name;
}
}
const newauthManager::AuthSlot& newauthManager::GetSlot(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) slot = 0;
return m_slots[slot];
}
newauth::JavaSession newauthManager::GetSlotSession(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) slot = 0;
auto& s = m_slots[slot];
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()
&& m_javaAccounts[s.accountIndex].isOffline)
return SynthesizeOfflineSession(m_javaAccounts[s.accountIndex]);
}
std::lock_guard<std::mutex> lock(s.mutex);
return s.session;
}
bool newauthManager::IsSlotLoggedIn(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return false;
auto& s = m_slots[slot];
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()) {
if (m_javaAccounts[s.accountIndex].isOffline)
return true;
if (m_javaAccounts[s.accountIndex].authProvider == "elyby") {
std::lock_guard<std::mutex> slock(s.mutex);
return !s.elybyTokens.accessToken.empty();
}
}
}
return s.auth && s.auth->IsLoggedIn();
}
bool newauthManager::IsTokenExpiringSoon(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return false;
auto& s = m_slots[slot];
std::lock_guard<std::mutex> lock(s.mutex);
if (s.session.expireMs <= 0) return false;
return NowMs() > s.session.expireMs - 60000;
}
bool newauthManager::IsAccountInUseByOtherSlot(int slot, int accountIndex) const {
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (accountIndex < 0 || accountIndex >= (int)m_javaAccounts.size()) return false;
for (int i = 0; i < XUSER_MAX_COUNT; ++i) {
if (i == slot) continue;
if (m_slots[i].accountIndex == accountIndex) return true;
}
return false;
}
void newauthManager::RunElybyRefresh(int slot, uint32_t gen, const std::string& tokenFile,
bool failOpen, bool alwaysSaveIndex) {
auto& s = m_slots[slot];
std::string error;
newauth::ElybyTokens tokens;
{
std::lock_guard<std::mutex> lock(s.mutex);
tokens = s.elybyTokens;
}
bool ok = newauth::ElybyRefresh(tokens, error);
if (s.generation != gen) return;
{
std::lock_guard<std::mutex> lock(s.mutex);
if (ok) {
s.elybyTokens = tokens;
s.session.username = tokens.username;
s.session.uuid = newauth::DashUuid(tokens.uuid);
s.session.accessToken = tokens.accessToken;
s.lastError.clear();
s.state = State::Success;
} else {
s.lastError = error;
s.state = failOpen ? State::Success : State::Failed;
}
s.cv.notify_all();
}
if (ok) {
if (!tokenFile.empty())
AuthDb::Get().SaveTokenBlob(tokenFile, newauth::ElybySerializeTokens(tokens));
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()) {
m_javaAccounts[s.accountIndex].username = tokens.username;
m_javaAccounts[s.accountIndex].uuid = newauth::DashUuid(tokens.uuid);
}
}
}
if (ok || alwaysSaveIndex)
SaveJavaAccountIndex();
}
void newauthManager::RefreshSlot(int slot) {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return;
auto& s = m_slots[slot];
std::string provider;
std::string elyTokenFile;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()) {
provider = m_javaAccounts[s.accountIndex].authProvider;
elyTokenFile = m_javaAccounts[s.accountIndex].tokenFile;
}
}
if (provider == "elyby") {
uint32_t gen = ++s.generation;
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Authenticating;
s.cv.notify_all();
}
std::thread([this, slot, gen, elyTokenFile]() {
RunElybyRefresh(slot, gen, elyTokenFile, /*failOpen=*/false, /*alwaysSaveIndex=*/false);
}).detach();
return;
}
uint32_t gen = ++s.generation;
auto authCopy = s.auth;
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Authenticating;
s.cv.notify_all();
}
std::thread([this, slot, gen, authCopy]() {
auto& s = m_slots[slot];
std::string error;
newauth::JavaSession session;
bool ok = false;
try {
ok = authCopy->Refresh(session, error);
} catch (const std::exception& ex) {
error = std::string("RefreshSlot network error: ") + ex.what();
} catch (...) {
error = "RefreshSlot unknown error";
}
if (s.generation != gen) {
AUTH_LOG("RefreshSlot(%d): generation mismatch (%u vs %u), discarding",
slot, gen, s.generation.load());
return;
}
std::string tokenFile;
{
std::lock_guard<std::mutex> lock(s.mutex);
if (ok) {
s.session = session;
s.lastError.clear();
s.state = State::Success;
} else {
s.lastError = error;
s.state = State::Failed;
}
s.cv.notify_all();
}
if (ok) {
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()) {
m_javaAccounts[s.accountIndex].username = session.username;
m_javaAccounts[s.accountIndex].uuid = session.uuid;
tokenFile = m_javaAccounts[s.accountIndex].tokenFile;
}
}
AUTH_LOG("RefreshSlot(%d): ok=%d, username='%s', error='%s'",
slot, (int)ok, ok ? session.username.c_str() : "", error.c_str());
if (ok && !tokenFile.empty()) {
AuthDb::Get().SaveTokenBlob(tokenFile, authCopy->SerializeTokens());
SaveJavaAccountIndex();
}
}).detach();
}
bool newauthManager::SetAccountForSlot(int slot, int accountIndex) {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return false;
auto& s = m_slots[slot];
bool isOffline = false;
bool isElyby = false;
std::string tokenFile;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (accountIndex < 0 || accountIndex >= (int)m_javaAccounts.size()) return false;
isOffline = m_javaAccounts[accountIndex].isOffline;
isElyby = (m_javaAccounts[accountIndex].authProvider == "elyby");
tokenFile = m_javaAccounts[accountIndex].tokenFile;
}
uint32_t gen = ++s.generation;
if (isOffline) {
newauth::JavaSession offlineSession;
std::string offlineName;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
offlineSession = SynthesizeOfflineSession(m_javaAccounts[accountIndex]);
offlineName = m_javaAccounts[accountIndex].username;
}
{
std::lock_guard<std::mutex> lock(s.mutex);
s.accountIndex = accountIndex;
s.session = offlineSession;
s.state = State::Success;
s.cv.notify_all();
}
AUTH_LOG("SetAccountForSlot(%d): switched to offline account %d '%s'",
slot, accountIndex, offlineName.c_str());
(void)gen;
} else if (isElyby) {
{
std::lock_guard<std::mutex> lock(s.mutex);
s.accountIndex = accountIndex;
}
AUTH_LOG("SetAccountForSlot(%d): switching to elyby account %d, tokenFile='%s'",
slot, accountIndex, tokenFile.c_str());
newauth::ElybyTokens elyTokens;
{
std::string blob = AuthDb::Get().LoadTokenBlob(tokenFile);
if (blob.empty() || !newauth::ElybyDeserializeTokens(blob, elyTokens)) {
AUTH_LOG("SetAccountForSlot(%d): ElybyLoadTokens from DB failed", slot);
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Failed;
s.cv.notify_all();
return false;
}
}
{
std::lock_guard<std::mutex> lock(s.mutex);
s.elybyTokens = elyTokens;
s.session.username = elyTokens.username;
s.session.uuid = newauth::DashUuid(elyTokens.uuid);
s.session.accessToken = elyTokens.accessToken;
s.session.expireMs = 0;
s.state = State::Authenticating;
s.cv.notify_all();
}
uint32_t elyGen = ++s.generation;
std::thread([this, slot, elyGen, tokenFile]() {
RunElybyRefresh(slot, elyGen, tokenFile, /*failOpen=*/true, /*alwaysSaveIndex=*/true);
}).detach();
} else {
{
std::lock_guard<std::mutex> lock(s.mutex);
s.accountIndex = accountIndex;
}
AUTH_LOG("SetAccountForSlot(%d): switching to online account %d, tokenFile='%s'",
slot, accountIndex, tokenFile.c_str());
auto freshAuth = ResetSlotAuth(s);
{
std::string blob = AuthDb::Get().LoadTokenBlob(tokenFile);
if (blob.empty() || !freshAuth->DeserializeTokens(blob)) {
AUTH_LOG("SetAccountForSlot(%d): LoadTokens from DB failed", slot);
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Failed;
s.cv.notify_all();
return false;
}
}
RefreshSlot(slot);
}
SaveJavaAccountIndex();
return true;
}
void newauthManager::ClearSlot(int slot) {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return;
auto& s = m_slots[slot];
++s.generation;
ResetSlotAuth(s);
std::lock_guard<std::mutex> lock(s.mutex);
s.session = {};
s.accountIndex = -1;
s.state = State::Idle;
s.lastError.clear();
s.cv.notify_all();
}
bool newauthManager::WaitForSlotReady(int slot, int timeoutMs) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return false;
auto& s = m_slots[slot];
std::unique_lock<std::mutex> lock(s.mutex);
bool ok = s.cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), [&] {
State st = s.state.load();
return st != State::Authenticating && st != State::WaitingForCode;
});
return ok && (s.state == State::Success);
}
int newauthManager::GetActiveJavaAccountIndex() const {
return m_slots[0].accountIndex;
}
newauth::JavaSession newauthManager::GetJavaSession() const {
return GetSlotSession(0);
}
bool newauthManager::IsJavaLoggedIn() const {
return IsSlotLoggedIn(0);
}
int newauthManager::AddOfflineJavaAccount(const std::string& username) {
if (username.empty()) return -1;
std::string uuid = newauth::GenerateOfflineUuid(username);
if (uuid.empty()) return -1;
std::lock_guard<std::mutex> alock(m_accountsMutex);
for (int i = 0; i < (int)m_javaAccounts.size(); ++i) {
if (m_javaAccounts[i].uuid == uuid) {
m_javaAccounts[i].username = username;
{
std::lock_guard<std::mutex> lock(m_slots[0].mutex);
m_slots[0].accountIndex = i;
m_slots[0].session = SynthesizeOfflineSession(m_javaAccounts[i]);
m_slots[0].state = State::Success;
m_slots[0].cv.notify_all();
}
AUTH_LOG("AddOfflineJavaAccount: updated existing offline account idx=%d '%s'",
i, username.c_str());
return i;
}
}
JavaAccountInfo info;
info.username = username;
info.uuid = uuid;
info.tokenFile = "";
info.isOffline = true;
info.authProvider = "offline";
m_javaAccounts.push_back(std::move(info));
int idx = (int)m_javaAccounts.size() - 1;
{
std::lock_guard<std::mutex> lock(m_slots[0].mutex);
m_slots[0].accountIndex = idx;
m_slots[0].session = SynthesizeOfflineSession(m_javaAccounts[idx]);
m_slots[0].state = State::Success;
m_slots[0].cv.notify_all();
}
AUTH_LOG("AddOfflineJavaAccount: added new offline account idx=%d '%s' uuid='%s'",
idx, username.c_str(), uuid.c_str());
return idx;
}
void newauthManager::BeginAddJavaAccount(DeviceCodeCb onDeviceCode,
JavaCompleteCb onComplete,
int timeoutSeconds)
{
auto& s = m_slots[0];
uint32_t gen = ++s.generation;
{
std::lock_guard<std::mutex> lock(m_deviceCodeMutex);
m_javaDeviceCode.clear();
m_javaDirectUri.clear();
}
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::WaitingForCode;
s.cv.notify_all();
}
auto freshAuth = ResetSlotAuth(s);
std::thread([this, freshAuth, onDeviceCode, onComplete, timeoutSeconds, gen]() {
try {
auto& s = m_slots[0];
newauth::JavaSession session;
std::string error;
bool ok = freshAuth->Login(
[&](const newauth::DeviceCodeInfo& dc) {
if (s.generation != gen) return;
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::WaitingForCode;
s.cv.notify_all();
}
{
std::lock_guard<std::mutex> lock(m_deviceCodeMutex);
m_javaDeviceCode = dc.userCode;
m_javaDirectUri = dc.directUri;
}
if (onDeviceCode) onDeviceCode(dc);
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Authenticating;
s.cv.notify_all();
}
},
session, error, timeoutSeconds);
{
std::lock_guard<std::mutex> lock(s.mutex);
if (s.generation != gen) {
AUTH_LOG("BeginAddJavaAccount: gen %u stale (current %u), discarding",
gen, s.generation.load());
if (onComplete) onComplete(false, {}, "cancelled");
return;
}
}
if (ok) {
std::string tokenFile;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
int existingIdx = -1;
for (int i = 0; i < (int)m_javaAccounts.size(); ++i) {
if (m_javaAccounts[i].uuid == session.uuid) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_javaAccounts[existingIdx].username = session.username;
tokenFile = m_javaAccounts[existingIdx].tokenFile;
s.accountIndex = existingIdx;
AUTH_LOG("BeginAddJavaAccount: updated existing account idx=%d '%s'",
existingIdx, session.username.c_str());
} else {
JavaAccountInfo info;
info.username = session.username;
info.uuid = session.uuid;
info.tokenFile = AllocTokenFile(session.uuid);
info.authProvider = "mojang";
tokenFile = info.tokenFile;
m_javaAccounts.push_back(std::move(info));
s.accountIndex = (int)m_javaAccounts.size() - 1;
AUTH_LOG("BeginAddJavaAccount: added new account idx=%d '%s' file='%s'",
s.accountIndex.load(), session.username.c_str(),
m_javaAccounts.back().tokenFile.c_str());
}
}
if (!tokenFile.empty())
AuthDb::Get().SaveTokenBlob(tokenFile, freshAuth->SerializeTokens());
{
std::lock_guard<std::mutex> lock(s.mutex);
s.session = session;
s.lastError.clear();
s.state = State::Success;
s.cv.notify_all();
}
SaveJavaAccountIndex();
} else {
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = error;
s.state = State::Failed;
s.cv.notify_all();
}
std::string reloadFile;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size())
reloadFile = m_javaAccounts[s.accountIndex].tokenFile;
}
if (!reloadFile.empty()) {
std::string blob = AuthDb::Get().LoadTokenBlob(reloadFile);
if (!blob.empty()) freshAuth->DeserializeTokens(blob);
}
}
if (onComplete) onComplete(ok, session, error);
} catch (const std::exception& ex) {
AUTH_LOG("BeginAddJavaAccount: uncaught exception: %s", ex.what());
auto& s = m_slots[0];
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = std::string("Auth network error: ") + ex.what();
s.state = State::Failed;
s.cv.notify_all();
}
if (onComplete) onComplete(false, {}, s.lastError);
} catch (...) {
AUTH_LOG("BeginAddJavaAccount: unknown uncaught exception");
auto& s = m_slots[0];
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = "Unknown auth error";
s.state = State::Failed;
s.cv.notify_all();
}
if (onComplete) onComplete(false, {}, "Unknown auth error");
}
}).detach();
}
void newauthManager::BeginAddElybyAccount(const std::string& username, const std::string& password,
ElybyCompleteCb onComplete, Elyby2FACb on2FA)
{
auto& s = m_slots[0];
uint32_t gen = ++s.generation;
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Authenticating;
s.cv.notify_all();
}
std::thread([this, username, password, onComplete, on2FA, gen]() {
try {
auto& s = m_slots[0];
newauth::ElybyTokens tokens;
std::string error;
bool ok = newauth::ElybyLogin(username, password, tokens, error);
if (!ok && error == "elyby_2fa_required") {
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::WaitingForCode;
s.cv.notify_all();
}
if (on2FA) on2FA();
if (onComplete) onComplete(false, {}, "elyby_2fa_required");
return;
}
{
std::lock_guard<std::mutex> lock(s.mutex);
if (s.generation != gen) {
if (onComplete) onComplete(false, {}, "cancelled");
return;
}
}
if (ok) {
std::string dashedUuid = newauth::DashUuid(tokens.uuid);
std::string tokenFile;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
int existingIdx = -1;
for (int i = 0; i < (int)m_javaAccounts.size(); ++i) {
if (m_javaAccounts[i].uuid == dashedUuid) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_javaAccounts[existingIdx].username = tokens.username;
m_javaAccounts[existingIdx].authProvider = "elyby";
tokenFile = m_javaAccounts[existingIdx].tokenFile;
s.accountIndex = existingIdx;
} else {
JavaAccountInfo info;
info.username = tokens.username;
info.uuid = dashedUuid;
info.tokenFile = AllocTokenFile(dashedUuid);
info.isOffline = false;
info.authProvider = "elyby";
tokenFile = info.tokenFile;
m_javaAccounts.push_back(std::move(info));
s.accountIndex = (int)m_javaAccounts.size() - 1;
}
}
if (!tokenFile.empty())
AuthDb::Get().SaveTokenBlob(tokenFile, newauth::ElybySerializeTokens(tokens));
newauth::JavaSession session;
session.username = tokens.username;
session.uuid = dashedUuid;
session.accessToken = tokens.accessToken;
session.expireMs = 0;
{
std::lock_guard<std::mutex> lock(s.mutex);
s.elybyTokens = tokens;
s.session = session;
s.lastError.clear();
s.state = State::Success;
s.cv.notify_all();
}
SaveJavaAccountIndex();
if (onComplete) onComplete(true, session, "");
} else {
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = error;
s.state = State::Failed;
s.cv.notify_all();
}
if (onComplete) onComplete(false, {}, error);
}
} catch (const std::exception& ex) {
auto& s = m_slots[0];
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = std::string("Elyby auth error: ") + ex.what();
s.state = State::Failed;
s.cv.notify_all();
}
if (onComplete) onComplete(false, {}, s.lastError);
} catch (...) {
auto& s = m_slots[0];
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = "Unknown elyby auth error";
s.state = State::Failed;
s.cv.notify_all();
}
if (onComplete) onComplete(false, {}, "Unknown elyby auth error");
}
}).detach();
}
bool newauthManager::RemoveJavaAccount(int index) {
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (index < 0 || index >= (int)m_javaAccounts.size()) return false;
AUTH_LOG("RemoveJavaAccount: removing index %d ('%s')",
index, m_javaAccounts[index].username.c_str());
std::remove(m_javaAccounts[index].tokenFile.c_str());
m_javaAccounts.erase(m_javaAccounts.begin() + index);
for (int i = 0; i < XUSER_MAX_COUNT; ++i) {
auto& s = m_slots[i];
if (s.accountIndex == index) {
if (m_javaAccounts.empty()) {
s.accountIndex = -1;
std::lock_guard<std::mutex> lock(s.mutex);
s.session = {};
s.state = State::Idle;
s.cv.notify_all();
} else {
s.accountIndex = 0;
}
} else if (s.accountIndex > index) {
--s.accountIndex;
}
}
return true;
}
void newauthManager::TryRestoreActiveJavaAccount() {
AUTH_LOG("TryRestoreActiveJavaAccount called (already attempted=%d)",
(int)m_javaRestoreAttempted);
if (m_javaRestoreAttempted) return;
m_javaRestoreAttempted = true;
if (!LoadJavaAccountIndex()) {
auto& s = m_slots[0];
std::string legacyBlob;
{
std::ifstream lf("java_auth.json");
if (lf) legacyBlob.assign((std::istreambuf_iterator<char>(lf)),
std::istreambuf_iterator<char>());
}
if (!legacyBlob.empty() && s.auth->DeserializeTokens(legacyBlob)) {
AUTH_LOG("TryRestoreActiveJavaAccount: migrating legacy java_auth.json");
std::lock_guard<std::mutex> alock(m_accountsMutex);
JavaAccountInfo info;
info.username = "";
info.uuid = "";
info.tokenFile = "legacy_migrated";
AuthDb::Get().SaveTokenBlob(info.tokenFile, s.auth->SerializeTokens());
m_javaAccounts.push_back(std::move(info));
s.accountIndex = 0;
SaveJavaAccountIndex();
std::remove("java_auth.json");
} else {
AUTH_LOG("TryRestoreActiveJavaAccount: no accounts found");
return;
}
}
auto& s = m_slots[0];
int activeIdx = s.accountIndex;
std::string tokenFile;
bool isOffline = false;
bool isElyby = false;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (activeIdx < 0 || activeIdx >= (int)m_javaAccounts.size()) {
AUTH_LOG("TryRestoreActiveJavaAccount: no active account");
return;
}
isOffline = m_javaAccounts[activeIdx].isOffline;
isElyby = (m_javaAccounts[activeIdx].authProvider == "elyby");
tokenFile = m_javaAccounts[activeIdx].tokenFile;
}
if (isElyby) {
AUTH_LOG("TryRestoreActiveJavaAccount: elyby account %d, loading tokens from '%s'",
activeIdx, tokenFile.c_str());
newauth::ElybyTokens elyTokens;
{
std::string blob = AuthDb::Get().LoadTokenBlob(tokenFile);
if (blob.empty() || !newauth::ElybyDeserializeTokens(blob, elyTokens)) {
AUTH_LOG("TryRestoreActiveJavaAccount: ElybyLoadTokens from DB failed for '%s'", tokenFile.c_str());
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Failed;
s.lastError = "Failed to load ely.by tokens: " + tokenFile;
s.cv.notify_all();
return;
}
}
{
std::lock_guard<std::mutex> lock(s.mutex);
s.elybyTokens = elyTokens;
s.session.username = elyTokens.username;
s.session.uuid = newauth::DashUuid(elyTokens.uuid);
s.session.accessToken = elyTokens.accessToken;
s.session.expireMs = 0;
s.state = State::Success;
s.cv.notify_all();
}
RefreshSlot(0);
return;
}
if (isOffline) {
AUTH_LOG("TryRestoreActiveJavaAccount: offline account %d, no refresh needed", activeIdx);
newauth::JavaSession offlineSession;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
offlineSession = SynthesizeOfflineSession(m_javaAccounts[activeIdx]);
}
{
std::lock_guard<std::mutex> lock(s.mutex);
s.session = offlineSession;
s.state = State::Success;
s.cv.notify_all();
}
return;
}
AUTH_LOG("TryRestoreActiveJavaAccount: loading tokenFile='%s' for account %d",
tokenFile.c_str(), activeIdx);
{
std::string blob = AuthDb::Get().LoadTokenBlob(tokenFile);
if (blob.empty() || !s.auth->DeserializeTokens(blob)) {
AUTH_LOG("TryRestoreActiveJavaAccount: LoadTokens from DB failed for '%s'", tokenFile.c_str());
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Failed;
s.lastError = "Failed to load token: " + tokenFile;
s.cv.notify_all();
return;
}
}
RefreshSlot(0);
}
std::string newauthManager::GetJavaDeviceCode() const {
std::lock_guard<std::mutex> lock(m_deviceCodeMutex);
return m_javaDeviceCode;
}
std::string newauthManager::GetJavaDirectUri() const {
std::lock_guard<std::mutex> lock(m_deviceCodeMutex);
return m_javaDirectUri;
}
std::string newauthManager::GetLastError() const {
auto& s = m_slots[0];
std::lock_guard<std::mutex> lock(s.mutex);
return s.lastError;
}

View file

@ -0,0 +1,410 @@
#include "../include/newauth.h"
#include "newauthCrypto.h"
#include "newauthHttp.h"
#include "newauthInternal.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <bcrypt.h>
#include <wincodec.h>
#include <sstream>
#include <cstdint>
#include <cstring>
#include <vector>
#pragma comment(lib, "bcrypt.lib")
#pragma comment(lib, "windowscodecs.lib")
#pragma comment(lib, "ole32.lib")
namespace newauth {
using namespace newauth;
std::string MakeSkinKey(const std::string& uuid) {
return "mojang_skin_" + UndashUuid(uuid) + ".png";
}
std::string UndashUuid(const std::string& dashed) {
std::string out;
out.reserve(32);
for (char c : dashed) {
if (c != '-') out += c;
}
return out;
}
std::string DashUuid(const std::string& u) {
if (u.size() < 32) return u;
return u.substr(0, 8) + "-" + u.substr(8, 4) + "-" + u.substr(12, 4)
+ "-" + u.substr(16, 4) + "-" + u.substr(20);
}
std::string GenerateOfflineUuid(const std::string& username) {
std::string input = "OfflinePlayer:" + username;
auto md5 = ComputeMD5(input.data(), input.size());
if (md5.size() < 16) return "";
uint8_t hash[16];
memcpy(hash, md5.data(), 16);
hash[6] = (hash[6] & 0x0F) | 0x30;
hash[8] = (hash[8] & 0x3F) | 0x80;
char hex[33];
for (int i = 0; i < 16; i++)
snprintf(hex + i * 2, 3, "%02x", hash[i]);
hex[32] = '\0';
return DashUuid(std::string(hex, 32));
}
static uint8_t HexNibble(char c) {
if (c >= '0' && c <= '9') return (uint8_t)(c - '0');
if (c >= 'a' && c <= 'f') return (uint8_t)(c - 'a' + 10);
if (c >= 'A' && c <= 'F') return (uint8_t)(c - 'A' + 10);
return 0;
}
Uuid128 ParseUuid128(const std::string& dashed) {
std::string hex = UndashUuid(dashed);
if (hex.size() != 32) return {};
Uuid128 result;
result.hi = 0;
result.lo = 0;
for (int i = 0; i < 16; i++) {
uint8_t byte = (HexNibble(hex[i * 2]) << 4) | HexNibble(hex[i * 2 + 1]);
if (i < 8)
result.hi = (result.hi << 8) | byte;
else
result.lo = (result.lo << 8) | byte;
}
return result;
}
static AuthErrorCode ClassifyHttpStatus(int status) {
if (status == 401 || status == 403) return AuthErrorCode::InvalidCredentials;
if (status == 429) return AuthErrorCode::RateLimited;
if (status >= 500 && status < 600) return AuthErrorCode::ServerUnavailable;
return AuthErrorCode::HttpError;
}
static bool JoinServerImpl(const std::string& url,
const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
const std::string& errorPrefix,
std::string& error)
{
std::string body = "{\"accessToken\":\"" + JsonEscape(accessToken)
+ "\",\"selectedProfile\":\"" + JsonEscape(undashedUuid)
+ "\",\"serverId\":\"" + JsonEscape(serverId) + "\"}";
try {
auto resp = HttpPost(url, body, "application/json");
if (resp.statusCode == 204 || resp.statusCode == 200)
return true;
error = errorPrefix + " HTTP " + std::to_string(resp.statusCode);
if (!resp.body.empty()) {
std::string msg = JsonGetString(resp.body, "errorMessage");
if (!msg.empty()) error += ": " + msg;
else error += " body: " + resp.body.substr(0, 200);
}
} catch (const std::exception& e) {
error = errorPrefix + " network error: " + e.what();
}
return false;
}
bool JoinServer(const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
std::string& error)
{
return JoinServerImpl(
"https://sessionserver.mojang.com/session/minecraft/join",
accessToken, undashedUuid, serverId, "JoinServer", error);
}
static void ParseTextureProperties(const std::string& body, std::string& skinUrl, std::string& capeUrl) {
size_t propsPos = body.find("\"properties\"");
if (propsPos == std::string::npos) return;
size_t texNamePos = body.find("\"textures\"", propsPos);
if (texNamePos == std::string::npos) return;
std::string b64Value = JsonGetString(body.substr(texNamePos), "value");
if (b64Value.empty()) return;
std::string decoded = Base64DecodeStr(b64Value);
if (decoded.empty()) return;
std::string skinObj = JsonRawValue(decoded, "SKIN");
if (!skinObj.empty())
skinUrl = JsonGetString(skinObj, "url");
std::string capeObj = JsonRawValue(decoded, "CAPE");
if (!capeObj.empty())
capeUrl = JsonGetString(capeObj, "url");
}
static HasJoinedResult HasJoinedImpl(const std::string& baseUrl,
const std::string& username,
const std::string& serverId,
const std::string& errorPrefix,
std::string& error)
{
std::string url = baseUrl + "?username="
+ UrlEncode(username) + "&serverId=" + UrlEncode(serverId);
HasJoinedResult result;
try {
auto resp = HttpGet(url);
if (resp.statusCode == 200 && !resp.body.empty()) {
result.success = true;
result.uuid = JsonGetString(resp.body, "id");
result.username = JsonGetString(resp.body, "name");
ParseTextureProperties(resp.body, result.skinUrl, result.capeUrl);
return result;
}
result.success = false;
if (resp.statusCode == 204) {
error = "Player has not joined (session not found)";
result.error = { AuthErrorCode::InvalidCredentials, error, 204 };
} else {
error = errorPrefix + " HTTP " + std::to_string(resp.statusCode);
result.error = { ClassifyHttpStatus(resp.statusCode), error, resp.statusCode };
}
} catch (const std::exception& e) {
error = errorPrefix + " network error: " + e.what();
result.success = false;
result.error = { AuthErrorCode::NetworkError, error, 0 };
}
return result;
}
HasJoinedResult HasJoined(const std::string& username,
const std::string& serverId,
std::string& error)
{
return HasJoinedImpl(
"https://sessionserver.mojang.com/session/minecraft/hasJoined",
username, serverId, "HasJoined", error);
}
static std::vector<uint8_t> CropSkinTo64x32(const std::vector<uint8_t>& pngData) {
try {
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
bool needUninit = SUCCEEDED(hr);
IWICImagingFactory* factory = nullptr;
IWICStream* stream = nullptr;
IWICBitmapDecoder* decoder = nullptr;
IWICBitmapFrameDecode*frame = nullptr;
IWICFormatConverter* converter = nullptr;
IWICStream* outStream = nullptr;
IStream* memStream = nullptr;
IWICBitmapEncoder* encoder = nullptr;
IWICBitmapFrameEncode*outFrame = nullptr;
auto cleanup = [&]() {
if (outFrame) outFrame->Release();
if (encoder) encoder->Release();
if (outStream) outStream->Release();
if (memStream) memStream->Release();
if (converter) converter->Release();
if (frame) frame->Release();
if (decoder) decoder->Release();
if (stream) stream->Release();
if (factory) factory->Release();
if (needUninit) CoUninitialize();
};
hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&factory));
if (FAILED(hr)) { cleanup(); return pngData; }
#define WIC_CHECK(expr) do { hr = (expr); if (FAILED(hr)) { cleanup(); return pngData; } } while(0)
WIC_CHECK(factory->CreateStream(&stream));
WIC_CHECK(stream->InitializeFromMemory((BYTE*)pngData.data(), (DWORD)pngData.size()));
WIC_CHECK(factory->CreateDecoderFromStream(stream, nullptr, WICDecodeMetadataCacheOnDemand, &decoder));
WIC_CHECK(decoder->GetFrame(0, &frame));
UINT width = 0, height = 0;
WIC_CHECK(frame->GetSize(&width, &height));
if (width != 64 || height != 64) { cleanup(); return pngData; }
WIC_CHECK(factory->CreateFormatConverter(&converter));
WIC_CHECK(converter->Initialize(frame, GUID_WICPixelFormat32bppBGRA,
WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom));
const UINT cropW = 64, cropH = 32;
const UINT stride = cropW * 4;
std::vector<BYTE> pixels(stride * cropH);
WICRect cropRect = { 0, 0, (INT)cropW, (INT)cropH };
WIC_CHECK(converter->CopyPixels(&cropRect, stride, (UINT)pixels.size(), pixels.data()));
WIC_CHECK(factory->CreateStream(&outStream));
hr = CreateStreamOnHGlobal(nullptr, TRUE, &memStream);
if (FAILED(hr)) { cleanup(); return pngData; }
WIC_CHECK(outStream->InitializeFromIStream(memStream));
WIC_CHECK(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder));
WIC_CHECK(encoder->Initialize(outStream, WICBitmapEncoderNoCache));
WIC_CHECK(encoder->CreateNewFrame(&outFrame, nullptr));
WIC_CHECK(outFrame->Initialize(nullptr));
WIC_CHECK(outFrame->SetSize(cropW, cropH));
WICPixelFormatGUID pixFmt = GUID_WICPixelFormat32bppBGRA;
WIC_CHECK(outFrame->SetPixelFormat(&pixFmt));
WIC_CHECK(outFrame->WritePixels(cropH, stride, (UINT)pixels.size(), pixels.data()));
WIC_CHECK(outFrame->Commit());
WIC_CHECK(encoder->Commit());
STATSTG stat = {};
WIC_CHECK(memStream->Stat(&stat, STATFLAG_NONAME));
ULONG pngSize = (ULONG)stat.cbSize.QuadPart;
#undef WIC_CHECK
std::vector<uint8_t> result(pngSize);
LARGE_INTEGER zero = {};
memStream->Seek(zero, STREAM_SEEK_SET, nullptr);
memStream->Read(result.data(), pngSize, nullptr);
cleanup();
return result;
} catch (...) {
return pngData;
}
}
bool ValidateSkinPng(const uint8_t* data, size_t size) {
static const uint8_t kPngMagic[8] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
if (data == nullptr || size < 24)
return false;
if (size > kMaxSkinBytes)
return false;
if (memcmp(data, kPngMagic, 8) != 0)
return false;
uint32_t width = ((uint32_t)data[16] << 24) | ((uint32_t)data[17] << 16) |
((uint32_t)data[18] << 8) | (uint32_t)data[19];
uint32_t height = ((uint32_t)data[20] << 24) | ((uint32_t)data[21] << 16) |
((uint32_t)data[22] << 8) | (uint32_t)data[23];
if (width != 64)
return false;
if (height != 32 && height != 64)
return false;
return true;
}
static std::vector<uint8_t> FetchSkinPngImpl(const std::string& url, bool cropToLCE, std::string& error) {
if (url.empty()) {
error = "Empty skin URL";
return {};
}
try {
auto resp = HttpGet(url);
if (resp.statusCode == 200 && !resp.body.empty()) {
auto rawPng = std::vector<uint8_t>(resp.body.begin(), resp.body.end());
if (!ValidateSkinPng(rawPng.data(), rawPng.size())) {
error = "FetchSkinPng: invalid PNG data (" + std::to_string(rawPng.size()) + " bytes)";
return {};
}
return cropToLCE ? CropSkinTo64x32(rawPng) : rawPng;
}
error = "FetchSkinPng HTTP " + std::to_string(resp.statusCode);
} catch (const std::exception& e) {
error = std::string("FetchSkinPng exception: ") + e.what();
}
return {};
}
std::vector<uint8_t> FetchSkinPng(const std::string& url, std::string& error) {
return FetchSkinPngImpl(url, /*cropToLCE=*/true, error);
}
std::vector<uint8_t> FetchSkinPngRaw(const std::string& url, std::string& error) {
return FetchSkinPngImpl(url, /*cropToLCE=*/false, error);
}
static std::string FetchProfileSkinUrlImpl(const std::string& baseUrl,
const std::string& uuid,
const std::string& errorPrefix,
const std::string& noSkinMsg,
std::string& error)
{
std::string undashed = UndashUuid(uuid);
if (undashed.empty()) {
error = "Empty UUID";
return "";
}
std::string url = baseUrl + undashed;
try {
auto resp = HttpGet(url);
if (resp.statusCode != 200 || resp.body.empty()) {
error = errorPrefix + " HTTP " + std::to_string(resp.statusCode);
return "";
}
std::string skinUrl, capeUrl;
ParseTextureProperties(resp.body, skinUrl, capeUrl);
if (skinUrl.empty())
error = noSkinMsg;
return skinUrl;
} catch (const std::exception& e) {
error = errorPrefix + " exception: " + e.what();
return "";
}
}
std::string FetchProfileSkinUrl(const std::string& uuid, std::string& error) {
return FetchProfileSkinUrlImpl(
"https://sessionserver.mojang.com/session/minecraft/profile/",
uuid, "Profile", "No skin in profile", error);
}
std::string MakeElybySkinKey(const std::string& uuid) {
return "elyby_skin_" + UndashUuid(uuid) + ".png";
}
bool ElybyJoinServer(const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
std::string& error)
{
return JoinServerImpl(
"https://authserver.ely.by/session/join",
accessToken, undashedUuid, serverId, "ElybyJoinServer", error);
}
HasJoinedResult ElybyHasJoined(const std::string& username,
const std::string& serverId,
std::string& error)
{
return HasJoinedImpl(
"https://authserver.ely.by/session/hasJoined",
username, serverId, "ElybyHasJoined", error);
}
std::string ElybyFetchProfileSkinUrl(const std::string& uuid, std::string& error) {
return FetchProfileSkinUrlImpl(
"https://authserver.ely.by/session/profile/",
uuid, "ElybyProfile", "No skin in ely.by profile", error);
}
}