Cemu/src/Cemu/napi/napi_act.cpp

614 lines
20 KiB
C++

#include "Common/precompiled.h"
#include "Cemu/ncrypto/ncrypto.h"
#include "napi.h"
#include "napi_helper.h"
#include "curl/curl.h"
#include "pugixml.hpp"
#include "Cafe/IOSU/legacy/iosu_crypto.h"
#include "config/ActiveSettings.h"
#include "util/helpers/StringHelpers.h"
#include "util/highresolutiontimer/HighResolutionTimer.h"
#include "config/LaunchSettings.h"
namespace NAPI
{
struct ACTOauthToken : public _NAPI_CommonResultACT
{
std::string token;
std::string refreshToken;
};
bool _parseActResponse(CurlRequestHelper& requestHelper, _NAPI_CommonResultACT& result, pugi::xml_document& doc)
{
if (!doc.load_buffer(requestHelper.getReceivedData().data(), requestHelper.getReceivedData().size()))
{
cemuLog_log(LogType::Force, fmt::format("Invalid XML in account service response"));
result.apiError = NAPI_RESULT::XML_ERROR;
return false;
}
// check for error codes
pugi::xml_node errors = doc.child("errors");
if (errors)
{
pugi::xml_node error = errors.child("error");
if (error)
{
std::string_view errorCodeStr = error.child_value("code");
std::string_view errorCodeMsg = error.child_value("message");
sint32 errorCode = StringHelpers::ToInt(errorCodeStr);
if (errorCode == 0)
{
cemuLog_force("Account response with unexpected error code 0");
result.apiError = NAPI_RESULT::XML_ERROR;
}
else
{
result.apiError = NAPI_RESULT::SERVICE_ERROR;
result.serviceError = (ACT_ERROR_CODE)errorCode;
cemuLog_force("Account response with error code {}", errorCode);
if(!errorCodeMsg.empty())
cemuLog_force("Message from server: {}", errorCodeMsg);
}
}
else
{
result.apiError = NAPI_RESULT::XML_ERROR;
}
return false;
}
return true;
}
void _ACTSetCommonHeaderParameters(CurlRequestHelper& req, AuthInfo& authInfo)
{
req.addHeaderField("X-Nintendo-Platform-ID", "1");
req.addHeaderField("X-Nintendo-Device-Type", "2");
req.addHeaderField("X-Nintendo-Client-ID", "a2efa818a34fa16b8afbc8a74eba3eda");
req.addHeaderField("X-Nintendo-Client-Secret", "c91cdb5658bd4954ade78533a339cf9a");
req.addHeaderField("Accept", "*/*");
if(authInfo.region == CafeConsoleRegion::USA)
req.addHeaderField("X-Nintendo-System-Version", "0270");
else
req.addHeaderField("X-Nintendo-System-Version", "0260");
}
void _ACTSetDeviceParameters(CurlRequestHelper& req, AuthInfo& authInfo)
{
req.addHeaderField("X-Nintendo-Device-ID", fmt::format("{}", authInfo.deviceId)); // deviceId without platform field
req.addHeaderField("X-Nintendo-Serial-Number", authInfo.serial);
}
void _ACTSetRegionAndCountryParameters(CurlRequestHelper& req, AuthInfo& authInfo)
{
req.addHeaderField("X-Nintendo-Region", fmt::format("{}", (uint32)authInfo.region));
req.addHeaderField("X-Nintendo-Country", authInfo.country);
}
struct OAuthTokenCacheEntry
{
OAuthTokenCacheEntry(std::string_view accountId, std::array<uint8, 32>& passwordHash, std::string_view token, std::string_view refreshToken, uint64 expiresIn) : accountId(accountId), passwordHash(passwordHash), token(token), refreshToken(refreshToken)
{
expires = HighResolutionTimer::now().getTickInSeconds() + expiresIn;
};
bool CheckIfSameAccount(const AuthInfo& authInfo) const
{
return authInfo.accountId == accountId && authInfo.passwordHash == passwordHash;
}
bool CheckIfExpired() const
{
return HighResolutionTimer::now().getTickInSeconds() >= expires;
}
std::string accountId;
std::array<uint8, 32> passwordHash;
std::string token;
std::string refreshToken;
uint64 expires;
};
std::vector<OAuthTokenCacheEntry> g_oauthTokenCache;
std::mutex g_oauthTokenCacheMtx;
// look up oauth token in cache, otherwise request from server
ACTOauthToken ACT_GetOauthToken_WithCache(AuthInfo& authInfo, uint64 titleId, uint16 titleVersion)
{
ACTOauthToken result{};
// check cache first
g_oauthTokenCacheMtx.lock();
auto cacheItr = g_oauthTokenCache.begin();
while (cacheItr != g_oauthTokenCache.end())
{
if (cacheItr->CheckIfSameAccount(authInfo))
{
if (cacheItr->CheckIfExpired())
{
cacheItr = g_oauthTokenCache.erase(cacheItr);
continue;
}
result.token = cacheItr->token;
result.refreshToken = cacheItr->refreshToken;
result.apiError = NAPI_RESULT::SUCCESS;
g_oauthTokenCacheMtx.unlock();
return result;
}
cacheItr++;
}
g_oauthTokenCacheMtx.unlock();
// token not cached, request from server via oauth2
CurlRequestHelper req;
req.initate(fmt::format("{}/v1/api/oauth20/access_token/generate", LaunchSettings::GetActURLPrefix()), CurlRequestHelper::SERVER_SSL_CONTEXT::ACT);
_ACTSetCommonHeaderParameters(req, authInfo);
_ACTSetDeviceParameters(req, authInfo);
_ACTSetRegionAndCountryParameters(req, authInfo);
req.addHeaderField("X-Nintendo-Device-Cert", authInfo.deviceCertBase64);
req.addHeaderField("X-Nintendo-FPD-Version", "0000");
req.addHeaderField("X-Nintendo-Environment", "L1");
req.addHeaderField("X-Nintendo-Title-ID", fmt::format("{:016x}", titleId));
uint32 uniqueId = ((titleId >> 8) & 0xFFFFF);
req.addHeaderField("X-Nintendo-Unique-ID", fmt::format("{:05x}", uniqueId));
req.addHeaderField("X-Nintendo-Application-Version", fmt::format("{:04x}", titleVersion));
// convert password hash to string
char passwordHashString[128];
for (sint32 i = 0; i < 32; i++)
sprintf(passwordHashString + i * 2, "%02x", authInfo.passwordHash[i]);
req.addPostField("grant_type", "password");
req.addPostField("user_id", authInfo.accountId);
req.addPostField("password", passwordHashString);
req.addPostField("password_type", "hash");
req.addHeaderField("Content-type", "application/x-www-form-urlencoded");
if (!req.submitRequest(true))
{
cemuLog_log(LogType::Force, fmt::format("Failed request /oauth20/access_token/generate"));
result.apiError = NAPI_RESULT::FAILED;
return result;
}
/*
Response example:
<OAuth20>
<access_token>
<token>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</token>
<refresh_token>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</refresh_token>
<expires_in>3600</expires_in>
</access_token>
</OAuth20>
*/
// parse result
pugi::xml_document doc;
if (!_parseActResponse(req, result, doc))
return result;
pugi::xml_node node = doc.child("OAuth20");
if (!node)
{
cemuLog_log(LogType::Force, fmt::format("Response does not contain OAuth20 node"));
result.apiError = NAPI_RESULT::XML_ERROR;
return result;
}
node = node.child("access_token");
if (!node)
{
cemuLog_log(LogType::Force, fmt::format("Response does not contain OAuth20/access_token node"));
result.apiError = NAPI_RESULT::XML_ERROR;
return result;
}
result.token = node.child_value("token");
result.refreshToken = node.child_value("refresh_token");
std::string_view expires_in = node.child_value("expires_in");
result.apiError = NAPI_RESULT::SUCCESS;
if (result.token.empty())
cemuLog_force("OAuth20/token is empty");
sint64 expiration = StringHelpers::ToInt64(expires_in);
expiration = std::max(expiration - 30LL, 0LL); // subtract a few seconds to compensate for the web request delay
// update cache
if (expiration > 0)
{
g_oauthTokenCacheMtx.lock();
g_oauthTokenCache.emplace_back(authInfo.accountId, authInfo.passwordHash, result.token, result.refreshToken, expiration);
g_oauthTokenCacheMtx.unlock();
}
return result;
}
bool ACT_GetProfile(AuthInfo& authInfo, ACTGetProfileResult& result)
{
CurlRequestHelper req;
req.initate(fmt::format("{}/v1/api/people/@me/profile", LaunchSettings::GetActURLPrefix()), CurlRequestHelper::SERVER_SSL_CONTEXT::ACT);
_ACTSetCommonHeaderParameters(req, authInfo);
_ACTSetDeviceParameters(req, authInfo);
// get oauth2 token
ACTOauthToken oauthToken = ACT_GetOauthToken_WithCache(authInfo, 0x0005001010001C00, 0x0001C);
cemu_assert_unimplemented();
return true;
}
struct NexTokenCacheEntry
{
NexTokenCacheEntry(std::string_view accountId, std::array<uint8, 32>& passwordHash, uint32 gameServerId, ACTNexToken& nexToken) : accountId(accountId), passwordHash(passwordHash), nexToken(nexToken), gameServerId(gameServerId) {};
bool IsMatch(const AuthInfo& authInfo, const uint32 gameServerId) const
{
return authInfo.accountId == accountId && authInfo.passwordHash == passwordHash && this->gameServerId == gameServerId;
}
std::string accountId;
std::array<uint8, 32> passwordHash;
uint32 gameServerId;
ACTNexToken nexToken;
};
std::vector<NexTokenCacheEntry> g_nexTokenCache;
std::mutex g_nexTokenCacheMtx;
ACTGetNexTokenResult ACT_GetNexToken_WithCache(AuthInfo& authInfo, uint64 titleId, uint16 titleVersion, uint32 serverId)
{
ACTGetNexTokenResult result{};
// check cache
g_nexTokenCacheMtx.lock();
for (auto& itr : g_nexTokenCache)
{
if (itr.IsMatch(authInfo, serverId))
{
result.nexToken = itr.nexToken;
result.apiError = NAPI_RESULT::SUCCESS;
g_nexTokenCacheMtx.unlock();
return result;
}
}
g_nexTokenCacheMtx.unlock();
// get Nex token
ACTOauthToken oauthToken = ACT_GetOauthToken_WithCache(authInfo, titleId, titleVersion);
if (!oauthToken.isValid())
{
cemuLog_force("ACT_GetNexToken(): Failed to retrieve OAuth token");
if (oauthToken.apiError == NAPI_RESULT::SERVICE_ERROR)
{
result.apiError = NAPI_RESULT::SERVICE_ERROR;
result.serviceError = oauthToken.serviceError;
}
else
{
result.apiError = NAPI_RESULT::DATA_ERROR;
}
return result;
}
// do request
CurlRequestHelper req;
req.initate(fmt::format("{}/v1/api/provider/nex_token/@me?game_server_id={:08X}", LaunchSettings::GetActURLPrefix(), serverId), CurlRequestHelper::SERVER_SSL_CONTEXT::ACT);
_ACTSetCommonHeaderParameters(req, authInfo);
_ACTSetDeviceParameters(req, authInfo);
_ACTSetRegionAndCountryParameters(req, authInfo);
req.addHeaderField("X-Nintendo-FPD-Version", "0000");
req.addHeaderField("X-Nintendo-Environment", "L1");
req.addHeaderField("X-Nintendo-Title-ID", fmt::format("{:016x}", titleId));
uint32 uniqueId = ((titleId >> 8) & 0xFFFFF);
req.addHeaderField("X-Nintendo-Unique-ID", fmt::format("{:05x}", uniqueId));
req.addHeaderField("X-Nintendo-Application-Version", fmt::format("{:04x}", titleVersion));
req.addHeaderField("Authorization", fmt::format("Bearer {}", oauthToken.token));
if (!req.submitRequest(false))
{
cemuLog_log(LogType::Force, fmt::format("Failed request /provider/nex_token/@me"));
result.apiError = NAPI_RESULT::FAILED;
return result;
}
/*
Example response (success):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<nex_token>
<host>HOST</host>
<nex_password>xxxxxxxxxxxxxxxx</nex_password>
<pid>123456</pid>
<port>60200</port>
<token>xxxxxxxxxxxxxxx</token>
</nex_token>
Example response (error case):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<errors>
<error>
<code>1021</code>
<message>The requested game server was not found.</message>
</error>
</errors>
*/
// code 0124 -> Version lower than useable registered
// parse result
pugi::xml_document doc;
if (!_parseActResponse(req, result, doc))
return result;
pugi::xml_node tokenNode = doc.child("nex_token");
if (!tokenNode)
{
cemuLog_force("Response does not contain NexToken node");
result.apiError = NAPI_RESULT::XML_ERROR;
return result;
}
std::string_view host = tokenNode.child_value("host");
std::string_view nex_password = tokenNode.child_value("nex_password");
std::string_view port = tokenNode.child_value("port");
std::string_view token = tokenNode.child_value("token");
std::memset(&result.nexToken, 0, sizeof(result.nexToken));
if (host.size() > 15)
cemuLog_force("NexToken response: host field too long");
if (nex_password.size() > 64)
cemuLog_force("NexToken response: nex_password field too long");
if (token.size() > 512)
cemuLog_force("NexToken response: token field too long");
for (size_t i = 0; i < std::min(host.size(), (size_t)15); i++)
result.nexToken.host[i] = host[i];
for (size_t i = 0; i < std::min(nex_password.size(), (size_t)64); i++)
result.nexToken.nexPassword[i] = nex_password[i];
for (size_t i = 0; i < std::min(token.size(), (size_t)512); i++)
result.nexToken.token[i] = token[i];
result.nexToken.port = (uint16)StringHelpers::ToInt(port);
result.apiError = NAPI_RESULT::SUCCESS;
g_nexTokenCacheMtx.lock();
g_nexTokenCache.emplace_back(authInfo.accountId, authInfo.passwordHash, serverId, result.nexToken);
g_nexTokenCacheMtx.unlock();
return result;
}
struct IndependentTokenCacheEntry
{
IndependentTokenCacheEntry(std::string_view accountId, std::array<uint8, 32>& passwordHash, std::string_view clientId, std::string_view independentToken, sint64 expiresIn) : accountId(accountId), passwordHash(passwordHash), clientId(clientId), independentToken(independentToken)
{
expires = HighResolutionTimer::now().getTickInSeconds() + expiresIn;
};
bool IsMatch(const AuthInfo& authInfo, const std::string_view clientId) const
{
return authInfo.accountId == accountId && authInfo.passwordHash == passwordHash && this->clientId == clientId;
}
bool CheckIfExpired() const
{
return (sint64)HighResolutionTimer::now().getTickInSeconds() >= expires;
}
std::string accountId;
std::array<uint8, 32> passwordHash;
std::string clientId;
sint64 expires;
std::string independentToken;
};
std::vector<IndependentTokenCacheEntry> g_IndependentTokenCache;
std::mutex g_IndependentTokenCacheMtx;
ACTGetIndependentTokenResult ACT_GetIndependentToken_WithCache(AuthInfo& authInfo, uint64 titleId, uint16 titleVersion, std::string_view clientId)
{
ACTGetIndependentTokenResult result{};
// check cache
g_IndependentTokenCacheMtx.lock();
auto itr = g_IndependentTokenCache.begin();
while(itr != g_IndependentTokenCache.end())
{
if (itr->CheckIfExpired())
{
itr = g_IndependentTokenCache.erase(itr);
continue;
}
else if (itr->IsMatch(authInfo, clientId))
{
result.token = itr->independentToken;
result.expiresIn = std::max(itr->expires - (sint64)HighResolutionTimer::now().getTickInSeconds(), (sint64)0);
result.apiError = NAPI_RESULT::SUCCESS;
g_IndependentTokenCacheMtx.unlock();
return result;
}
itr++;
}
g_IndependentTokenCacheMtx.unlock();
// get Independent token
ACTOauthToken oauthToken = ACT_GetOauthToken_WithCache(authInfo, titleId, titleVersion);
if (!oauthToken.isValid())
{
cemuLog_force("ACT_GetIndependentToken(): Failed to retrieve OAuth token");
if (oauthToken.apiError == NAPI_RESULT::SERVICE_ERROR)
{
result.apiError = NAPI_RESULT::SERVICE_ERROR;
result.serviceError = oauthToken.serviceError;
}
else
{
result.apiError = NAPI_RESULT::DATA_ERROR;
}
return result;
}
// do request
CurlRequestHelper req;
req.initate(fmt::format("{}/v1/api/provider/service_token/@me?client_id={}", LaunchSettings::GetActURLPrefix(), clientId), CurlRequestHelper::SERVER_SSL_CONTEXT::ACT);
_ACTSetCommonHeaderParameters(req, authInfo);
_ACTSetDeviceParameters(req, authInfo);
_ACTSetRegionAndCountryParameters(req, authInfo);
req.addHeaderField("X-Nintendo-FPD-Version", "0000");
req.addHeaderField("X-Nintendo-Environment", "L1");
req.addHeaderField("X-Nintendo-Title-ID", fmt::format("{:016x}", titleId));
uint32 uniqueId = ((titleId >> 8) & 0xFFFFF);
req.addHeaderField("X-Nintendo-Unique-ID", fmt::format("{:05x}", uniqueId));
req.addHeaderField("X-Nintendo-Application-Version", fmt::format("{:04x}", titleVersion));
req.addHeaderField("Authorization", fmt::format("Bearer {}", oauthToken.token));
if (!req.submitRequest(false))
{
cemuLog_log(LogType::Force, fmt::format("Failed request /provider/service_token/@me"));
result.apiError = NAPI_RESULT::FAILED;
return result;
}
/*
Example response (success):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<service_token>
<token>xxxxxxxxxxxx</token>
</service_token>
*/
// parse result
pugi::xml_document doc;
if (!_parseActResponse(req, result, doc))
return result;
pugi::xml_node tokenNode = doc.child("service_token");
if (!tokenNode)
{
cemuLog_force("Response does not contain service_token node");
result.apiError = NAPI_RESULT::XML_ERROR;
return result;
}
std::string_view token = tokenNode.child_value("token");
result.token = token;
result.apiError = NAPI_RESULT::SUCCESS;
g_IndependentTokenCacheMtx.lock();
g_IndependentTokenCache.emplace_back(authInfo.accountId, authInfo.passwordHash, clientId, result.token, 3600);
g_IndependentTokenCacheMtx.unlock();
return result;
}
ACTConvertNnidToPrincipalIdResult ACT_ACTConvertNnidToPrincipalId(AuthInfo& authInfo, std::string_view nnid)
{
ACTConvertNnidToPrincipalIdResult result{};
// get Independent token
ACTOauthToken oauthToken = ACT_GetOauthToken_WithCache(authInfo, 0x0005001010001C00, 0);
if (!oauthToken.isValid())
{
cemuLog_force("ACT_ACTConvertNnidToPrincipalId(): Failed to retrieve OAuth token");
if (oauthToken.apiError == NAPI_RESULT::SERVICE_ERROR)
{
result.apiError = NAPI_RESULT::SERVICE_ERROR;
result.serviceError = oauthToken.serviceError;
}
else
{
result.apiError = NAPI_RESULT::DATA_ERROR;
}
return result;
}
// do request
CurlRequestHelper req;
req.initate(fmt::format("{}/v1/api/admin/mapped_ids?input_type=user_id&output_type=pid&input={}", LaunchSettings::GetActURLPrefix(), nnid), CurlRequestHelper::SERVER_SSL_CONTEXT::ACT);
_ACTSetCommonHeaderParameters(req, authInfo);
_ACTSetDeviceParameters(req, authInfo);
_ACTSetRegionAndCountryParameters(req, authInfo);
req.addHeaderField("X-Nintendo-FPD-Version", "0000");
req.addHeaderField("X-Nintendo-Environment", "L1");
req.addHeaderField("X-Nintendo-Title-ID", fmt::format("{:016x}", 0x0005001010001C00));
uint32 uniqueId = 0x50010;
req.addHeaderField("X-Nintendo-Unique-ID", fmt::format("{:05x}", uniqueId));
req.addHeaderField("X-Nintendo-Application-Version", fmt::format("{:04x}", 0));
req.addHeaderField("Authorization", fmt::format("Bearer {}", oauthToken.token));
if (!req.submitRequest(false))
{
cemuLog_log(LogType::Force, fmt::format("Failed request /admin/mapped_ids"));
result.apiError = NAPI_RESULT::FAILED;
return result;
}
/*
Example response (success):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<mapped_ids>
<mapped_id>
<in_id>input-nnid</in_id>
<out_id>12345</out_id>
</mapped_id>
</mapped_ids>
*/
// parse result
pugi::xml_document doc;
if (!_parseActResponse(req, result, doc))
return result;
pugi::xml_node tokenNode = doc.child("mapped_ids");
if (!tokenNode)
{
cemuLog_force("Response does not contain mapped_ids node");
result.apiError = NAPI_RESULT::XML_ERROR;
return result;
}
tokenNode = tokenNode.child("mapped_id");
if (!tokenNode)
{
cemuLog_force("Response does not contain mapped_id node");
result.apiError = NAPI_RESULT::XML_ERROR;
return result;
}
std::string_view pidString = tokenNode.child_value("out_id");
if (!pidString.empty())
{
result.isFound = true;
result.principalId = StringHelpers::ToInt(pidString);
}
else
{
result.isFound = false;
result.principalId = 0;
}
result.apiError = NAPI_RESULT::SUCCESS;
return result;
}
bool NAPI_MakeAuthInfoFromCurrentAccount(AuthInfo& authInfo)
{
authInfo = {};
if (!NCrypto::SEEPROM_IsPresent())
return false;
const Account& account = Account::GetCurrentAccount();
authInfo.accountId = account.GetAccountId();
auto passwordHash = account.GetAccountPasswordCache();
authInfo.passwordHash = passwordHash;
if (std::all_of(passwordHash.cbegin(), passwordHash.cend(), [](uint8 v) { return v == 0; }))
{
static bool s_showedLoginError = false;
if (!s_showedLoginError)
{
cemuLog_force("Account login is impossible because the cached password hash is not set");
s_showedLoginError = true;
}
return false; // password hash not set
}
authInfo.deviceId = NCrypto::GetDeviceId();
authInfo.serial = NCrypto::GetSerial();
authInfo.region = NCrypto::SEEPROM_GetRegion();
authInfo.country = NCrypto::GetCountryAsString(account.GetCountry());
authInfo.deviceCertBase64 = NCrypto::CertECC::GetDeviceCertificate().encodeToBase64();
return true;
}
}