Botcraft 26.3
Loading...
Searching...
No Matches
Authentifier.cpp
Go to the documentation of this file.
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>
6
7#ifdef USE_ENCRYPTION
8#include <asio/ssl.hpp>
9#include <openssl/sha.h>
10#endif
11
12#include <fstream>
13#include <iomanip>
14#include <iostream>
15#include <sstream>
16
22
24
25
26using namespace ProtocolCraft;
27
28namespace Botcraft
29{
30#ifdef USE_ENCRYPTION
31 namespace
32 {
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 = {
36 { "msa", {
37 { "access_token", nullptr },
38 { "expires_date", nullptr },
39 { "refresh_token", nullptr }
40 }},
41 { "name", nullptr },
42 { "id", nullptr },
43 { "mc_token", nullptr },
44 { "expires_date", nullptr }
45 };
46
47 /// @brief Get the cached credentials of one account, throw if the cache file is not valid JSON
48 /// @param key Key of the account in the cache file
49 /// @return Cached credentials, or the default ones if the account is not in the file
50 Json::Value LoadCachedAccount(const std::string& key);
51
52 /// @brief Save the credentials of one account, keeping the other accounts of the file
53 void SaveCachedAccount(const std::string& key, const Json::Value& account);
54
55 /// @brief Get a Minecraft token using the Microsoft auth flow (MSA -> XBL -> XSTS -> MC)
56 /// @param cached Cached credentials of the account. "msa", "mc_token" and "expires_date" are erased, then set again on success
57 /// @return The Minecraft access token, throw if failed
58 std::string GetMCTokenFromMicrosoft(Json::Value& cached);
59
60 /// @brief Get the Minecraft profile of a Minecraft token
61 /// @param mc_token Minecraft access token
62 /// @return Pair of {uuid, name}, throw if failed
63 std::pair<std::string, std::string> GetMCProfile(const std::string& mc_token);
64
65 /// @brief Convert ISO8601 time to timestamp
66 /// @param s Date/time
67 /// @return Number of ms since epoch
68 long long int TimestampMilliFromISO8601(const std::string& s);
69 }
70#endif
71
72
74 {
76#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
77 key_timestamp = 0;
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()));
79#endif
80 }
81
83 {
84#ifdef USE_ENCRYPTION
85#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
86 if (private_key != nullptr)
87 {
88 RSA_free(private_key);
89 private_key = nullptr;
90 }
91#endif
92#endif
93 }
94
95 bool Authentifier::AuthMicrosoft(const std::string& cache_key)
96 {
97#ifndef USE_ENCRYPTION
98 return false;
99#else
100 try
101 {
102 Json::Value cached = LoadCachedAccount(cache_key);
103 // Cached value will be updated upon leaving the scope. If any step fails, value will have missing fields to make sure next run doesn't use garbage values
104 Utilities::OnEndScope save_cache([&]() { SaveCachedAccount(cache_key, cached); });
105
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 =
108 cached.contains("mc_token") && cached["mc_token"].is_string() &&
109 cached.contains("expires_date") && cached["expires_date"].is_number() &&
110 cached.contains("name") && cached["name"].is_string() &&
111 cached.contains("id") && cached["id"].is_string();
112
113 if (cached_ok && cached["expires_date"].get<long long int>() >= now)
114 {
115 LOG_INFO("Cached Minecraft token for Microsoft account still valid.");
116 mc_access_token = cached["mc_token"].get_string();
117 player_display_name = cached["name"].get_string();
118 mc_player_uuid = cached["id"].get_string();
119 }
120 else
121 {
122 if (cached_ok)
123 {
124 LOG_INFO("Cached Minecraft token for Microsoft account expired, starting auth flow...");
125 }
126 else
127 {
128 LOG_WARNING("Missing or malformed cached credentials for Microsoft account, starting auth flow...");
129 }
130
131 mc_access_token = GetMCTokenFromMicrosoft(cached);
132
133 // We assume you're using an account owning minecraft so we don't check (and also a bit because it's complicated)
134 // If you don't, Botcraft won't work on online mode.
135 // But you can buy yourself a copy of the game: https://www.minecraft.net/get-minecraft
136 LOG_INFO("Assuming the account owns Minecraft...");
137
138 LOG_INFO("Trying to get MC profile...");
139 cached.get_object().erase("id");
140 cached.get_object().erase("name");
141 std::tie(mc_player_uuid, player_display_name) = GetMCProfile(mc_access_token);
142 cached["id"] = mc_player_uuid;
143 cached["name"] = player_display_name;
144 LOG_INFO("MC profile obtained!");
145 }
147
148#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
150#endif
151 LOG_INFO("Authentication completed!");
152 return true;
153 }
154 catch (const std::exception& e)
155 {
156 LOG_ERROR(e.what());
157 return false;
158 }
159#endif
160 }
161
162 bool Authentifier::AuthMCToken(const std::string& mc_token)
163 {
164#ifndef USE_ENCRYPTION
165 return false;
166#else
167 try
168 {
169 mc_access_token = mc_token;
170
171 LOG_INFO("Trying to get MC profile...");
172 std::tie(mc_player_uuid, player_display_name) = GetMCProfile(mc_access_token);
174 LOG_INFO("MC profile obtained!");
175
176#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
178#endif
179 LOG_INFO("Authentication completed!");
180 return true;
181 }
182 catch (const std::exception& e)
183 {
184 LOG_ERROR(e.what());
185 return false;
186 }
187#endif
188 }
189
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
191 {
192#ifndef USE_ENCRYPTION
193 return false;
194#else
195 if (mc_player_uuid.empty())
196 {
197 LOG_ERROR("Trying to join a server before authentication");
198 return false;
199 }
200
201 SHA_CTX sha_context;
202 SHA1_Init(&sha_context);
203
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());
207
208 std::vector<unsigned char> digest(SHA_DIGEST_LENGTH);
209 SHA1_Final(digest.data(), &sha_context);
210
211 // Compute minecraft special hexdigest (see https://wiki.vg/Protocol_Encryption#Client)
212
213 bool is_negative = digest[0] & (1 << 7);
214
215 // Take two complement
216 if (is_negative)
217 {
218 // Revert bits
219 for (int i = 0; i < digest.size(); ++i)
220 {
221 digest[i] = ~digest[i];
222 }
223
224 // add 1
225 int position = static_cast<int>(digest.size()) - 1;
226 while (digest[position] == 255 && position > 0)
227 {
228 digest[position] = 0;
229 position -= 1;
230 }
231 digest[position] += 1;
232 }
233
234 // Get hex representation
235 std::stringstream ss;
236 for (int i = 0; i < digest.size(); ++i)
237 {
238 ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(digest[i] & 0xFF);
239 }
240
241 std::string server_hash = ss.str();
242 // Remove leading 0
243 const size_t start = server_hash.find_first_not_of('0');
244 if (start != std::string::npos)
245 {
246 server_hash = server_hash.substr(start);
247 }
248 else
249 {
250 server_hash = "";
251 }
252
253 if (is_negative)
254 {
255 server_hash = "-" + server_hash;
256 }
257
258 // Prepare the data to send to the server
259 const Json::Value data = {
260 { "accessToken", mc_access_token },
261 { "selectedProfile", mc_player_uuid },
262 { "serverId", server_hash}
263 };
264
265 const WebRequestResponse post_response = POSTRequest("sessionserver.mojang.com", "/session/minecraft/join",
266 "application/json; charset=utf-8", "*/*", "", data.Dump());
267
268 if (post_response.status_code != 204)
269 {
270 LOG_ERROR("Response returned with status code " << post_response.status_code
271 << " (" << post_response.status_message << ") during server join:\n"
272 << post_response.response.Dump(4));
273 return false;
274 }
275
276 return true;
277#endif
278 }
279
280 const std::string& Authentifier::GetPlayerDisplayName() const
281 {
282 return player_display_name;
283 }
284
285 const std::array<unsigned char, 16>& Authentifier::GetPlayerUUID() const
286 {
288 }
289
290#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
292 {
293 return private_key;
294 }
295
296 const std::string& Authentifier::GetPublicKey() const
297 {
298 return public_key;
299 }
300
301 const std::string& Authentifier::GetKeySignature() const
302 {
303 return key_signature;
304 }
305
306 long long int Authentifier::GetKeyTimestamp() const
307 {
308 return key_timestamp;
309 }
310
311#if PROTOCOL_VERSION == 759 /* 1.19 */
312 std::vector<unsigned char> Authentifier::GetMessageSignature(const std::string& message, long long int& salt, long long int& timestamp)
313#elif PROTOCOL_VERSION == 760 /* 1.19.1/2 */
314 std::vector<unsigned char> Authentifier::GetMessageSignature(const std::string& message,
315 const std::vector<unsigned char>& previous_signature, const std::vector<LastSeenMessagesEntry>& last_seen,
316 long long int& salt, long long int& timestamp)
317#else
318 std::vector<unsigned char> Authentifier::GetMessageSignature(const std::string& message,
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)
322#endif
323 {
324#ifndef USE_ENCRYPTION
325 LOG_ERROR("Trying to compute message signature while botcraft was compiled without USE_ENCRYPTION.");
326 return {};
327#else
328 if (mc_player_uuid.empty() || private_key == nullptr)
329 {
330 LOG_ERROR("Trying to compute message signature before authentication");
331 return {};
332 }
333
334 // Generate random salt and timestamp
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;
339
340 for (int i = 0; i < 8; ++i)
341 {
342 salt_bytes[i] = static_cast<unsigned char>((salt >> (8 * (7 - i))) & 0xFF);
343 // Signature is computed with seconds not milliseconds
344 timestamp_bytes[i] = static_cast<unsigned char>(((timestamp / 1000) >> (8 * (7 - i))) & 0xFF);
345 }
346
347 std::array<unsigned char, SHA256_DIGEST_LENGTH> signature_hash;
348#if PROTOCOL_VERSION == 759 /* 1.19 */
349 // Signature is computed with a dumb json instead of the actual string
350 const std::string jsoned_message = "{\"text\":\"" + message + "\"}";
351
352 // Compute hash
353 SHA256_CTX sha256;
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 /* 1.19.1/2 */
360 const unsigned char const_byte_70 = 70;
361
362 // Body hash
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);
370 // All previously seen messages
371 for (int i = 0; i < last_seen.size(); ++i)
372 {
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());
376 }
377 SHA256_Final(body_hash.data(), &body_sha256);
378
379
380 // Signature hash
381 SHA256_CTX sha256;
382 SHA256_Init(&sha256);
383 if (!previous_signature.empty())
384 {
385 SHA256_Update(&sha256, previous_signature.data(), previous_signature.size());
386 }
387 SHA256_Update(&sha256, mc_player_uuid_bytes.data(), mc_player_uuid_bytes.size());
388 SHA256_Update(&sha256, body_hash.data(), body_hash.size());
389#else
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;
394
395 for (int i = 0; i < 4; ++i)
396 {
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);
401 }
402
403 // Compute hash
404 SHA256_CTX sha256;
405 SHA256_Init(&sha256);
406 SHA256_Init(&sha256);
407 // Big endian (int)1
408 SHA256_Update(&sha256, bytes_1_big_endian.data(), bytes_1_big_endian.size());
409 // signed message link
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());
413 // signed message body
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)
420 {
421 SHA256_Update(&sha256, last_seen[i].data(), last_seen[i].size());
422 }
423#endif
424 SHA256_Final(signature_hash.data(), &sha256);
425
426 // Compute signature
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);
432
433 return signature;
434#endif
435 }
436#endif
437
439 {
440 for (int i = 0; i < 32; i += 2)
441 {
442 const std::string byte_str = mc_player_uuid.substr(i, 2);
443 mc_player_uuid_bytes[i / 2] = static_cast<unsigned char>(std::strtol(byte_str.c_str(), nullptr, 16));
444 }
445 }
446
447#ifdef USE_ENCRYPTION
448#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
450 {
451 // Certificates are not cached cause they're sometimes made invalid before the expire date (not sure why or when)
452 // So instead we get new ones for each connection (vanilla like behaviour)
453 LOG_INFO("Getting player certificates...");
454
455#if PROTOCOL_VERSION == 759 /* 1.19 */
456 const std::string signature_field = "publicKeySignature";
457#else
458 const std::string signature_field = "publicKeySignatureV2";
459#endif
460 const Json::Value response = CheckJsonResponse(
461 POSTRequest("api.minecraftservices.com", "/player/certificates", "application/json", "application/json", "Bearer " + mc_access_token, ""),
462 "player certificates acquisition",
463 { "keyPair", signature_field, "expiresAt" }
464 );
465
466 const Json::Value& key_pair = response["keyPair"];
467 if (!key_pair.contains("privateKey") || !key_pair.contains("publicKey"))
468 {
469 throw std::runtime_error("Error trying to get player certificates, no keyPair/privateKey or keyPair/publicKey in response");
470 }
471
472 // Extract signature key from PEM string
473 if (private_key != nullptr)
474 {
475 RSA_free(private_key);
476 }
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);
479 BIO_free(keybio);
480
481 public_key = key_pair["publicKey"].get_string();
482 key_signature = response[signature_field].get_string();
483 // Convert expires date in ISO8601 to ms since UNIX epoch
484 key_timestamp = TimestampMilliFromISO8601(response["expiresAt"].get_string());
485
486 LOG_INFO("Player certificates obtained!");
487 }
488#endif
489#endif
490
491#ifdef USE_ENCRYPTION
492 namespace
493 {
494 Json::Value ReadCacheFile()
495 {
496 std::ifstream file(cached_credentials_path, std::ios::in);
497 std::stringstream content;
498 // If the file is missing, nothing is read and content stays empty
499 content << file.rdbuf();
500 return content.str().empty() ? Json::Object() : Json::Parse(content.str(), true);
501 }
502
503 Json::Value LoadCachedAccount(const std::string& key)
504 {
505 const Json::Value profiles = ReadCacheFile();
506 // File is not valid
507 if (!profiles.is_object())
508 {
509 throw std::runtime_error("Error trying to read " + cached_credentials_path + ", fix or delete it");
510 }
511 if (profiles.contains(key) && profiles[key].is_object())
512 {
513 return profiles[key];
514 }
515 return default_cached_credentials;
516 }
517
518 void SaveCachedAccount(const std::string& key, const Json::Value& account)
519 {
520 // Read again: another bot of this program may have saved its account since we loaded ours
521 Json::Value profiles = ReadCacheFile();
522 if (!profiles.is_object())
523 {
524 LOG_ERROR("Error trying to read " << cached_credentials_path << ", credentials not saved");
525 return;
526 }
527 profiles[key] = account;
528 std::ofstream file(cached_credentials_path);
529 file << profiles.Dump(4) << std::endl;
530 }
531
532 Json::Value MSAAuthDeviceFlow()
533 {
534 const Json::Value auth_response = CheckJsonResponse(
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" }
538 );
539
540 // Display the instructions the user has to follow to authenticate in the console
541 LOG_ALWAYS(auth_response["message"].get_string());
542
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();
548
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)
553 {
554 std::this_thread::sleep_for(std::chrono::seconds(poll_interval + 1));
555
556 const WebRequestResponse status = POSTRequest("login.microsoftonline.com", "/consumers/oauth2/v2.0/token",
557 "application/x-www-form-urlencoded", "*/*", "", check_auth_status_data);
558
559 if (status.status_code == 400 && status.response.contains("error"))
560 {
561 const std::string error = status.response["error"].get_string();
562 if (error == "authorization_pending")
563 {
564 continue;
565 }
566 if (error == "slow_down")
567 {
568 poll_interval += 5;
569 continue;
570 }
571 // authorization_declined, expired_token, bad_verification_code, invalid_grant...
572 throw std::runtime_error("Error during microsoft device authentication: " + error +
573 (status.response.contains("error_description") ? " (" + status.response["error_description"].get_string() + ")" : ""));
574 }
575
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");
578 return response;
579 }
580 throw std::runtime_error("Microsoft device authentication timed out");
581 }
582
583 std::string GetMSAToken(Json::Value& cached)
584 {
585 const long long int now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
586
587 // Copy, as "msa" is erased below
588 const Json::Value msa = cached["msa"];
589 const bool cached_ok =
590 msa.contains("refresh_token") && msa["refresh_token"].is_string() &&
591 msa.contains("access_token") && msa["access_token"].is_string() &&
592 msa.contains("expires_date") && msa["expires_date"].is_number();
593
594 if (cached_ok && msa["expires_date"].get<long long int>() >= now)
595 {
596 LOG_INFO("Cached Microsoft token still valid");
597 return msa["access_token"].get_string();
598 }
599
600 // Set again at the end, only if we get a new token
601 cached.get_object().erase("msa");
602
603 Json::Value response;
604 if (!cached_ok)
605 {
606 LOG_ERROR("Error trying to get cached Microsoft credentials");
607 LOG_INFO("Starting authentication process...");
608 response = MSAAuthDeviceFlow();
609 }
610 else
611 {
612 LOG_INFO("Refreshing Microsoft token...");
613 try
614 {
615 response = CheckJsonResponse(POSTRequest("login.live.com", "/oauth20_token.srf",
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");
623 }
624 catch (const std::exception& e)
625 {
626 // If refresh fails restart the whole auth flow
627 LOG_ERROR(e.what());
628 LOG_INFO("Failed to refresh token, starting Microsoft authentication process...");
629 response = MSAAuthDeviceFlow();
630 }
631 }
632
633 cached["msa"] = {
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 }
637 };
638 return response["access_token"].get_string();
639 }
640
641 std::string GetXBLToken(const std::string& msa_token)
642 {
643 const Json::Value request_data = {
644 { "Properties", {
645 {"AuthMethod", "RPS"},
646 {"SiteName", "user.auth.xboxlive.com"},
647 {"RpsTicket", "d=" + msa_token}
648 }
649 },
650 { "RelyingParty", "http://auth.xboxlive.com"},
651 { "TokenType", "JWT"}
652 };
653
654 return CheckJsonResponse(
655 POSTRequest("user.auth.xboxlive.com", "/user/authenticate", "application/json", "application/json", "", request_data.Dump()),
656 "XBL authentication",
657 { "Token" }
658 )["Token"].get_string();
659 }
660
661 std::pair<std::string, std::string> GetXSTSToken(const std::string& xbl_token)
662 {
663 const Json::Value request_data = {
664 { "Properties", {
665 {"SandboxId", "RETAIL"},
666 {"UserTokens", { xbl_token } }
667 }},
668 { "RelyingParty", "rp://api.minecraftservices.com/"},
669 { "TokenType", "JWT"}
670 };
671
672 const Json::Value response = CheckJsonResponse(
673 POSTRequest("xsts.auth.xboxlive.com", "/xsts/authorize", "application/json", "application/json", "", request_data.Dump()),
674 "XSTS authentication",
675 { "Token", "DisplayClaims" }
676 );
677
678 const Json::Value& display_claims = response["DisplayClaims"];
679 if (
680 !display_claims.contains("xui") ||
681 !display_claims["xui"].is_array() ||
682 display_claims["xui"].size() < 1 ||
683 !display_claims["xui"][0].contains("uhs")
684 )
685 {
686 throw std::runtime_error("Error trying to get XSTS token, no DisplayClaims/xui/0/uhs in authentication response");
687 }
688
689 return { response["Token"].get_string(), display_claims["xui"][0]["uhs"].get_string() };
690 }
691
692 std::string GetMCTokenFromMicrosoft(Json::Value& cached)
693 {
694 LOG_INFO("Trying to get Microsoft access token...");
695 const std::string msa_token = GetMSAToken(cached);
696
697 LOG_INFO("Trying to get XBL token...");
698 const std::string xbl_token = GetXBLToken(msa_token);
699 LOG_INFO("XBL token obtained!");
700
701 LOG_INFO("Trying to get XSTS token...");
702 const auto [xsts_token, user_hash] = GetXSTSToken(xbl_token);
703 LOG_INFO("XSTS token obtained!");
704
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();
707 // Set again below, only if we get a new token with an expiry date
708 cached.get_object().erase("mc_token");
709 cached.get_object().erase("expires_date");
710
711 const Json::Value request_data = {
712 { "identityToken", "XBL3.0 x=" + user_hash + ";" + xsts_token }
713 };
714 const Json::Value response = CheckJsonResponse(
715 POSTRequest("api.minecraftservices.com", "/authentication/login_with_xbox", "application/json", "application/json", "", request_data.Dump()),
716 "MC authentication",
717 { "access_token" }
718 );
719
720 if (response.contains("expires_in"))
721 {
722 cached["mc_token"] = response["access_token"].get_string();
723 cached["expires_date"] = response["expires_in"].get<long long int>() + now;
724 }
725 else
726 {
727 // If no expires_in assuming it is one-time, don't need to cache it
728 LOG_WARNING("No expires_in in authentication response of MC");
729 }
730 LOG_INFO("MC token obtained! Almost there...");
731
732 return response["access_token"].get_string();
733 }
734
735 std::pair<std::string, std::string> GetMCProfile(const std::string& mc_token)
736 {
737 const Json::Value response = CheckJsonResponse(
738 GETRequest("api.minecraftservices.com", "/minecraft/profile", "Bearer " + mc_token),
739 "MC profile retrieval",
740 { "id", "name" }
741 );
742
743 return { response["id"].get_string(), response["name"].get_string() };
744 }
745
746 int IsLeap(const int year)
747 {
748 if (year % 400 == 0) { return 1; }
749 if (year % 100 == 0) { return 0; }
750 if (year % 4 == 0) { return 1; }
751 return 0;
752 }
753
754 int DaysFrom0(const int year)
755 {
756 const long long y = year - 1;
757 return 365 * y + y / 400 - y / 100 + y / 4;
758 }
759
760 int DaysFrom1970(int32_t year)
761 {
762 return DaysFrom0(year) - DaysFrom0(1970);
763 }
764
765 int DaysFrom1Jan(const int year, const int month, const int day)
766 {
767 static constexpr std::array<std::array<int, 12>, 2> days =
768 { {
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}
771 } };
772
773 return days[IsLeap(year)][month - 1] + day - 1;
774 }
775
776 long long int TimestampMilliFromISO8601(const std::string& s)
777 {
778 std::tm t{};
779 std::stringstream iso8601(s);
780 iso8601 >> std::get_time(&t, "%Y-%m-%dT%H:%M:%S");
781
782 if (iso8601.fail() || t.tm_mon < 0 || t.tm_mon > 11)
783 {
784 LOG_ERROR("Error trying to parse ISO 8601 string: " << s);
785 return 0;
786 }
787
788 std::string millis;
789 if (iso8601.peek() == '.')
790 {
791 iso8601.get();
792 while (millis.size() < 3 && std::isdigit(iso8601.peek()))
793 {
794 millis += static_cast<char>(iso8601.get());
795 }
796 }
797 millis.resize(3, '0');
798
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);
803
804 const long long int sec = 3600LL * 24 * days_since_epoch + 3600 * t.tm_hour + 60 * t.tm_min + t.tm_sec;
805
806 return sec * 1000 + std::stoll(millis);
807 }
808 }
809#endif
810}
struct rsa_st RSA
#define LOG_ERROR(osstream)
Definition Logger.hpp:45
#define LOG_ALWAYS(osstream)
Definition Logger.hpp:47
#define LOG_WARNING(osstream)
Definition Logger.hpp:44
#define LOG_INFO(osstream)
Definition Logger.hpp:43
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.
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::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 &timestamp)
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>
Definition Json.hpp:186
Main class, basically a JsonVariant with extra utility functions it doesn't inherit JsonVariant direc...
Definition Json.hpp:45
bool is_string() const
Definition Json.cpp:144
bool is_number() const
Definition Json.cpp:170
std::string Dump(const int indent=-1, const char indent_char=' ') const
public dump interface
Definition Json.cpp:287
bool is_object() const
Definition Json.cpp:149
bool contains(const std::string &s) const
Definition Json.cpp:232
std::string & get_string()
Definition Json.cpp:119
WebRequestResponse GETRequest(const std::string &host, const std::string &endpoint, const std::string &authorization="")
Send a GET request with ssl stuff.
Definition Https.cpp:305
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.
Definition Https.cpp:326
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.
Definition Https.cpp:279
std::array< unsigned char, 16 > UUID
ProtocolCraft::Json::Value response
Definition Https.hpp:13