From 0d600e16ba6c148af196eb357a18bec3174c3c07 Mon Sep 17 00:00:00 2001 From: Repflez <659133+Repflez@users.noreply.github.com> Date: Thu, 21 May 2026 17:20:22 -0700 Subject: [PATCH 1/2] Add BB hardware id space in the license --- src/Account.cc | 19 ++++++++++++++++--- src/Account.hh | 5 +++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Account.cc b/src/Account.cc index c237b6265..95f52f0f2 100644 --- a/src/Account.cc +++ b/src/Account.cc @@ -102,6 +102,7 @@ shared_ptr BBLicense::from_json(const phosg::JSON& json) { auto ret = make_shared(); ret->username = json.get_string("UserName"); ret->password = json.get_string("Password"); + ret->hardware_id = ""; if (ret->username.size() > 16) { throw runtime_error("username is too long"); } @@ -114,11 +115,22 @@ shared_ptr BBLicense::from_json(const phosg::JSON& json) { if (ret->password.empty()) { throw runtime_error("password is too short"); } + try { + ret->hardware_id = json.get_string("HardwareID"); + } catch (const out_of_range&) { + } + if (ret->hardware_id.size() > 16) { + throw runtime_error("hardware id is not valid (over 16 characters)"); + } return ret; } phosg::JSON BBLicense::json() const { - return phosg::JSON::dict({{"UserName", this->username}, {"Password", this->password}}); + return phosg::JSON::dict({ + {"UserName", this->username}, + {"Password", this->password}, + {"HardwareID", this->hardware_id} + }); } Account::Account(const phosg::JSON& json) @@ -179,6 +191,7 @@ Account::Account(const phosg::JSON& json) auto lic = make_shared(); lic->username = bb_username; lic->password = bb_password; + lic->hardware_id = ""; this->bb_licenses.emplace(lic->username, lic); } } else { @@ -388,8 +401,8 @@ string Account::str() const { it.second->gamertag, it.second->user_id, it.second->account_id); } for (const auto& it : this->bb_licenses) { - ret += std::format(" BB license: username={} password={}\n", - it.second->username, it.second->password); + ret += std::format(" BB license: username={} password={} hardware_id={}\n", + it.second->username, it.second->password, it.second->hardware_id); } phosg::strip_trailing_whitespace(ret); diff --git a/src/Account.hh b/src/Account.hh index 0a4cc3210..5e411afd5 100644 --- a/src/Account.hh +++ b/src/Account.hh @@ -50,6 +50,7 @@ struct XBLicense { struct BBLicense { std::string username; std::string password; + std::string hardware_id; static std::shared_ptr from_json(const phosg::JSON& json); phosg::JSON json() const; @@ -173,6 +174,10 @@ public: public: incorrect_access_key() : invalid_argument("incorrect access key") {} }; + class incorrect_hardware_id : public std::invalid_argument { + public: + incorrect_hardware_id() : invalid_argument("mismatched hardware id") {} + }; class missing_account : public std::invalid_argument { public: missing_account() : invalid_argument("missing account") {} From 5d3ef55b428f6ba0be291b5d68346e20c87b07d0 Mon Sep 17 00:00:00 2001 From: Repflez <659133+Repflez@users.noreply.github.com> Date: Thu, 21 May 2026 17:46:10 -0700 Subject: [PATCH 2/2] Add hwid checks and binding for BB --- src/Account.cc | 22 ++++++++++++++++++---- src/Account.hh | 8 +++++--- src/ChatCommands.cc | 2 +- src/ReceiveCommands.cc | 10 +++++++--- src/ServerState.cc | 1 + src/ServerState.hh | 1 + system/config.example.json | 6 ++++++ 7 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/Account.cc b/src/Account.cc index 95f52f0f2..045d8079d 100644 --- a/src/Account.cc +++ b/src/Account.cc @@ -742,7 +742,8 @@ shared_ptr AccountIndex::from_xb_credentials( } } -shared_ptr AccountIndex::from_bb_credentials_locked(const string& username, const string* password) { +shared_ptr AccountIndex::from_bb_credentials_locked( + const string& username, const string* password, const uint64_t* hardware_id, bool bind_hardware_id) { auto login = make_shared(); login->account = this->by_bb_username.at(username); login->bb_license = login->account->bb_licenses.at(username); @@ -752,24 +753,33 @@ shared_ptr AccountIndex::from_bb_credentials_locked(const string& usernam if (login->account->ban_end_time && (login->account->ban_end_time >= phosg::now())) { throw account_banned(); } + if (hardware_id) { + string hardware_id_str = format("{:016X}", *hardware_id); + if (!login->bb_license->hardware_id.empty() && login->bb_license->hardware_id != hardware_id_str) { + throw incorrect_hardware_id(); + } + if (login->bb_license->hardware_id.empty() && bind_hardware_id) { + login->bb_license->hardware_id = hardware_id_str; + } + } return login; } shared_ptr AccountIndex::from_bb_credentials( - const string& username, const string* password, bool allow_create) { + const string& username, const string* password, const uint64_t* hardware_id, bool allow_create, bool bind_hardware_id) { if (username.empty() || (password && password->empty())) { throw no_username(); } try { shared_lock g(this->lock); - return this->from_bb_credentials_locked(username, password); + return this->from_bb_credentials_locked(username, password, hardware_id, bind_hardware_id); } catch (const out_of_range&) { } unique_lock g(this->lock); try { - return this->from_bb_credentials_locked(username, password); + return this->from_bb_credentials_locked(username, password, hardware_id, bind_hardware_id); } catch (const out_of_range&) { } @@ -781,6 +791,10 @@ shared_ptr AccountIndex::from_bb_credentials( auto lic = make_shared(); lic->username = username; lic->password = *password; + lic->hardware_id = ""; + if (bind_hardware_id) { + lic->hardware_id = format("{:016X}", *hardware_id); + } login->account->bb_licenses.emplace(lic->username, lic); login->bb_license = lic; this->add_locked(login->account); diff --git a/src/Account.hh b/src/Account.hh index 5e411afd5..c03457ca1 100644 --- a/src/Account.hh +++ b/src/Account.hh @@ -228,7 +228,8 @@ public: std::shared_ptr from_xb_credentials( const std::string& gamertag, uint64_t user_id, uint64_t account_id, bool allow_create); std::shared_ptr from_bb_credentials( - const std::string& username, const std::string* password, bool allow_create); + const std::string& username, const std::string* password, const uint64_t* hardware_id, bool allow_create, + bool bind_hardware_id); std::shared_ptr create_temporary_account_for_shared_account( std::shared_ptr src_a, const std::string& variation_data) const; @@ -250,7 +251,7 @@ protected: std::shared_ptr from_dc_nte_credentials_locked( const std::string& serial_number, const std::string& access_key); std::shared_ptr from_dc_credentials_locked( - uint32_t serial_number, const std::string& access_key, const std::string& character_name); + uint32_t serial_number, const std::string& access_key, const std::string& character_name); std::shared_ptr from_pc_credentials_locked( uint32_t serial_number, const std::string& access_key, const std::string& character_name); std::shared_ptr from_gc_credentials_locked( @@ -259,5 +260,6 @@ protected: const std::string* password, const std::string& character_name); std::shared_ptr from_xb_credentials_locked(uint64_t user_id); - std::shared_ptr from_bb_credentials_locked(const std::string& username, const std::string* password); + std::shared_ptr from_bb_credentials_locked( + const std::string& username, const std::string* password, const uint64_t* hardware_id, bool bind_hardware_id); }; diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 116190edd..0ebcdda8e 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -466,7 +466,7 @@ static asio::awaitable server_command_bbchar_savechar(const Args& a, bool } try { - auto dest_login = s->account_index->from_bb_credentials(tokens[0], &tokens[1], false); + auto dest_login = s->account_index->from_bb_credentials(tokens[0], &tokens[1], nullptr, false, false); dest_account = dest_login->account; dest_bb_license = dest_login->bb_license; } catch (const exception& e) { diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 98e656cb1..6aec12e61 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -575,7 +575,7 @@ static asio::awaitable on_04_U(shared_ptr c, Channel::Message& msg auto s = c->require_server_state(); if (!c->username.empty() && !c->password.empty()) { try { - s->account_index->from_bb_credentials(c->username, &c->password, false); + s->account_index->from_bb_credentials(c->username, &c->password, nullptr, false, false); } catch (const AccountIndex::incorrect_password& e) { result_code = 0x03; } catch (const AccountIndex::missing_account& e) { @@ -585,7 +585,7 @@ static asio::awaitable on_04_U(shared_ptr c, Channel::Message& msg } } else if (!c->username.empty() && !s->allow_unregistered_users) { try { - s->account_index->from_bb_credentials(c->username, nullptr, false); + s->account_index->from_bb_credentials(c->username, nullptr, nullptr, false, false); } catch (const AccountIndex::missing_account& e) { result_code = 0x08; } @@ -1427,13 +1427,17 @@ static asio::awaitable on_93_BB(shared_ptr c, Channel::Message& ms auto s = c->require_server_state(); try { - c->set_login(s->account_index->from_bb_credentials(c->username, &c->password, s->allow_unregistered_users)); + c->set_login(s->account_index->from_bb_credentials( + c->username, &c->password, &c->hardware_id, s->allow_unregistered_users, s->bind_hardware_ids_on_login)); } catch (const AccountIndex::no_username& e) { c->log.info_f("Login failed (no username)"); send_client_init_bb(c, 0x08); } catch (const AccountIndex::incorrect_password& e) { c->log.info_f("Login failed (incorrect password)"); send_client_init_bb(c, 0x03); + } catch (const AccountIndex::incorrect_hardware_id& e) { + c->log.info_f("Login failed (mismatched hardware id)"); + send_client_init_bb(c, 0x02); } catch (const AccountIndex::missing_account& e) { c->log.info_f("Login failed (missing account)"); send_client_init_bb(c, 0x08); diff --git a/src/ServerState.cc b/src/ServerState.cc index c4d6b5cf0..725d64b26 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -866,6 +866,7 @@ void ServerState::load_config_early() { this->allow_unregistered_users = this->config_json->get_bool("AllowUnregisteredUsers", false); this->allow_pc_nte = this->config_json->get_bool("AllowPCNTE", false); this->allow_same_account_concurrent_logins = this->config_json->get_bool("AllowSameAccountConcurrentLogins", false); + this->bind_hardware_ids_on_login = this->config_json->get_bool("BindHardwareIDsOnLogin", false); this->allow_saving_accounts = this->config_json->get_bool("AllowSavingAccounts", true); this->use_temp_accounts_for_prototypes = this->config_json->get_bool("UseTemporaryAccountsForPrototypes", true); this->notify_server_for_max_level_achieved = this->config_json->get_bool("NotifyServerForMaxLevelAchieved", false); diff --git a/src/ServerState.hh b/src/ServerState.hh index e38e7484b..6549ad81b 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -128,6 +128,7 @@ struct ServerState : public std::enable_shared_from_this { bool allow_pc_nte = false; bool use_temp_accounts_for_prototypes = true; bool allow_same_account_concurrent_logins = true; + bool bind_hardware_ids_on_login = false; std::array compatibility_groups = {}; bool enable_chat_commands = true; char chat_command_sentinel = '\0'; // 0 = default (@ on 11/2000; $ on all other versions) diff --git a/system/config.example.json b/system/config.example.json index 171843105..3dd4aa8bd 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -231,6 +231,12 @@ // account are disconnected. "AllowSameAccountConcurrentLogins": true, + // Automatically binds a license to a Hardware ID during login if the license has no Hardware ID defined. + // This locks the license to a specific Hardware ID. Mismatching Hardware IDs are rejected during login. + // To unbind a license, replace the HardwareID value to an empty string ("") and reload accounts from the + // server shell. + "BindHardwareIDsOnLogin": false, + // Whether to enable chat commands for all players. If this is true, all players will be able to use chat commands as // normal; if this is false, only players with the ALWAYS_ENABLE_CHAT_COMMANDS account flag will be able to use chat // commands.