Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions src/Account.cc
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ shared_ptr<BBLicense> BBLicense::from_json(const phosg::JSON& json) {
auto ret = make_shared<BBLicense>();
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");
}
Expand All @@ -114,11 +115,22 @@ shared_ptr<BBLicense> 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)
Expand Down Expand Up @@ -179,6 +191,7 @@ Account::Account(const phosg::JSON& json)
auto lic = make_shared<BBLicense>();
lic->username = bb_username;
lic->password = bb_password;
lic->hardware_id = "";
this->bb_licenses.emplace(lic->username, lic);
}
} else {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -729,7 +742,8 @@ shared_ptr<Login> AccountIndex::from_xb_credentials(
}
}

shared_ptr<Login> AccountIndex::from_bb_credentials_locked(const string& username, const string* password) {
shared_ptr<Login> 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>();
login->account = this->by_bb_username.at(username);
login->bb_license = login->account->bb_licenses.at(username);
Expand All @@ -739,24 +753,33 @@ shared_ptr<Login> 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<Login> 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) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be cleaner to not have bool bind_hardware_id, and if the hardware_id pointer is null, it means the hardware ID shouldn't be checked or bound. Then the callsite in on_93_BB would just pass null here if s->bind_hardware_ids_on_login is false.

@Repflez Repflez Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember exactly why I did it that way, I think it was to make it work with other usages like $bbsave (I think it was that one), I'll change it to be that way, though.

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&) {
}

Expand All @@ -768,6 +791,10 @@ shared_ptr<Login> AccountIndex::from_bb_credentials(
auto lic = make_shared<BBLicense>();
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);
Expand Down
13 changes: 10 additions & 3 deletions src/Account.hh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ struct XBLicense {
struct BBLicense {
std::string username;
std::string password;
std::string hardware_id;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::optional<uint64_t> would make more sense here. We could serialize it as an integer or null in JSON.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally tried to serialize it as a uint64_t, but either I screwed something up or it was a serialization issue that it ended up being as signed in the json file and when read back, it was doing it wrong, that's why I went for a string. I'll change it to the std::optional<uint64_t>, though.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The negative value in the JSON file should still work as long as its 64-bit representation matches the unsigned value that was originally serialized. If that feels a bit weird (as it probably would to me, to be honest), you could also change all the places where hardware_id is used to le_int64_t/be_int64_t/int64_t for consistency. The treatment of hardware ID as a 64-bit integer is nonstandard anyway with regard to vanilla PSO, so it's not important that the value is unsigned.


static std::shared_ptr<BBLicense> from_json(const phosg::JSON& json);
phosg::JSON json() const;
Expand Down Expand Up @@ -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") {}
Expand Down Expand Up @@ -223,7 +228,8 @@ public:
std::shared_ptr<Login> from_xb_credentials(
const std::string& gamertag, uint64_t user_id, uint64_t account_id, bool allow_create);
std::shared_ptr<Login> 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<Account> create_temporary_account_for_shared_account(
std::shared_ptr<const Account> src_a, const std::string& variation_data) const;
Expand All @@ -245,7 +251,7 @@ protected:
std::shared_ptr<Login> from_dc_nte_credentials_locked(
const std::string& serial_number, const std::string& access_key);
std::shared_ptr<Login> 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<Login> from_pc_credentials_locked(
uint32_t serial_number, const std::string& access_key, const std::string& character_name);
std::shared_ptr<Login> from_gc_credentials_locked(
Expand All @@ -254,5 +260,6 @@ protected:
const std::string* password,
const std::string& character_name);
std::shared_ptr<Login> from_xb_credentials_locked(uint64_t user_id);
std::shared_ptr<Login> from_bb_credentials_locked(const std::string& username, const std::string* password);
std::shared_ptr<Login> from_bb_credentials_locked(
const std::string& username, const std::string* password, const uint64_t* hardware_id, bool bind_hardware_id);
};
2 changes: 1 addition & 1 deletion src/ChatCommands.cc
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ static asio::awaitable<void> 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) {
Expand Down
10 changes: 7 additions & 3 deletions src/ReceiveCommands.cc
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ static asio::awaitable<void> on_04_U(shared_ptr<Client> 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) {
Expand All @@ -585,7 +585,7 @@ static asio::awaitable<void> on_04_U(shared_ptr<Client> 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;
}
Expand Down Expand Up @@ -1427,13 +1427,17 @@ static asio::awaitable<void> on_93_BB(shared_ptr<Client> 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);
Expand Down
1 change: 1 addition & 0 deletions src/ServerState.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/ServerState.hh
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
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<uint16_t, NUM_VERSIONS> compatibility_groups = {};
bool enable_chat_commands = true;
char chat_command_sentinel = '\0'; // 0 = default (@ on 11/2000; $ on all other versions)
Expand Down
6 changes: 6 additions & 0 deletions system/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading