Botcraft 1.21.10
Loading...
Searching...
No Matches
Authentifier.cpp
Go to the documentation of this file.
1#include <asio/ip/tcp.hpp>
2#include <asio/connect.hpp>
3#include <asio/streambuf.hpp>
4#include <asio/read_until.hpp>
5#include <asio/read.hpp>
6
7#ifdef USE_ENCRYPTION
8#include <asio/ssl.hpp>
9#include <openssl/sha.h>
10#endif
11
12#include <iomanip>
13#include <fstream>
14#include <sstream>
15#include <iostream>
16
19
21
22using namespace ProtocolCraft;
23
24namespace Botcraft
25{
26 const std::string Authentifier::cached_credentials_path = "botcraft_cached_credentials.json";
27 const std::string Authentifier::botcraft_app_id = "a0ad834d-e78a-4881-87f6-390aa0f4b283";
29 { "msa", {
30 { "access_token", nullptr },
31 { "expires_date", nullptr },
32 { "refresh_token", nullptr }
33 }},
34 { "name", nullptr },
35 { "id", nullptr },
36 { "mc_token", nullptr },
37 { "expires_date", nullptr }
38 };
39
40
42 {
44#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
45 key_timestamp = 0;
46 rnd = std::mt19937(static_cast<unsigned int>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count()));
47#endif
48 }
49
51 {
52#ifdef USE_ENCRYPTION
53#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
54 if (private_key != nullptr)
55 {
56 RSA_free(private_key);
57 private_key = nullptr;
58 }
59#endif
60#endif
61 }
62
63 bool Authentifier::AuthMicrosoft(const std::string& cache_key)
64 {
65#ifndef USE_ENCRYPTION
66 return false;
67#else
68
69 const Json::Value cached = GetCachedAccountOrDefault(cache_key);
70 if (!cached.contains("mc_token") || !cached["mc_token"].is_string() ||
71 !cached.contains("expires_date") || !cached["expires_date"].is_number() ||
72 !cached.contains("name") || !cached["name"].is_string() ||
73 !cached.contains("id") || !cached["id"].is_string())
74 {
75 LOG_WARNING("Missing or malformed cached credentials for Microsoft account, starting auth flow...");
76 }
77 else if (cached["expires_date"].get<long long int>() < std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count())
78 {
79 LOG_INFO("Cached Minecraft token for Microsoft account expired, starting auth flow...");
80 }
81 else
82 {
83 mc_access_token = cached["mc_token"].get_string();
84 player_display_name = cached["name"].get_string();
85 mc_player_uuid = cached["id"].get_string();
87 LOG_INFO("Cached Minecraft token for Microsoft account still valid.");
88
89#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
90 LOG_INFO("Getting player certificates...");
92 {
93 LOG_ERROR("Unable to get player certificates");
94 return false;
95 }
96 LOG_INFO("Player certificates obtained!");
97#endif
98 return true;
99 }
100
101 // This auth flow was initially inspired from https://github.com/maxsupermanhd/go-mc-ms-auth
102 LOG_INFO("Trying to get Microsoft access token...");
103 const std::string msa_token = GetMSAToken(cache_key);
104 if (msa_token.empty())
105 {
106 LOG_ERROR("Unable to get a microsoft auth token");
107 return false;
108 }
109
110 LOG_INFO("Trying to get XBL token...");
111 const std::string xbl_token = GetXBLToken(msa_token);
112 if (xbl_token.empty())
113 {
114 LOG_ERROR("Unable to get a XBL token");
115 return false;
116 }
117 LOG_INFO("XBL token obtained!");
118
119 LOG_INFO("Trying to get XSTS token...");
120 const auto [xsts_token, xsts_userhash] = GetXSTSToken(xbl_token);
121 if (xsts_token.empty())
122 {
123 LOG_ERROR("Unable to get a XSTS token");
124 return false;
125 }
126 LOG_INFO("XSTS token obtained!");
127
128 LOG_INFO("Trying to get MC token...");
129 if (!GetMCToken(xsts_token, xsts_userhash, cache_key))
130 {
131 LOG_ERROR("Unable to get a MC token");
132 return false;
133 }
134 LOG_INFO("MC token obtained! Almost there...");
135
136
137 // We assume you're using an account owning minecraft so
138 // we don't check (and also a bit because it's complicated)
139 // If you don't, Botcraft won't work on online mode.
140 // But you can buy yourself a copy of the game:
141 // https://www.minecraft.net/get-minecraft
142 LOG_INFO("Assuming the account owns Minecraft...");
143
144 LOG_INFO("Trying to get MC profile...");
145 if (!GetMCProfile(cache_key))
146 {
147 LOG_ERROR("Unable to get a MC profile");
148 return false;
149 }
151 LOG_INFO("MC profile obtained!");
152
153#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
154 LOG_INFO("Getting player certificates...");
156 {
157 LOG_ERROR("Unable to get player certificates");
158 return false;
159 }
160 LOG_INFO("Player certificates obtained!");
161#endif
162
163 LOG_INFO("Authentication completed!");
164
165 return true;
166#endif
167 }
168
169 bool Authentifier::AuthMCToken(const std::string& mc_token)
170 {
171#ifndef USE_ENCRYPTION
172 return false;
173#else
174 mc_access_token = mc_token;
175
176 LOG_INFO("Trying to get MC profile...");
177 if (!GetMCProfile(std::nullopt))
178 {
179 LOG_ERROR("Unable to get a MC profile");
180 return false;
181 }
183 LOG_INFO("MC profile obtained!");
184
185#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
186 LOG_INFO("Getting player certificates...");
188 {
189 LOG_ERROR("Unable to get player certificates");
190 return false;
191 }
192 LOG_INFO("Player certificates obtained!");
193#endif
194
195 LOG_INFO("Authentication completed!");
196
197 return true;
198#endif
199 }
200
201 bool Authentifier::JoinServer(const std::string& server_id, const std::vector<unsigned char>& shared_secret, const std::vector<unsigned char>& server_public_key) const
202 {
203#ifndef USE_ENCRYPTION
204 return false;
205#else
206 if (mc_player_uuid.empty())
207 {
208 LOG_ERROR("Trying to join a server before authentication");
209 return false;
210 }
211
212 SHA_CTX sha_context;
213 SHA1_Init(&sha_context);
214
215 SHA1_Update(&sha_context, server_id.c_str(), server_id.length());
216 SHA1_Update(&sha_context, shared_secret.data(), shared_secret.size());
217 SHA1_Update(&sha_context, server_public_key.data(), server_public_key.size());
218
219 std::vector<unsigned char> digest(SHA_DIGEST_LENGTH);
220 SHA1_Final(digest.data(), &sha_context);
221
222 // Compute minecraft special hexdigest (see https://wiki.vg/Protocol_Encryption#Client)
223
224 bool is_negative = digest[0] & (1 << 7);
225
226 // Take two complement
227 if (is_negative)
228 {
229 // Revert bits
230 for (int i = 0; i < digest.size(); ++i)
231 {
232 digest[i] = ~digest[i];
233 }
234
235 // add 1
236 int position = static_cast<int>(digest.size()) - 1;
237 while (digest[position] == 255 && position > 0)
238 {
239 digest[position] = 0;
240 position -= 1;
241 }
242 digest[position] += 1;
243 }
244
245 // Get hex representation
246 std::stringstream ss;
247 for (int i = 0; i < digest.size(); ++i)
248 {
249 ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(digest[i] & 0xFF);
250 }
251
252 std::string server_hash = ss.str();
253 // Remove leading 0
254 const size_t start = server_hash.find_first_not_of('0');
255 if (start != std::string::npos)
256 {
257 server_hash = server_hash.substr(start);
258 }
259 else
260 {
261 server_hash = "";
262 }
263
264 if (is_negative)
265 {
266 server_hash = "-" + server_hash;
267 }
268
269 // Prepare the data to send to the server
270 const Json::Value data = {
271 { "accessToken", mc_access_token },
272 { "selectedProfile", mc_player_uuid },
273 { "serverId", server_hash}
274 };
275
276 const WebRequestResponse post_response = POSTRequest("sessionserver.mojang.com", "/session/minecraft/join",
277 "application/json; charset=utf-8", "*/*", "", data.Dump());
278
279 if (post_response.status_code != 204)
280 {
281 LOG_ERROR("Response returned with status code " << post_response.status_code
282 << " (" << post_response.status_message << ") during server join:\n"
283 << post_response.response.Dump(4));
284 return false;
285 }
286
287 return true;
288#endif
289 }
290
291 const std::string& Authentifier::GetPlayerDisplayName() const
292 {
293 return player_display_name;
294 }
295
296 const std::array<unsigned char, 16>& Authentifier::GetPlayerUUID() const
297 {
299 }
300
301#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
303 {
304 return private_key;
305 }
306
307 const std::string& Authentifier::GetPublicKey() const
308 {
309 return public_key;
310 }
311
312 const std::string& Authentifier::GetKeySignature() const
313 {
314 return key_signature;
315 }
316
317 const long long int Authentifier::GetKeyTimestamp() const
318 {
319 return key_timestamp;
320 }
321
322#if PROTOCOL_VERSION == 759 /* 1.19 */
323 std::vector<unsigned char> Authentifier::GetMessageSignature(const std::string& message, long long int& salt, long long int& timestamp)
324#elif PROTOCOL_VERSION == 760 /* 1.19.1/2 */
325 std::vector<unsigned char> Authentifier::GetMessageSignature(const std::string& message,
326 const std::vector<unsigned char>& previous_signature, const std::vector<LastSeenMessagesEntry>& last_seen,
327 long long int& salt, long long int& timestamp)
328#else
329 std::vector<unsigned char> Authentifier::GetMessageSignature(const std::string& message,
330 const int message_sent_index, const UUID& chat_session_uuid,
331 const std::vector<std::vector<unsigned char>>& last_seen,
332 long long int& salt, long long int& timestamp)
333#endif
334 {
335#ifndef USE_ENCRYPTION
336 LOG_ERROR("Trying to compute message signature while botcraft was compiled without USE_ENCRYPTION.");
337 return {};
338#else
339 if (mc_player_uuid.empty() || private_key == nullptr)
340 {
341 LOG_ERROR("Trying to compute message signature before authentication");
342 return {};
343 }
344
345 // Generate random salt and timestamp
346 salt = std::uniform_int_distribution<long long int>(std::numeric_limits<long long int>::min(), std::numeric_limits<long long int>::max())(rnd);
347 timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
348 std::array<unsigned char, 8> salt_bytes;
349 std::array<unsigned char, 8> timestamp_bytes;
350
351 for (int i = 0; i < 8; ++i)
352 {
353 salt_bytes[i] = static_cast<unsigned char>((salt >> (8 * (7 - i))) & 0xFF);
354 // Signature is computed with seconds not milliseconds
355 timestamp_bytes[i] = static_cast<unsigned char>(((timestamp / 1000) >> (8 * (7 - i))) & 0xFF);
356 }
357
358 std::array<unsigned char, SHA256_DIGEST_LENGTH> signature_hash;
359#if PROTOCOL_VERSION == 759 /* 1.19 */
360 // Signature is computed with a dumb json instead of the actual string
361 const std::string jsoned_message = "{\"text\":\"" + message + "\"}";
362
363 // Compute hash
364 SHA256_CTX sha256;
365 SHA256_Init(&sha256);
366 SHA256_Update(&sha256, salt_bytes.data(), salt_bytes.size());
367 SHA256_Update(&sha256, mc_player_uuid_bytes.data(), mc_player_uuid_bytes.size());
368 SHA256_Update(&sha256, timestamp_bytes.data(), timestamp_bytes.size());
369 SHA256_Update(&sha256, jsoned_message.data(), jsoned_message.size());
370#elif PROTOCOL_VERSION == 760 /* 1.19.1/2 */
371 const unsigned char const_byte_70 = 70;
372
373 // Body hash
374 std::array<unsigned char, SHA256_DIGEST_LENGTH> body_hash;
375 SHA256_CTX body_sha256;
376 SHA256_Init(&body_sha256);
377 SHA256_Update(&body_sha256, salt_bytes.data(), salt_bytes.size());
378 SHA256_Update(&body_sha256, timestamp_bytes.data(), timestamp_bytes.size());
379 SHA256_Update(&body_sha256, message.data(), message.size());
380 SHA256_Update(&body_sha256, &const_byte_70, 1);
381 // All previously seen messages
382 for (int i = 0; i < last_seen.size(); ++i)
383 {
384 SHA256_Update(&body_sha256, &const_byte_70, 1);
385 SHA256_Update(&body_sha256, last_seen[i].GetProfileId().data(), last_seen[i].GetProfileId().size());
386 SHA256_Update(&body_sha256, last_seen[i].GetLastSignature().data(), last_seen[i].GetLastSignature().size());
387 }
388 SHA256_Final(body_hash.data(), &body_sha256);
389
390
391 // Signature hash
392 SHA256_CTX sha256;
393 SHA256_Init(&sha256);
394 if (!previous_signature.empty())
395 {
396 SHA256_Update(&sha256, previous_signature.data(), previous_signature.size());
397 }
398 SHA256_Update(&sha256, mc_player_uuid_bytes.data(), mc_player_uuid_bytes.size());
399 SHA256_Update(&sha256, body_hash.data(), body_hash.size());
400#else
401 std::array<unsigned char, 4> bytes_1_big_endian;
402 std::array<unsigned char, 4> message_sent_index_bytes;
403 std::array<unsigned char, 4> message_size_bytes;
404 std::array<unsigned char, 4> last_seen_size_bytes;
405
406 for (int i = 0; i < 4; ++i)
407 {
408 bytes_1_big_endian[i] = static_cast<unsigned char>((1 >> (8 * (3 - i))) & 0xFF);
409 message_sent_index_bytes[i] = static_cast<unsigned char>((message_sent_index >> (8 * (3 - i))) & 0xFF);
410 message_size_bytes[i] = static_cast<unsigned char>((static_cast<int>(message.size()) >> (8 * (3 - i))) & 0xFF);
411 last_seen_size_bytes[i] = static_cast<unsigned char>((static_cast<int>(last_seen.size()) >> (8 * (3 - i))) & 0xFF);
412 }
413
414 // Compute hash
415 SHA256_CTX sha256;
416 SHA256_Init(&sha256);
417 SHA256_Init(&sha256);
418 // Big endian (int)1
419 SHA256_Update(&sha256, bytes_1_big_endian.data(), bytes_1_big_endian.size());
420 // signed message link
421 SHA256_Update(&sha256, mc_player_uuid_bytes.data(), mc_player_uuid_bytes.size());
422 SHA256_Update(&sha256, chat_session_uuid.data(), chat_session_uuid.size());
423 SHA256_Update(&sha256, message_sent_index_bytes.data(), message_sent_index_bytes.size());
424 // signed message body
425 SHA256_Update(&sha256, salt_bytes.data(), salt_bytes.size());
426 SHA256_Update(&sha256, timestamp_bytes.data(), timestamp_bytes.size());
427 SHA256_Update(&sha256, message_size_bytes.data(), message_size_bytes.size());
428 SHA256_Update(&sha256, message.data(), message.size());
429 SHA256_Update(&sha256, last_seen_size_bytes.data(), last_seen_size_bytes.size());
430 for (size_t i = 0; i < last_seen.size(); ++i)
431 {
432 SHA256_Update(&sha256, last_seen[i].data(), last_seen[i].size());
433 }
434#endif
435 SHA256_Final(signature_hash.data(), &sha256);
436
437 // Compute signature
438 const int private_key_size = RSA_size(private_key);
439 std::vector<unsigned char> signature(private_key_size);
440 unsigned int signature_size;
441 RSA_sign(NID_sha256, signature_hash.data(), static_cast<unsigned int>(signature_hash.size()), signature.data(), &signature_size, private_key);
442 signature.resize(signature_size);
443
444 return signature;
445#endif
446 }
447#endif
448
450 {
451 for (int i = 0; i < 32; i += 2)
452 {
453 const std::string byte_str = mc_player_uuid.substr(i, 2);
454 mc_player_uuid_bytes[i / 2] = static_cast<unsigned char>(std::strtol(byte_str.c_str(), nullptr, 16));
455 }
456 }
457
458#ifdef USE_ENCRYPTION
460 {
461 std::ifstream cache_file(cached_credentials_path);
462 if (!cache_file.good())
463 {
464 return {};
465 }
466 Json::Value cached_content;
467 cache_file >> cached_content;
468 cache_file.close();
469
470 return cached_content;
471 }
472
473 Json::Value Authentifier::GetCachedAccountOrDefault(const std::optional<std::string>& cache_key) const
474 {
475 if (!cache_key.has_value())
476 {
478 }
479
480 const Json::Value profiles = GetAllCachedAccounts();
481
482 if (profiles.size() > 0 &&
483 profiles.contains(cache_key.value()) &&
484 profiles[cache_key.value()].is_object())
485 {
486 return profiles[cache_key.value()];
487 }
488
490 }
491
492 void Authentifier::WriteCacheFile(const Json::Value& profiles) const
493 {
494 std::ofstream cached_ofile(cached_credentials_path);
495 if (!cached_ofile.is_open())
496 {
497 return;
498 }
499 cached_ofile << profiles.Dump(4) << std::endl;
500 cached_ofile.close();
501 }
502
503 std::string Authentifier::GetMSAToken(const std::optional<std::string>& cache_key) const
504 {
505 // Retrieve cached microsoft credentials
506 Json::Value cached = GetCachedAccountOrDefault(cache_key);
507
508 auto save_cache = [&] {
509 if (!cache_key.has_value()) { return; }
511 profiles[cache_key.value()] = cached;
512 WriteCacheFile(profiles);
513 };
514
515 // In case there is something wrong in the cached data
516 if (!cached.contains("msa") || !cached["msa"].is_object() ||
517 !cached["msa"].contains("refresh_token") || !cached["msa"]["refresh_token"].is_string() ||
518 !cached["msa"].contains("access_token") || !cached["msa"]["access_token"].is_string() ||
519 !cached["msa"].contains("expires_date") || !cached["msa"]["expires_date"].is_number())
520 {
521 LOG_ERROR("Error trying to get cached Microsoft credentials");
522 cached.get_object().erase("msa");
523 save_cache();
524 LOG_INFO("Starting authentication process...");
525 return MSAAuthDeviceFlow(cache_key);
526 }
527
528 if (cached["msa"]["expires_date"].get<long long int>() < std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count())
529 {
530 LOG_INFO("Refreshing Microsoft token...");
531 const std::string refresh_data =
532 "client_id=" + botcraft_app_id +
533 "&refresh_token=" + cached["msa"]["refresh_token"].get_string() +
534 "&grant_type=refresh_token" +
535 "&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient";
536
537 const WebRequestResponse post_response = POSTRequest("login.live.com", "/oauth20_token.srf",
538 "application/x-www-form-urlencoded", "*/*", "", refresh_data);
539
540 // If refresh fails restart the whole auth flow
541 if (post_response.status_code != 200)
542 {
543 LOG_ERROR("Response returned with status code " << post_response.status_code
544 << " (" << post_response.status_message << ") during Microsoft token refresh:\n"
545 << post_response.response.Dump(4));
546 cached.get_object().erase("msa");
547 save_cache();
548 LOG_INFO("Failed to refresh token, starting Microsoft authentication process...");
549 return MSAAuthDeviceFlow(cache_key);
550 }
551
552 const Json::Value& response = post_response.response;
553
554 if (!response.contains("expires_in"))
555 {
556 LOG_ERROR("Error trying to refresh Microsoft token, no expires_in in response");
557 cached.get_object().erase("msa");
558 save_cache();
559 LOG_INFO("Failed to refresh token, starting Microsoft authentication process...");
560 return MSAAuthDeviceFlow(cache_key);
561 }
562
563 if (!response.contains("refresh_token"))
564 {
565 LOG_ERROR("Error trying to refresh microsoft token, no refresh_token in response");
566 cached.get_object().erase("msa");
567 save_cache();
568 LOG_INFO("Failed to refresh token, starting Microsoft authentication process...");
569 return MSAAuthDeviceFlow(cache_key);
570 }
571
572 if (!response.contains("access_token"))
573 {
574 LOG_ERROR("Error trying to refresh microsoft token, no access_token in response");
575 cached.get_object().erase("msa");
576 save_cache();
577 LOG_INFO("Failed to refresh token, starting Microsoft authentication process...");
578 return MSAAuthDeviceFlow(cache_key);
579 }
580
581 cached["msa"] = {
582 { "access_token", response["access_token"].get_string() },
583 { "refresh_token", response["refresh_token"].get_string() },
584 { "expires_date", response["expires_in"].get<long long int>() + std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count() },
585 };
586
587 save_cache();
588
589 LOG_INFO("Cached Microsoft token refreshed");
590
591 return response["access_token"].get_string();
592 }
593
594 LOG_INFO("Cached Microsoft token still valid");
595
596 return cached["msa"]["access_token"].get_string();
597 }
598
599 std::string Authentifier::MSAAuthDeviceFlow(const std::optional<std::string>& cache_key) const
600 {
601 const std::string auth_data = "client_id=" + botcraft_app_id + "&scope=XboxLive.signin%20offline_access";
602
603 const WebRequestResponse post_response = POSTRequest("login.microsoftonline.com", "/consumers/oauth2/v2.0/devicecode",
604 "application/x-www-form-urlencoded", "*/*", "", auth_data);
605
606 if (post_response.status_code != 200)
607 {
608 LOG_ERROR("Response returned with status code " << post_response.status_code << " ("
609 << post_response.status_message << ") during microsoft authentification:\n"
610 << post_response.response.Dump(4));
611 return "";
612 }
613
614 const Json::Value& auth_response = post_response.response;
615
616 if (!auth_response.contains("interval"))
617 {
618 LOG_ERROR("Error trying to get microsoft token, no interval in authentication response");
619 return "";
620 }
621
622 if (!auth_response.contains("message"))
623 {
624 LOG_ERROR("Error trying to get microsoft token, no message in authentication response");
625 return "";
626 }
627
628 if (!auth_response.contains("device_code"))
629 {
630 LOG_ERROR("Error trying to get microsoft token, no device_code in authentication response");
631 return "";
632 }
633
634 // Display the instructions the user has to follow to authenticate in the console
635 LOG_ALWAYS(auth_response["message"].get_string());
636
637 const long long int pool_interval = auth_response["interval"].get_number<long long int>();
638 while (true)
639 {
640 std::this_thread::sleep_for(std::chrono::seconds(pool_interval + 1));
641
642 const std::string check_auth_status_data =
643 "client_id=" + botcraft_app_id +
644 "&scope=XboxLive.signin%20offline_access" +
645 "&grant_type=urn:ietf:params:oauth:grant-type:device_code" +
646 "&device_code=" + auth_response["device_code"].get_string();
647
648 const WebRequestResponse post_response = POSTRequest("login.microsoftonline.com", "/consumers/oauth2/v2.0/token",
649 "application/x-www-form-urlencoded", "*/*", "", check_auth_status_data);
650
651 const Json::Value& status_response = post_response.response;
652
653 if (post_response.status_code == 400)
654 {
655 if (!status_response.contains("error"))
656 {
657 LOG_ERROR("Unknown error happened during microsoft device authentication process");
658 return "";
659 }
660
661 const std::string error = status_response["error"].get_string();
662
663 if (error == "authorization_pending")
664 {
665 continue;
666 }
667 else if (error == "authorization_declined")
668 {
669 LOG_ERROR("User declined authorization during microsoft device authentication check");
670 return "";
671 }
672 else if (error == "expired_token")
673 {
674 LOG_ERROR("User took too long to perform device authentication, aborting");
675 return "";
676 }
677 else if (error == "invalid_grant")
678 {
679 if (!status_response.contains("error_description"))
680 {
681 LOG_ERROR("While waiting for microsoft device authentication, token got invalidated (no further information)");
682 }
683 else
684 {
685 LOG_ERROR("While waiting for microsoft device authentication, token got invalidated: " << status_response["error_description"].get_string());
686 }
687
688 return "";
689 }
690 }
691 else if (post_response.status_code == 200)
692 {
693 if (!status_response.contains("expires_in"))
694 {
695 LOG_ERROR("Error trying to get microsoft token, no expires_in in device authentication status response");
696 return "";
697 }
698
699 if (!status_response.contains("refresh_token"))
700 {
701 LOG_ERROR("Error trying to get microsoft token, no refresh_token in device authentication status response");
702 return "";
703 }
704
705 if (!status_response.contains("access_token"))
706 {
707 LOG_ERROR("Error trying to get microsoft token, no access_token in device authentication status response");
708 return "";
709 }
710
711 if (cache_key.has_value())
712 {
714 profiles[cache_key.value()]["msa"] = {
715 { "access_token", status_response["access_token"].get_string() },
716 { "refresh_token", status_response["refresh_token"].get_string() },
717 { "expires_date", status_response["expires_in"].get<long long int>() + std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count() },
718 };
719 WriteCacheFile(profiles);
720 }
721
722 LOG_INFO("Newly obtained Microsoft token stored in cache");
723
724 return status_response["access_token"].get_string();
725 }
726 else
727 {
728 LOG_ERROR("Response returned with status code " << post_response.status_code << " (" << post_response.status_message << ") during microsoft device authentification check");
729 return "";
730 }
731 }
732 }
733
734 std::string Authentifier::GetXBLToken(const std::string& msa_token) const
735 {
736 Json::Value request_data = {
737 { "Properties", {
738 {"AuthMethod", "RPS"},
739 {"SiteName", "user.auth.xboxlive.com"},
740 {"RpsTicket", "d=" + msa_token}
741 }
742 },
743 { "RelyingParty", "http://auth.xboxlive.com"},
744 { "TokenType", "JWT"}
745 };
746
747 const WebRequestResponse post_response = POSTRequest("user.auth.xboxlive.com", "/user/authenticate",
748 "application/json", "application/json", "", request_data.Dump());
749
750 if (post_response.status_code != 200)
751 {
752 LOG_ERROR("Response returned with status code " << post_response.status_code
753 << " (" << post_response.status_message << ") during XBL authentication:\n"
754 << post_response.response.Dump(4));
755 return "";
756 }
757
758 const Json::Value& response = post_response.response;
759
760 if (!response.contains("Token"))
761 {
762 LOG_ERROR("Error trying to get XBL token, no Token in authentication response");
763 return "";
764 }
765
766 return response["Token"].get_string();
767 }
768
769 std::pair<std::string, std::string> Authentifier::GetXSTSToken(const std::string& xbl_token) const
770 {
771 Json::Value request_data = {
772 { "Properties", {
773 {"SandboxId", "RETAIL"},
774 {"UserTokens", { xbl_token } }
775 }
776 },
777 { "RelyingParty", "rp://api.minecraftservices.com/"},
778 { "TokenType", "JWT"}
779 };
780
781 const WebRequestResponse post_response = POSTRequest("xsts.auth.xboxlive.com", "/xsts/authorize",
782 "application/json", "application/json", "", request_data.Dump());
783
784 if (post_response.status_code != 200)
785 {
786 LOG_ERROR("Response returned with status code " << post_response.status_code
787 << " (" << post_response.status_message << ") during XSTS authentication:\n"
788 << post_response.response.Dump(4));
789 return { "", "" };
790 }
791
792 const Json::Value& response = post_response.response;
793
794 if (!response.contains("Token"))
795 {
796 LOG_ERROR("Error trying to get XSTS token, no Token in authentication response");
797 return { "", "" };
798 }
799
800 if (!response.contains("DisplayClaims") || !response["DisplayClaims"].contains("xui")
801 || !response["DisplayClaims"]["xui"].is_array() || response["DisplayClaims"]["xui"].size() < 1
802 || !response["DisplayClaims"]["xui"][0].contains("uhs"))
803 {
804 LOG_ERROR("Error trying to get XSTS token, no DisplayClaims/xui/0/uhs in authentication response");
805 return { "", "" };
806 }
807
808 return { response["Token"].get_string(), response["DisplayClaims"]["xui"][0]["uhs"].get_string() };
809 }
810
811 bool Authentifier::GetMCToken(const std::string& xsts_token, const std::string& user_hash, const std::optional<std::string>& cache_key)
812 {
813 Json::Value cached = GetCachedAccountOrDefault(cache_key);
814
815 auto save_cache = [&] {
816 if (!cache_key.has_value()) { return; }
818 profiles[cache_key.value()] = cached;
819 WriteCacheFile(profiles);
820 };
821
822 Json::Value request_data = {
823 { "identityToken", "XBL3.0 x=" + user_hash + ";" + xsts_token }
824 };
825
826 const WebRequestResponse post_response = POSTRequest("api.minecraftservices.com", "/authentication/login_with_xbox",
827 "application/json", "application/json", "", request_data.Dump());
828
829 if (post_response.status_code != 200)
830 {
831 LOG_ERROR("Response returned with status code " << post_response.status_code
832 << " (" << post_response.status_message << ") during MC authentication:\n"
833 << post_response.response.Dump(4));
834 cached.get_object().erase("mc_token");
835 cached.get_object().erase("expires_date");
836 save_cache();
837 return false;
838 }
839
840 const Json::Value& response = post_response.response;
841
842 if (!response.contains("access_token"))
843 {
844 LOG_ERROR("Error trying to get MC token, no access_token in authentication response");
845 cached.get_object().erase("mc_token");
846 cached.get_object().erase("expires_date");
847 save_cache();
848 return false;
849 }
850
851 if (!response.contains("expires_in"))
852 {
853 LOG_WARNING("No expires_in in authentication response of MC");
854 cached.get_object().erase("mc_token");
855 cached.get_object().erase("expires_date");
856 save_cache();
857 mc_access_token = response["access_token"].get_string();
858 // if no expires_in assuming it is one-time, don't need to cache it
859 return true;
860 }
861
862 mc_access_token = response["access_token"].get_string();
863
864 cached["mc_token"] = mc_access_token;
865 cached["expires_date"] = response["expires_in"].get<long long int>() + std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
866
867 save_cache();
868
869 return true;
870 }
871
872 bool Authentifier::GetMCProfile(const std::optional<std::string>& cache_key)
873 {
874 Json::Value cached = GetCachedAccountOrDefault(cache_key);
875
876 auto save_cache = [&] {
877 if (!cache_key.has_value()) { return; }
879 profiles[cache_key.value()] = cached;
880 WriteCacheFile(profiles);
881 };
882
883 const WebRequestResponse get_response = GETRequest("api.minecraftservices.com", "/minecraft/profile",
884 "Bearer " + mc_access_token);
885
886 if (get_response.status_code != 200)
887 {
888 LOG_ERROR("Response returned with status code " << get_response.status_code << " (" << get_response.status_message << ") during MC profile retrieval");
889 cached.get_object().erase("name");
890 cached.get_object().erase("id");
891 save_cache();
892 return false;
893 }
894
895 const Json::Value& response = get_response.response;
896
897 if (response.contains("errorMessage"))
898 {
899 LOG_ERROR("Error trying to get MC profile : " << response["errorMessage"].get_string());
900 cached.get_object().erase("name");
901 cached.get_object().erase("id");
902 save_cache();
903 return false;
904 }
905
906 if (!response.contains("id"))
907 {
908 LOG_ERROR("Error trying to get MC profile, no id in response");
909 cached.get_object().erase("name");
910 cached.get_object().erase("id");
911 save_cache();
912 return false;
913 }
914
915 if (!response.contains("name"))
916 {
917 LOG_ERROR("Error trying to get MC profile, no name in response");
918 cached.get_object().erase("name");
919 cached.get_object().erase("id");
920 save_cache();
921 return false;
922 }
923
924 mc_player_uuid = response["id"].get_string();
926 player_display_name = response["name"].get_string();
927
928 cached["id"] = mc_player_uuid;
929 cached["name"] = player_display_name;
930 save_cache();
931
932 return true;
933 }
934
935#if PROTOCOL_VERSION > 758 /* > 1.18.2 */
937 {
938 // Certificates are not cached cause they're sometimes made invalid before the expire date (not sure why or when)
939 // So instead we get new ones for each connection (vanilla like behaviour)
940
941 LOG_INFO("Starting player certificates acquisition process...");
942
943 const WebRequestResponse post_response = POSTRequest("api.minecraftservices.com", "/player/certificates",
944 "application/json", "application/json", "Bearer " + mc_access_token, "");
945
946 if (post_response.status_code != 200)
947 {
948 LOG_ERROR("Response returned with status code " << post_response.status_code
949 << " (" << post_response.status_message << ") during player certificates acquisition:\n"
950 << post_response.response.Dump(4));
951 return false;
952 }
953
954 const Json::Value& response = post_response.response;
955
956 if (!response.contains("keyPair"))
957 {
958 LOG_ERROR("Error trying to get player certificates, no keyPair in response");
959 return false;
960 }
961
962 if (!response["keyPair"].contains("privateKey"))
963 {
964 LOG_ERROR("Error trying to get player certificates, no privateKey in response");
965 return false;
966 }
967
968 if (!response["keyPair"].contains("publicKey"))
969 {
970 LOG_ERROR("Error trying to get player certificates, no publicKey in response");
971 return false;
972 }
973
974#if PROTOCOL_VERSION == 759 /* 1.19 */
975 if (!response.contains("publicKeySignature"))
976 {
977 LOG_ERROR("Error trying to get player certificates, no publicKeySignature in response");
978 return false;
979 }
980#else
981 if (!response.contains("publicKeySignatureV2"))
982 {
983 LOG_ERROR("Error trying to get player certificates, no publicKeySignatureV2 in response");
984 return false;
985 }
986#endif
987
988 // Extract signature key from PEM string
989 if (private_key != nullptr)
990 {
991 RSA_free(private_key);
992 }
993 private_key = nullptr;
994 BIO* keybio = BIO_new_mem_buf((void*)response["keyPair"]["privateKey"].get_string().c_str(), -1);
995 private_key = PEM_read_bio_RSAPrivateKey(keybio, &private_key, NULL, NULL);
996 BIO_free(keybio);
997
998 public_key = response["keyPair"]["publicKey"].get_string();
999#if PROTOCOL_VERSION == 759 /* 1.19 */
1000 key_signature = response["publicKeySignature"].get_string();
1001#else
1002 key_signature = response["publicKeySignatureV2"].get_string();
1003#endif
1004 // Convert expires date in ISO8601 to ms since UNIX epoch
1005 key_timestamp = Utilities::TimestampMilliFromISO8601(response["expiresAt"].get_string());
1006
1007 return true;
1008 }
1009#endif
1010
1011 const WebRequestResponse Authentifier::WebRequest(const std::string& host, const std::string& raw_request) const
1012 {
1013 asio::io_context io_context;
1014
1015 // Get a list of endpoints corresponding to the server name.
1016 asio::ip::tcp::resolver resolver(io_context);
1017 asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, "https");
1018
1019 asio::ssl::context ctx(asio::ssl::context::sslv23);
1020 ctx.set_default_verify_paths();
1021 ctx.set_options(asio::ssl::context::default_workarounds | asio::ssl::context::verify_none);
1022
1023 asio::ssl::stream<asio::ip::tcp::socket> socket(io_context, ctx);
1024 socket.set_verify_mode(asio::ssl::verify_none);
1025 socket.set_verify_callback([](bool, asio::ssl::verify_context&) {return true; });
1026 SSL_set_tlsext_host_name(socket.native_handle(), host.c_str());
1027 asio::connect(socket.lowest_layer(), endpoints);
1028 socket.handshake(socket.client);
1029 socket.lowest_layer().set_option(asio::ip::tcp::no_delay(true));
1030
1031 // Send the request
1032 asio::streambuf request;
1033 std::ostream request_stream(&request);
1034 request_stream << raw_request;
1035
1036 asio::write(socket, request);
1037
1038 WebRequestResponse web_response;
1039
1040 // Read the response status line. The response streambuf will automatically
1041 // grow to accommodate the entire line. The growth may be limited by passing
1042 // a maximum size to the streambuf constructor.
1043 asio::streambuf response;
1044 asio::read_until(socket, response, "\r\n");
1045
1046 // Check that response is OK.
1047 std::istream response_stream(&response);
1048 std::string http_version;
1049 response_stream >> http_version;
1050 response_stream >> web_response.status_code;
1051 std::getline(response_stream, web_response.status_message);
1052
1053 // Remove any \r in status message
1054 web_response.status_message.erase(std::remove(web_response.status_message.begin(), web_response.status_message.end(), '\r'),
1055 web_response.status_message.end());
1056
1057 if (!response_stream || http_version.substr(0, 5) != "HTTP/")
1058 {
1059 LOG_ERROR("Invalid response during web request");
1060 web_response.response = {};
1061 return web_response;
1062 }
1063
1064 // Empty response
1065 if (web_response.status_code == 204)
1066 {
1067 web_response.response = {};
1068 return web_response;
1069 }
1070
1071 // Read the response headers, which are terminated by a blank line.
1072 asio::read_until(socket, response, "\r\n\r\n");
1073
1074 // Process the response headers.
1075 std::string header;
1076 long long int data_length = -1;
1077 while (std::getline(response_stream, header) && header != "\r")
1078 {
1079 if (header.find("Content-Length: ") == 0)
1080 {
1081 data_length = std::stoll(header.substr(16));
1082 }
1083 }
1084
1085 // Write whatever content we already have to output.
1086 std::stringstream output_stringstream;
1087 if (response.size() > 0)
1088 {
1089 output_stringstream << &response;
1090 }
1091
1092 // Read until EOF, writing data to output as we go.
1093 asio::error_code error;
1094 while (asio::read(socket, response, asio::transfer_at_least(1), error))
1095 {
1096 output_stringstream << &response;
1097 }
1098 const std::string raw_response = output_stringstream.str();
1099
1100 if (error != asio::error::eof && raw_response.size() != data_length)
1101 {
1102 LOG_ERROR("Error trying to read web request response, Error:\n " << error);
1103 web_response.response = {};
1104 }
1105 else
1106 {
1107 web_response.response = Json::Parse(raw_response);
1108 }
1109
1110 return web_response;
1111 }
1112
1113 const WebRequestResponse Authentifier::POSTRequest(const std::string& host, const std::string& endpoint,
1114 const std::string& content_type, const std::string& accept,
1115 const std::string& authorization, const std::string& data) const
1116 {
1117 // Form the request. We specify the "Connection: close" header so that the
1118 // server will close the socket after transmitting the response. This will
1119 // allow us to treat all data up until the EOF as the content.
1120 std::string raw_request = "";
1121 raw_request += "POST " + endpoint + " HTTP/1.1 \r\n";
1122 raw_request += "Host: " + host + "\r\n";
1123 raw_request += "User-Agent: C/1.0\r\n";
1124 raw_request += "Content-Type: " + content_type + " \r\n";
1125 raw_request += "Accept: " + accept + "\r\n";
1126 if (!authorization.empty())
1127 {
1128 raw_request += "Authorization: " + authorization + "\r\n";
1129 }
1130 raw_request += "Content-Length: " + std::to_string(data.length()) + "\r\n";
1131 raw_request += "Connection: close\r\n\r\n";
1132 raw_request += data;
1133
1134 return WebRequest(host, raw_request);
1135 }
1136
1137 const WebRequestResponse Authentifier::GETRequest(const std::string& host, const std::string& endpoint, const std::string& authorization) const
1138 {
1139 // Form the request. We specify the "Connection: close" header so that the
1140 // server will close the socket after transmitting the response. This will
1141 // allow us to treat all data up until the EOF as the content.
1142 std::string raw_request = "";
1143 raw_request += "GET " + endpoint + " HTTP/1.1 \r\n";
1144 raw_request += "Host: " + host + "\r\n";
1145 if (!authorization.empty())
1146 {
1147 raw_request += "Authorization: " + authorization + "\r\n";
1148 }
1149 raw_request += "User-Agent: C/1.0\r\n";
1150 raw_request += "Connection: close\r\n\r\n";
1151
1152 return WebRequest(host, raw_request);
1153 }
1154#endif
1155}
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 GetPlayerCertificates()
Try to get player certificates using Minecraft token.
bool GetMCProfile(const std::optional< std::string > &cache_key)
Try to get Minecraft profile from Minecraft token.
bool JoinServer(const std::string &server_id, const std::vector< unsigned char > &shared_secret, const std::vector< unsigned char > &server_public_key) const
std::string GetXBLToken(const std::string &msa_token) const
Try to get XBox Live token from Microsoft token.
bool AuthMicrosoft(const std::string &cache_key)
Authentication using a Microsoft account, storing the credentials in the cache file.
const WebRequestResponse WebRequest(const std::string &host, const std::string &raw_request) const
Send a web request with ssl stuff.
ProtocolCraft::Json::Value GetAllCachedAccounts() const
Get the content of the whole cache file.
std::pair< std::string, std::string > GetXSTSToken(const std::string &xbl_token) const
Try to get XSTS token from XBL token.
std::string player_display_name
static const std::string cached_credentials_path
Path to cache the credentials.
static const std::string botcraft_app_id
Botcraft app ID for microsoft auth.
const std::string & GetPlayerDisplayName() const
long long int key_timestamp
void UpdateUUIDBytes()
Compute the UUID bytes from the string one.
static const ProtocolCraft::Json::Value defaultCachedCredentials
Default cached credentials JSON.
const 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) const
Send a POST request with ssl stuff.
bool GetMCToken(const std::string &xsts_token, const std::string &user_hash, const std::optional< std::string > &cache_key)
Try to get MC token from XSTS token and user hash.
const std::string & GetKeySignature() const
const long long int GetKeyTimestamp() const
const WebRequestResponse GETRequest(const std::string &host, const std::string &endpoint, const std::string &authorization="") const
Send a GET request with ssl stuff.
std::string GetMSAToken(const std::optional< std::string > &cache_key) const
Check if there is a saved credentials file and if the token is still valid.
const std::array< unsigned char, 16 > & GetPlayerUUID() const
RSA * GetPrivateKey() const
std::array< unsigned char, 16 > mc_player_uuid_bytes
std::string MSAAuthDeviceFlow(const std::optional< std::string > &cache_key) const
Try to authenticate with microsoft account using device flow.
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.
void WriteCacheFile(const ProtocolCraft::Json::Value &profiles) const
Save a profiles list to cache file.
ProtocolCraft::Json::Value GetCachedAccountOrDefault(const std::optional< std::string > &cache_key) const
Get the cached credentials for a key.
bool AuthMCToken(const std::string &mc_token)
Authentication using a minecraft token.
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
size_t size() const
Definition Json.cpp:237
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_array() const
Definition Json.cpp:154
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
long long int TimestampMilliFromISO8601(const std::string &s)
Value Parse(std::string_view::const_iterator iter, size_t length, bool no_except=false)
Parse a string_view from iter for at most length characters.
Definition Json.cpp:390
std::array< unsigned char, 16 > UUID
ProtocolCraft::Json::Value response