1#include <asio/connect.hpp>
2#include <asio/ip/tcp.hpp>
3#include <asio/read.hpp>
4#include <asio/read_until.hpp>
5#include <asio/streambuf.hpp>
9#include <openssl/sha.h>
33 static const std::string botcraft_app_id =
"a0ad834d-e78a-4881-87f6-390aa0f4b283";
34 static const std::string cached_credentials_path =
"botcraft_cached_credentials.json";
35 static const Json::Value default_cached_credentials = {
37 {
"access_token",
nullptr },
38 {
"expires_date",
nullptr },
39 {
"refresh_token",
nullptr }
43 {
"mc_token",
nullptr },
44 {
"expires_date",
nullptr }
50 Json::Value LoadCachedAccount(
const std::string& key);
53 void SaveCachedAccount(
const std::string& key,
const Json::Value& account);
58 std::string GetMCTokenFromMicrosoft(
Json::Value& cached);
63 std::pair<std::string, std::string> GetMCProfile(
const std::string& mc_token);
68 long long int TimestampMilliFromISO8601(
const std::string& s);
76#if PROTOCOL_VERSION > 758
78 rnd = std::mt19937(
static_cast<unsigned int>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count()));
85#if PROTOCOL_VERSION > 758
106 const long long int now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
107 const bool cached_ok =
113 if (cached_ok && cached[
"expires_date"].get<long long int>() >= now)
115 LOG_INFO(
"Cached Minecraft token for Microsoft account still valid.");
124 LOG_INFO(
"Cached Minecraft token for Microsoft account expired, starting auth flow...");
128 LOG_WARNING(
"Missing or malformed cached credentials for Microsoft account, starting auth flow...");
136 LOG_INFO(
"Assuming the account owns Minecraft...");
138 LOG_INFO(
"Trying to get MC profile...");
148#if PROTOCOL_VERSION > 758
151 LOG_INFO(
"Authentication completed!");
154 catch (
const std::exception& e)
164#ifndef USE_ENCRYPTION
171 LOG_INFO(
"Trying to get MC profile...");
176#if PROTOCOL_VERSION > 758
179 LOG_INFO(
"Authentication completed!");
182 catch (
const std::exception& e)
190 bool Authentifier::JoinServer(
const std::string& server_id,
const std::vector<unsigned char>& shared_secret,
const std::vector<unsigned char>& server_public_key)
const
192#ifndef USE_ENCRYPTION
197 LOG_ERROR(
"Trying to join a server before authentication");
202 SHA1_Init(&sha_context);
204 SHA1_Update(&sha_context, server_id.c_str(), server_id.length());
205 SHA1_Update(&sha_context, shared_secret.data(), shared_secret.size());
206 SHA1_Update(&sha_context, server_public_key.data(), server_public_key.size());
208 std::vector<unsigned char> digest(SHA_DIGEST_LENGTH);
209 SHA1_Final(digest.data(), &sha_context);
213 bool is_negative = digest[0] & (1 << 7);
219 for (
int i = 0; i < digest.size(); ++i)
221 digest[i] = ~digest[i];
225 int position =
static_cast<int>(digest.size()) - 1;
226 while (digest[position] == 255 && position > 0)
228 digest[position] = 0;
231 digest[position] += 1;
235 std::stringstream ss;
236 for (
int i = 0; i < digest.size(); ++i)
238 ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(digest[i] & 0xFF);
241 std::string server_hash = ss.str();
243 const size_t start = server_hash.find_first_not_of(
'0');
244 if (start != std::string::npos)
246 server_hash = server_hash.substr(start);
255 server_hash =
"-" + server_hash;
262 {
"serverId", server_hash}
266 "application/json; charset=utf-8",
"*/*",
"", data.
Dump());
271 <<
" (" << post_response.
status_message <<
") during server join:\n"
290#if PROTOCOL_VERSION > 758
311#if PROTOCOL_VERSION == 759
313#elif PROTOCOL_VERSION == 760
315 const std::vector<unsigned char>& previous_signature,
const std::vector<LastSeenMessagesEntry>& last_seen,
316 long long int& salt,
long long int& timestamp)
319 const int message_sent_index,
const UUID& chat_session_uuid,
320 const std::vector<std::vector<unsigned char>>& last_seen,
321 long long int& salt,
long long int& timestamp)
324#ifndef USE_ENCRYPTION
325 LOG_ERROR(
"Trying to compute message signature while botcraft was compiled without USE_ENCRYPTION.");
328 if (mc_player_uuid.empty() || private_key ==
nullptr)
330 LOG_ERROR(
"Trying to compute message signature before authentication");
335 salt = std::uniform_int_distribution<long long int>(std::numeric_limits<long long int>::min(), std::numeric_limits<long long int>::max())(rnd);
336 timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
337 std::array<unsigned char, 8> salt_bytes;
338 std::array<unsigned char, 8> timestamp_bytes;
340 for (
int i = 0; i < 8; ++i)
342 salt_bytes[i] =
static_cast<unsigned char>((salt >> (8 * (7 - i))) & 0xFF);
344 timestamp_bytes[i] =
static_cast<unsigned char>(((timestamp / 1000) >> (8 * (7 - i))) & 0xFF);
347 std::array<unsigned char, SHA256_DIGEST_LENGTH> signature_hash;
348#if PROTOCOL_VERSION == 759
350 const std::string jsoned_message =
"{\"text\":\"" + message +
"\"}";
354 SHA256_Init(&sha256);
355 SHA256_Update(&sha256, salt_bytes.data(), salt_bytes.size());
356 SHA256_Update(&sha256, mc_player_uuid_bytes.data(), mc_player_uuid_bytes.size());
357 SHA256_Update(&sha256, timestamp_bytes.data(), timestamp_bytes.size());
358 SHA256_Update(&sha256, jsoned_message.data(), jsoned_message.size());
359#elif PROTOCOL_VERSION == 760
360 const unsigned char const_byte_70 = 70;
363 std::array<unsigned char, SHA256_DIGEST_LENGTH> body_hash;
364 SHA256_CTX body_sha256;
365 SHA256_Init(&body_sha256);
366 SHA256_Update(&body_sha256, salt_bytes.data(), salt_bytes.size());
367 SHA256_Update(&body_sha256, timestamp_bytes.data(), timestamp_bytes.size());
368 SHA256_Update(&body_sha256, message.data(), message.size());
369 SHA256_Update(&body_sha256, &const_byte_70, 1);
371 for (
int i = 0; i < last_seen.size(); ++i)
373 SHA256_Update(&body_sha256, &const_byte_70, 1);
374 SHA256_Update(&body_sha256, last_seen[i].GetProfileId().data(), last_seen[i].GetProfileId().size());
375 SHA256_Update(&body_sha256, last_seen[i].GetLastSignature().data(), last_seen[i].GetLastSignature().size());
377 SHA256_Final(body_hash.data(), &body_sha256);
382 SHA256_Init(&sha256);
383 if (!previous_signature.empty())
385 SHA256_Update(&sha256, previous_signature.data(), previous_signature.size());
387 SHA256_Update(&sha256, mc_player_uuid_bytes.data(), mc_player_uuid_bytes.size());
388 SHA256_Update(&sha256, body_hash.data(), body_hash.size());
390 std::array<unsigned char, 4> bytes_1_big_endian;
391 std::array<unsigned char, 4> message_sent_index_bytes;
392 std::array<unsigned char, 4> message_size_bytes;
393 std::array<unsigned char, 4> last_seen_size_bytes;
395 for (
int i = 0; i < 4; ++i)
397 bytes_1_big_endian[i] =
static_cast<unsigned char>((1 >> (8 * (3 - i))) & 0xFF);
398 message_sent_index_bytes[i] =
static_cast<unsigned char>((message_sent_index >> (8 * (3 - i))) & 0xFF);
399 message_size_bytes[i] =
static_cast<unsigned char>((
static_cast<int>(message.size()) >> (8 * (3 - i))) & 0xFF);
400 last_seen_size_bytes[i] =
static_cast<unsigned char>((
static_cast<int>(last_seen.size()) >> (8 * (3 - i))) & 0xFF);
405 SHA256_Init(&sha256);
406 SHA256_Init(&sha256);
408 SHA256_Update(&sha256, bytes_1_big_endian.data(), bytes_1_big_endian.size());
410 SHA256_Update(&sha256, mc_player_uuid_bytes.data(), mc_player_uuid_bytes.size());
411 SHA256_Update(&sha256, chat_session_uuid.data(), chat_session_uuid.size());
412 SHA256_Update(&sha256, message_sent_index_bytes.data(), message_sent_index_bytes.size());
414 SHA256_Update(&sha256, salt_bytes.data(), salt_bytes.size());
415 SHA256_Update(&sha256, timestamp_bytes.data(), timestamp_bytes.size());
416 SHA256_Update(&sha256, message_size_bytes.data(), message_size_bytes.size());
417 SHA256_Update(&sha256, message.data(), message.size());
418 SHA256_Update(&sha256, last_seen_size_bytes.data(), last_seen_size_bytes.size());
419 for (
size_t i = 0; i < last_seen.size(); ++i)
421 SHA256_Update(&sha256, last_seen[i].data(), last_seen[i].size());
424 SHA256_Final(signature_hash.data(), &sha256);
427 const int private_key_size = RSA_size(private_key);
428 std::vector<unsigned char> signature(private_key_size);
429 unsigned int signature_size;
430 RSA_sign(NID_sha256, signature_hash.data(),
static_cast<unsigned int>(signature_hash.size()), signature.data(), &signature_size, private_key);
431 signature.resize(signature_size);
440 for (
int i = 0; i < 32; i += 2)
443 mc_player_uuid_bytes[i / 2] =
static_cast<unsigned char>(std::strtol(byte_str.c_str(),
nullptr, 16));
448#if PROTOCOL_VERSION > 758
453 LOG_INFO(
"Getting player certificates...");
455#if PROTOCOL_VERSION == 759
456 const std::string signature_field =
"publicKeySignature";
458 const std::string signature_field =
"publicKeySignatureV2";
461 POSTRequest(
"api.minecraftservices.com",
"/player/certificates",
"application/json",
"application/json",
"Bearer " +
mc_access_token,
""),
462 "player certificates acquisition",
463 {
"keyPair", signature_field,
"expiresAt" }
467 if (!key_pair.contains(
"privateKey") || !key_pair.contains(
"publicKey"))
469 throw std::runtime_error(
"Error trying to get player certificates, no keyPair/privateKey or keyPair/publicKey in response");
477 BIO* keybio = BIO_new_mem_buf((
void*)key_pair[
"privateKey"].get_string().c_str(), -1);
478 private_key = PEM_read_bio_RSAPrivateKey(keybio,
nullptr,
nullptr,
nullptr);
481 public_key = key_pair[
"publicKey"].get_string();
484 key_timestamp = TimestampMilliFromISO8601(response[
"expiresAt"].get_string());
486 LOG_INFO(
"Player certificates obtained!");
496 std::ifstream file(cached_credentials_path, std::ios::in);
497 std::stringstream content;
499 content << file.rdbuf();
500 return content.str().empty() ?
Json::Object() : Json::Parse(content.str(), true);
503 Json::Value LoadCachedAccount(
const std::string& key)
509 throw std::runtime_error(
"Error trying to read " + cached_credentials_path +
", fix or delete it");
511 if (profiles.
contains(key) && profiles[key].is_object())
513 return profiles[key];
515 return default_cached_credentials;
518 void SaveCachedAccount(
const std::string& key,
const Json::Value& account)
524 LOG_ERROR(
"Error trying to read " << cached_credentials_path <<
", credentials not saved");
527 profiles[key] = account;
528 std::ofstream file(cached_credentials_path);
529 file << profiles.
Dump(4) << std::endl;
535 POSTRequest(
"login.microsoftonline.com",
"/consumers/oauth2/v2.0/devicecode",
"application/x-www-form-urlencoded",
"*/*",
"",
536 "client_id=" + botcraft_app_id +
"&scope=XboxLive.signin%20offline_access"),
537 "microsoft authentification", {
"interval",
"message",
"device_code" }
541 LOG_ALWAYS(auth_response[
"message"].get_string());
543 const std::string check_auth_status_data =
544 "client_id=" + botcraft_app_id +
545 "&scope=XboxLive.signin%20offline_access" +
546 "&grant_type=urn:ietf:params:oauth:grant-type:device_code" +
547 "&device_code=" + auth_response[
"device_code"].
get_string();
549 long long int poll_interval = auth_response[
"interval"].
get_number<
long long int>();
550 const long long int expires_in = auth_response.
contains(
"expires_in") ? auth_response[
"expires_in"].
get_number<
long long int>() : 900;
551 const std::chrono::steady_clock::time_point deadline = std::chrono::steady_clock::now() + std::chrono::seconds(expires_in);
552 while (std::chrono::steady_clock::now() < deadline)
554 std::this_thread::sleep_for(std::chrono::seconds(poll_interval + 1));
556 const WebRequestResponse status =
POSTRequest(
"login.microsoftonline.com",
"/consumers/oauth2/v2.0/token",
557 "application/x-www-form-urlencoded",
"*/*",
"", check_auth_status_data);
559 if (status.status_code == 400 && status.response.contains(
"error"))
561 const std::string error = status.response[
"error"].get_string();
562 if (error ==
"authorization_pending")
566 if (error ==
"slow_down")
572 throw std::runtime_error(
"Error during microsoft device authentication: " + error +
573 (status.response.contains(
"error_description") ?
" (" + status.response[
"error_description"].get_string() +
")" :
""));
576 const Json::Value response =
CheckJsonResponse(status,
"microsoft device authentification check", {
"access_token",
"refresh_token",
"expires_in" });
577 LOG_INFO(
"Newly obtained Microsoft token stored in cache");
580 throw std::runtime_error(
"Microsoft device authentication timed out");
585 const long long int now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
589 const bool cached_ok =
594 if (cached_ok && msa[
"expires_date"].get<long long int>() >= now)
596 LOG_INFO(
"Cached Microsoft token still valid");
606 LOG_ERROR(
"Error trying to get cached Microsoft credentials");
607 LOG_INFO(
"Starting authentication process...");
608 response = MSAAuthDeviceFlow();
612 LOG_INFO(
"Refreshing Microsoft token...");
616 "application/x-www-form-urlencoded",
"*/*",
"",
617 "client_id=" + botcraft_app_id +
618 "&refresh_token=" + msa[
"refresh_token"].get_string() +
619 "&grant_type=refresh_token" +
620 "&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient"),
621 "Microsoft token refresh", {
"access_token",
"refresh_token",
"expires_in" });
622 LOG_INFO(
"Cached Microsoft token refreshed");
624 catch (
const std::exception& e)
628 LOG_INFO(
"Failed to refresh token, starting Microsoft authentication process...");
629 response = MSAAuthDeviceFlow();
634 {
"access_token", response[
"access_token"].
get_string() },
635 {
"refresh_token", response[
"refresh_token"].
get_string() },
636 {
"expires_date", response[
"expires_in"].
get<
long long int>() + now }
641 std::string GetXBLToken(
const std::string& msa_token)
645 {
"AuthMethod",
"RPS"},
646 {
"SiteName",
"user.auth.xboxlive.com"},
647 {
"RpsTicket",
"d=" + msa_token}
650 {
"RelyingParty",
"http://auth.xboxlive.com"},
651 {
"TokenType",
"JWT"}
655 POSTRequest(
"user.auth.xboxlive.com",
"/user/authenticate",
"application/json",
"application/json",
"", request_data.
Dump()),
656 "XBL authentication",
661 std::pair<std::string, std::string> GetXSTSToken(
const std::string& xbl_token)
665 {
"SandboxId",
"RETAIL"},
666 {
"UserTokens", { xbl_token } }
668 {
"RelyingParty",
"rp://api.minecraftservices.com/"},
669 {
"TokenType",
"JWT"}
673 POSTRequest(
"xsts.auth.xboxlive.com",
"/xsts/authorize",
"application/json",
"application/json",
"", request_data.
Dump()),
674 "XSTS authentication",
675 {
"Token",
"DisplayClaims" }
678 const Json::Value& display_claims = response[
"DisplayClaims"];
681 !display_claims[
"xui"].is_array() ||
682 display_claims[
"xui"].size() < 1 ||
683 !display_claims[
"xui"][0].contains(
"uhs")
686 throw std::runtime_error(
"Error trying to get XSTS token, no DisplayClaims/xui/0/uhs in authentication response");
692 std::string GetMCTokenFromMicrosoft(
Json::Value& cached)
694 LOG_INFO(
"Trying to get Microsoft access token...");
695 const std::string msa_token = GetMSAToken(cached);
697 LOG_INFO(
"Trying to get XBL token...");
698 const std::string xbl_token = GetXBLToken(msa_token);
701 LOG_INFO(
"Trying to get XSTS token...");
702 const auto [xsts_token, user_hash] = GetXSTSToken(xbl_token);
705 LOG_INFO(
"Trying to get MC token...");
706 const long long int now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
712 {
"identityToken",
"XBL3.0 x=" + user_hash +
";" + xsts_token }
715 POSTRequest(
"api.minecraftservices.com",
"/authentication/login_with_xbox",
"application/json",
"application/json",
"", request_data.
Dump()),
720 if (response.
contains(
"expires_in"))
722 cached[
"mc_token"] = response[
"access_token"].
get_string();
723 cached[
"expires_date"] = response[
"expires_in"].
get<
long long int>() + now;
728 LOG_WARNING(
"No expires_in in authentication response of MC");
730 LOG_INFO(
"MC token obtained! Almost there...");
735 std::pair<std::string, std::string> GetMCProfile(
const std::string& mc_token)
738 GETRequest(
"api.minecraftservices.com",
"/minecraft/profile",
"Bearer " + mc_token),
739 "MC profile retrieval",
746 int IsLeap(
const int year)
748 if (year % 400 == 0) {
return 1; }
749 if (year % 100 == 0) {
return 0; }
750 if (year % 4 == 0) {
return 1; }
754 int DaysFrom0(
const int year)
756 const long long y = year - 1;
757 return 365 * y + y / 400 - y / 100 + y / 4;
760 int DaysFrom1970(int32_t year)
762 return DaysFrom0(year) - DaysFrom0(1970);
765 int DaysFrom1Jan(
const int year,
const int month,
const int day)
767 static constexpr std::array<std::array<int, 12>, 2> days =
769 { 0,31,59,90,120,151,181,212,243,273,304,334},
770 { 0,31,60,91,121,152,182,213,244,274,305,335}
773 return days[IsLeap(year)][month - 1] + day - 1;
776 long long int TimestampMilliFromISO8601(
const std::string& s)
779 std::stringstream iso8601(s);
780 iso8601 >> std::get_time(&t,
"%Y-%m-%dT%H:%M:%S");
782 if (iso8601.fail() || t.tm_mon < 0 || t.tm_mon > 11)
784 LOG_ERROR(
"Error trying to parse ISO 8601 string: " << s);
789 if (iso8601.peek() ==
'.')
792 while (millis.size() < 3 && std::isdigit(iso8601.peek()))
794 millis +=
static_cast<char>(iso8601.get());
797 millis.resize(3,
'0');
799 const int year = t.tm_year + 1900;
800 const int month = t.tm_mon + 1;
801 const int day = t.tm_mday;
802 const int days_since_epoch = DaysFrom1970(year) + DaysFrom1Jan(year, month, day);
804 const long long int sec = 3600LL * 24 * days_since_epoch + 3600 * t.tm_hour + 60 * t.tm_min + t.tm_sec;
806 return sec * 1000 + std::stoll(millis);
#define LOG_ERROR(osstream)
#define LOG_ALWAYS(osstream)
#define LOG_WARNING(osstream)
#define LOG_INFO(osstream)
bool JoinServer(const std::string &server_id, const std::vector< unsigned char > &shared_secret, const std::vector< unsigned char > &server_public_key) const
bool AuthMicrosoft(const std::string &cache_key)
Authentication using a Microsoft account, storing the credentials in the cache file.
std::string player_display_name
const std::string & GetPlayerDisplayName() const
long long int key_timestamp
void UpdateUUIDBytes()
Compute the UUID bytes from the string one.
std::string mc_access_token
void GetPlayerCertificates()
Get player certificates using the Minecraft token, throw if failed.
const std::string & GetKeySignature() const
const std::array< unsigned char, 16 > & GetPlayerUUID() const
RSA * GetPrivateKey() const
std::string mc_player_uuid
std::string key_signature
std::array< unsigned char, 16 > mc_player_uuid_bytes
const std::string & GetPublicKey() const
std::vector< unsigned char > GetMessageSignature(const std::string &message, const int message_sent_index, const ProtocolCraft::UUID &chat_session_uuid, const std::vector< std::vector< unsigned char > > &last_seen, long long int &salt, long long int ×tamp)
Compute the signature of a message.
bool AuthMCToken(const std::string &mc_token)
Authentication using a minecraft token.
long long int GetKeyTimestamp() const
A class to execute a callback when destroyed.
Real class declaration, just a derived class of std::map<std::string, Value>
Main class, basically a JsonVariant with extra utility functions it doesn't inherit JsonVariant direc...
std::string Dump(const int indent=-1, const char indent_char=' ') const
public dump interface
bool contains(const std::string &s) const
std::string & get_string()
WebRequestResponse GETRequest(const std::string &host, const std::string &endpoint, const std::string &authorization="")
Send a GET request with ssl stuff.
ProtocolCraft::Json::Value CheckJsonResponse(const WebRequestResponse &response, const std::string &s, const std::vector< std::string > &fields, const unsigned int expected_status=200)
Check that a WebRequestResponse has the right fields and return code, throw if not.
WebRequestResponse POSTRequest(const std::string &host, const std::string &endpoint, const std::string &content_type, const std::string &accept, const std::string &authorization, const std::string &data)
Send a POST request with ssl stuff.
std::array< unsigned char, 16 > UUID
ProtocolCraft::Json::Value response
std::string status_message