From d5c38c2bc578ae319c3025ed6c45640552a77e98 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 4 May 2022 14:57:02 -0700 Subject: [PATCH] automatically determine the correct BB private key for each client --- CMakeLists.txt | 2 ++ src/Client.hh | 4 +-- src/Main.cc | 22 +++++++----- src/PSOEncryption.cc | 73 ++++++++++++++++++++++++++++++++++++++ src/PSOEncryption.hh | 45 ++++++++++++++++++++++- src/ProxyCommands.cc | 12 ++++--- src/ProxyServer.cc | 58 +++++++++++++++++------------- src/SendCommands.cc | 9 ++--- src/Server.cc | 24 ++++++------- src/ServerState.cc | 2 -- src/ServerState.hh | 2 +- system/config.example.json | 4 --- 12 files changed, 194 insertions(+), 63 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b3db3d06..c45cf2bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,8 @@ endif() include_directories("/usr/local/include") link_directories("/usr/local/lib") +set(CMAKE_BUILD_TYPE Debug) + # Executable definitions diff --git a/src/Client.hh b/src/Client.hh index 03e6aff8..50140ce5 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -74,8 +74,8 @@ struct Client { uint16_t flags; // Encryption - std::unique_ptr crypt_in; - std::unique_ptr crypt_out; + std::shared_ptr crypt_in; + std::shared_ptr crypt_out; // Network struct sockaddr_storage local_addr; diff --git a/src/Main.cc b/src/Main.cc index 4d239bfd..40e6447a 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -239,18 +239,22 @@ void populate_state_from_config(shared_ptr s, s->allow_unregistered_users = true; } - { - string key_file_name = d.at("BlueBurstKeyFile")->as_string(); - string key_file_contents = load_file("system/blueburst/keys/" + key_file_name + ".nsk"); - if (key_file_contents.size() != sizeof(PSOBBEncryption::KeyFile)) { - log(WARNING, "Blue Burst key file is the wrong size (%zu bytes; should be %zu bytes)", - key_file_contents.size(), sizeof(PSOBBEncryption::KeyFile)); - log(WARNING, "Ignoring key file; Blue Burst clients will not be able to connect"); + for (const string& filename : list_directory("system/blueburst/keys")) { + if (!ends_with(filename, ".nsk")) { + continue; + } + string contents = load_file("system/blueburst/keys/" + filename); + if (contents.size() != sizeof(PSOBBEncryption::KeyFile)) { + log(WARNING, "Blue Burst key file %s is the wrong size (%zu bytes; should be %zu bytes)", + filename.c_str(), contents.size(), sizeof(PSOBBEncryption::KeyFile)); } else { - memcpy(&s->default_key_file, key_file_contents.data(), sizeof(PSOBBEncryption::KeyFile)); - log(INFO, "Loaded Blue Burst key file: %s", key_file_name.c_str()); + shared_ptr k(new PSOBBEncryption::KeyFile()); + memcpy(k.get(), contents.data(), sizeof(PSOBBEncryption::KeyFile)); + s->bb_private_keys.emplace_back(k); + log(INFO, "Loaded Blue Burst key file: %s", filename.c_str()); } } + log(INFO, "%zu Blue Burst key file(s) loaded", s->bb_private_keys.size()); try { bool run_shell = d.at("RunInteractiveShell")->as_bool(); diff --git a/src/PSOEncryption.cc b/src/PSOEncryption.cc index 3c325151..367ba0e1 100644 --- a/src/PSOEncryption.cc +++ b/src/PSOEncryption.cc @@ -281,6 +281,8 @@ PSOBBEncryption::PSOBBEncryption( const KeyFile& key, const void* original_seed, size_t seed_size) : stream(this->generate_stream(key, original_seed, seed_size)) { } +PSOBBEncryption::PSOBBEncryption() { } + vector PSOBBEncryption::generate_stream( @@ -506,3 +508,74 @@ vector PSOBBEncryption::generate_stream( return stream; } + + + +PSOBBMultiKeyClientEncryption::PSOBBMultiKeyClientEncryption( + const vector>& possible_keys, + const string& expected_first_data, + const void* seed, + size_t seed_size) + : possible_keys(possible_keys), + expected_first_data(expected_first_data), + seed(reinterpret_cast(seed), seed_size) { } + +void PSOBBMultiKeyClientEncryption::encrypt(void* data, size_t size, bool advance) { + if (this->stream.empty()) { + throw logic_error("PSOBB multi-key encryption requires client input first"); + } + this->PSOBBEncryption::encrypt(data, size, advance); +} + +void PSOBBMultiKeyClientEncryption::decrypt(void* data, size_t size, bool advance) { + if (this->stream.empty()) { + if (size != this->expected_first_data.size()) { + throw logic_error("initial decryption size does not match expected first data size"); + } + + for (const auto& key : this->possible_keys) { + this->active_key = key; + this->stream = PSOBBEncryption::generate_stream( + *this->active_key.get(), this->seed.data(), this->seed.size()); + string test_data(reinterpret_cast(data), size); + this->PSOBBEncryption::decrypt(test_data.data(), test_data.size()); + if (test_data == this->expected_first_data) { + break; + } + this->active_key.reset(); + this->stream.clear(); + } + if (!this->active_key.get()) { + throw runtime_error("none of the registered private keys are valid for this client"); + } + } + this->PSOBBEncryption::decrypt(data, size, advance); +} + +PSOBBMultiKeyServerEncryption::PSOBBMultiKeyServerEncryption( + shared_ptr client_crypt, + const void* seed, + size_t seed_size) + : client_crypt(client_crypt), + seed(reinterpret_cast(seed), seed_size) { } + +void PSOBBMultiKeyServerEncryption::encrypt(void* data, size_t size, bool advance) { + this->ensure_stream_ready(); + this->PSOBBEncryption::encrypt(data, size, advance); +} + +void PSOBBMultiKeyServerEncryption::decrypt(void* data, size_t size, bool advance) { + this->ensure_stream_ready(); + this->PSOBBEncryption::decrypt(data, size, advance); +} + +void PSOBBMultiKeyServerEncryption::ensure_stream_ready() { + if (this->stream.empty()) { + if (!this->client_crypt->active_key.get()) { + throw logic_error("server crypt cannot be initialized because client crypt is not ready"); + } + log(INFO, "[PSOBB/MK] Generating server stream"); + this->stream = PSOBBEncryption::generate_stream( + *this->client_crypt->active_key, this->seed.data(), this->seed.size()); + } +} diff --git a/src/PSOEncryption.hh b/src/PSOEncryption.hh index aca42994..d56041dc 100644 --- a/src/PSOEncryption.hh +++ b/src/PSOEncryption.hh @@ -79,8 +79,51 @@ public: virtual void skip(size_t size); protected: + PSOBBEncryption(); + static std::vector generate_stream( const KeyFile& key, const void* seed, size_t seed_size); - const std::vector stream; + std::vector stream; +}; + +// The following classes provide support for multiple PSOBB private keys, and +// the ability to automatically detect which key the client is using based on +// the first 8 bytes they send. + +class PSOBBMultiKeyClientEncryption : public PSOBBEncryption { +public: + PSOBBMultiKeyClientEncryption( + const std::vector>& possible_keys, + const std::string& expected_first_data, + const void* seed, + size_t seed_size); + + virtual void encrypt(void* data, size_t size, bool advance = true); + virtual void decrypt(void* data, size_t size, bool advance = true); + + friend class PSOBBMultiKeyServerEncryption; + +protected: + std::vector> possible_keys; + std::shared_ptr active_key; + std::string expected_first_data; + std::string seed; +}; + +class PSOBBMultiKeyServerEncryption : public PSOBBEncryption { +public: + PSOBBMultiKeyServerEncryption( + std::shared_ptr client_crypt, + const void* seed, + size_t seed_size); + + virtual void encrypt(void* data, size_t size, bool advance = true); + virtual void decrypt(void* data, size_t size, bool advance = true); + +protected: + void ensure_stream_ready(); + + std::shared_ptr client_crypt; + std::string seed; }; diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index f65339e8..43570824 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -223,10 +223,14 @@ static bool process_server_bb_03(shared_ptr s, // BB encryption is stateless after it's initialized, unlike previous // versions, so we can get away with only two instances instead of four here. - session.server_input_crypt.reset(new PSOBBEncryption( - s->default_key_file, cmd.server_key.data(), sizeof(cmd.server_key))); - session.server_output_crypt.reset(new PSOBBEncryption( - s->default_key_file, cmd.client_key.data(), sizeof(cmd.client_key))); + // This is convenient because the two encryptions are linked together due to + // our use of multiple private keys, unlike for the other versions. + static const string expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8); + shared_ptr client_encr(new PSOBBMultiKeyClientEncryption( + s->bb_private_keys, expected_first_data, cmd.client_key.data(), sizeof(cmd.client_key))); + session.server_input_crypt.reset(new PSOBBMultiKeyServerEncryption( + client_encr, cmd.server_key.data(), sizeof(cmd.server_key))); + session.server_output_crypt = client_encr; session.client_input_crypt = session.server_output_crypt; session.client_output_crypt = session.server_input_crypt; diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index b2a5d82b..72f888c6 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -575,33 +575,43 @@ void ProxyServer::LinkedSession::disconnect() { void ProxyServer::LinkedSession::on_client_input() { - for_each_received_command(this->client_bev.get(), this->version, this->client_input_crypt.get(), - [&](uint16_t command, uint32_t flag, string& data) { - print_received_command(command, flag, data.data(), data.size(), - this->version, this->client_name.c_str()); - process_proxy_command( - this->server->state, - *this, - false, // from_server - command, - flag, - data); - }); + try { + for_each_received_command(this->client_bev.get(), this->version, this->client_input_crypt.get(), + [&](uint16_t command, uint32_t flag, string& data) { + print_received_command(command, flag, data.data(), data.size(), + this->version, this->client_name.c_str()); + process_proxy_command( + this->server->state, + *this, + false, // from_server + command, + flag, + data); + }); + } catch (const exception& e) { + this->log(ERROR, "Failed to process command from client: %s", e.what()); + this->disconnect(); + } } void ProxyServer::LinkedSession::on_server_input() { - for_each_received_command(this->server_bev.get(), this->version, this->server_input_crypt.get(), - [&](uint16_t command, uint32_t flag, string& data) { - print_received_command(command, flag, data.data(), data.size(), - this->version, this->server_name.c_str(), TerminalFormat::FG_RED); - process_proxy_command( - this->server->state, - *this, - true, // from_server - command, - flag, - data); - }); + try { + for_each_received_command(this->server_bev.get(), this->version, this->server_input_crypt.get(), + [&](uint16_t command, uint32_t flag, string& data) { + print_received_command(command, flag, data.data(), data.size(), + this->version, this->server_name.c_str(), TerminalFormat::FG_RED); + process_proxy_command( + this->server->state, + *this, + true, // from_server + command, + flag, + data); + }); + } catch (const exception& e) { + this->log(ERROR, "Failed to process command from server: %s", e.what()); + this->disconnect(); + } } void ProxyServer::LinkedSession::send_to_end( diff --git a/src/SendCommands.cc b/src/SendCommands.cc index ceae046a..91bb54c0 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -207,10 +207,11 @@ void send_server_init_bb(shared_ptr s, shared_ptr c) { cmd.after_message = anti_copyright; send_command(c, 0x03, 0x00, cmd); - c->crypt_out.reset(new PSOBBEncryption(s->default_key_file, - cmd.server_key.data(), cmd.server_key.bytes())); - c->crypt_in.reset(new PSOBBEncryption(s->default_key_file, - cmd.client_key.data(), cmd.client_key.bytes())); + static const string expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8); + shared_ptr client_encr(new PSOBBMultiKeyClientEncryption( + s->bb_private_keys, expected_first_data, cmd.client_key.data(), sizeof(cmd.client_key))); + c->crypt_in = client_encr; + c->crypt_out.reset(new PSOBBMultiKeyServerEncryption(client_encr, cmd.server_key.data(), sizeof(cmd.server_key))); } void send_server_init_patch(shared_ptr c) { diff --git a/src/Server.cc b/src/Server.cc index c939a624..c099c8d8 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -38,9 +38,9 @@ void Server::disconnect_client(shared_ptr c) { int fd = bufferevent_getfd(bev); if (fd < 0) { - this->log(INFO, "[Server] Client on virtual connection %p disconnected", bev); + this->log(INFO, "Client on virtual connection %p disconnected", bev); } else { - this->log(INFO, "[Server] Client on fd %d disconnected", fd); + this->log(INFO, "Client on fd %d disconnected", fd); } // if the output buffer is not empty, move the client into the draining pool @@ -208,16 +208,16 @@ void Server::on_disconnecting_client_error(struct bufferevent* bev, } void Server::receive_and_process_commands(shared_ptr c) { - for_each_received_command(c->bev, c->version, c->crypt_in.get(), - [this, c](uint16_t command, uint16_t flag, const std::string& data) { - try { - process_command(this->state, c, command, flag, data); - } catch (const exception& e) { - this->log(INFO, "Error in client stream: %s", e.what()); - c->should_disconnect = true; - return; - } - }); + try { + for_each_received_command(c->bev, c->version, c->crypt_in.get(), + [this, c](uint16_t command, uint16_t flag, const std::string& data) { + process_command(this->state, c, command, flag, data); + }); + } catch (const exception& e) { + this->log(INFO, "Error in client stream: %s", e.what()); + c->should_disconnect = true; + return; + } } Server::Server( diff --git a/src/ServerState.cc b/src/ServerState.cc index 37981453..7444ac26 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -20,8 +20,6 @@ ServerState::ServerState() run_shell_behavior(RunShellBehavior::DEFAULT), next_lobby_id(1), pre_lobby_event(0), ep3_menu_song(-1) { - memset(&this->default_key_file, 0, sizeof(this->default_key_file)); - vector> ep3_only_lobbies; for (size_t x = 0; x < 20; x++) { diff --git a/src/ServerState.hh b/src/ServerState.hh index ce9b919e..37ef7d47 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -44,7 +44,7 @@ struct ServerState { bool ip_stack_debug; bool allow_unregistered_users; RunShellBehavior run_shell_behavior; - PSOBBEncryption::KeyFile default_key_file; + std::vector> bb_private_keys; std::shared_ptr quest_index; std::shared_ptr level_table; std::shared_ptr battle_params; diff --git a/system/config.example.json b/system/config.example.json index 94bed224..4f1eff6c 100755 --- a/system/config.example.json +++ b/system/config.example.json @@ -49,10 +49,6 @@ // users cannot be banned! "AllowUnregisteredUsers": false, - // If you want to use Blue Burst clients with a different private key, put a - // .nsk file in system/blueburst/keys and put its name here. - "BlueBurstKeyFile": "default", - // User to run the server as. If present, newserv will attempt to switch to // this user's permissions after loading its configuration and opening // listening sockets. The special value $SUDO_USER causes newserv to look up