diff --git a/src/Main.cc b/src/Main.cc index 5760ef2a..f5b54883 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -663,6 +663,19 @@ Action a_generate_pc_v2_registry( write_output_data(args, output_data.data(), output_data.size(), "reg"); }); +Action a_encrypt_challenge_time( + "encrypt-challenge-time", nullptr, +[](phosg::Arguments& args) { + uint16_t time = args.get(1); + uint32_t ret = encrypt_challenge_time(time); + phosg::fwrite_fmt(stderr, "{} => {:08X}\n", phosg::format_duration(time * 1000000), ret); + }); +Action a_decrypt_challenge_time( + "decrypt-challenge-time", nullptr, +[](phosg::Arguments& args) { + uint32_t time = args.get(1, phosg::Arguments::IntFormat::HEX); + uint16_t ret = decrypt_challenge_time(time); + phosg::fwrite_fmt(stderr, "{:08X} => {}\n", time, phosg::format_duration(ret * 1000000)); + }); + Action a_encrypt_challenge_data( "encrypt-challenge-data", nullptr, +[](phosg::Arguments& args) { string data = read_input_data(args); diff --git a/src/PSOEncryption.cc b/src/PSOEncryption.cc index d55fbdf8..a0ddc696 100644 --- a/src/PSOEncryption.cc +++ b/src/PSOEncryption.cc @@ -706,27 +706,28 @@ void PSOBBMultiKeyDetectorEncryption::encrypt(void* data, size_t size) { } void PSOBBMultiKeyDetectorEncryption::decrypt(void* data, size_t size) { - if (!this->active_crypt.get()) { - if (size != 8) { - 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->active_crypt = make_shared(*this->active_key, this->seed.data(), this->seed.size()); - string test_data(reinterpret_cast(data), size); - this->active_crypt->decrypt(test_data.data(), test_data.size()); - if (this->expected_first_data.count(test_data)) { - break; - } - this->active_key.reset(); - this->active_crypt.reset(); - } - if (!this->active_crypt.get()) { - throw runtime_error("none of the registered private keys are valid for this client"); - } + if (this->active_crypt.get()) { + this->active_crypt->decrypt(data, size); + return; } - this->active_crypt->decrypt(data, size); + + if (size != 8) { + 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->active_crypt = make_shared(*this->active_key, this->seed.data(), this->seed.size()); + string test_data(reinterpret_cast(data), size); + this->active_crypt->decrypt(test_data.data(), test_data.size()); + if (this->expected_first_data.count(test_data)) { + memcpy(data, test_data.data(), size); + return; + } + this->active_key.reset(); + this->active_crypt.reset(); + } + throw runtime_error("none of the registered private keys are valid for this client"); } PSOEncryption::Type PSOBBMultiKeyDetectorEncryption::type() const { diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 3f505328..d59f83f3 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -356,11 +356,11 @@ static asio::awaitable S_B_03(shared_ptr c, Channel::Mess resp.character_slot = c->bb_character_index; resp.connection_phase = c->bb_connection_phase; resp.client_code = c->bb_client_code; - resp.security_token = c->proxy_session->remote_bb_security_token; + resp.security_token = c->bb_security_token; resp.username.encode(c->username, c->language()); resp.password.encode(c->password, c->language()); resp.hardware_id = c->hardware_id; - resp.client_config = c->proxy_session->remote_client_config_data; + resp.client_config = c->bb_client_config; if (c->proxy_session->enable_remote_ip_crc_patch) { *reinterpret_cast(resp.client_config.data() + 0x10) = c->proxy_session->remote_ip_crc ^ (1309539928UL + 1248334810UL); @@ -373,27 +373,17 @@ static asio::awaitable S_B_03(shared_ptr c, Channel::Mess static asio::awaitable S_B_E6(shared_ptr c, Channel::Message& msg) { const auto& cmd = msg.check_size_t(0xFFFF); c->proxy_session->remote_guild_card_number = cmd.guild_card_number; - c->proxy_session->remote_bb_security_token = cmd.security_token; - c->proxy_session->remote_client_config_data = cmd.client_config; + c->bb_security_token = cmd.security_token; + c->bb_client_config = cmd.client_config; auto s = c->require_server_state(); auto& pc = s->proxy_persistent_configs[c->login->account->account_id]; pc.account_id = c->login->account->account_id; pc.remote_guild_card_number = c->proxy_session->remote_guild_card_number; - pc.remote_bb_security_token = c->proxy_session->remote_bb_security_token; - pc.remote_client_config_data = c->proxy_session->remote_client_config_data; pc.enable_remote_ip_crc_patch = c->proxy_session->enable_remote_ip_crc_patch; c->log.info_f("Updated persistent config for proxy session"); - if ((c->bb_connection_phase == 0) && c->proxy_session->received_reconnect) { - c->proxy_session->server_channel->send(0x00E0); // Request system file - } - - co_return HandlerResult::SUPPRESS; -} - -static asio::awaitable C_B_E0(shared_ptr, Channel::Message&) { - co_return HandlerResult::SUPPRESS; + co_return HandlerResult::FORWARD; } static asio::awaitable S_V123_04(shared_ptr c, Channel::Message& msg) { @@ -748,18 +738,18 @@ static asio::awaitable S_G_E4(shared_ptr c, Channel::Mess static asio::awaitable S_B_22(shared_ptr c, Channel::Message& msg) { // We use this command (which is sent before the init encryption command) to // detect a particular server behavior that we'll have to work around later. - // It looks like this command's existence is another anti-proxy measure, since + // It looks like this command's existence is an anti-proxy measure, since // this command is 0x34 bytes in total, and the logic that adds padding bytes // when the command size isn't a multiple of 8 is only active when encryption // is enabled. Presumably some simpler proxies would get this wrong. // Editor's note: There's an unsavory message in this command's data field, - // hence the hash here instead of a direct string comparison. I'd love to hear - // the story behind why they put that string there. + // hence the hash here instead of a direct string comparison. I'd love to + // hear the story behind why they put that string there. if ((msg.data.size() == 0x2C) && (phosg::fnv1a64(msg.data.data(), msg.data.size()) == 0x8AF8314316A27994)) { c->log.info_f("Enabling remote IP CRC patch"); c->proxy_session->enable_remote_ip_crc_patch = true; } - co_return HandlerResult::FORWARD; + co_return HandlerResult::SUPPRESS; } static asio::awaitable S_19_U_14(shared_ptr c, Channel::Message& msg) { @@ -2249,7 +2239,7 @@ static on_message_t handlers[0x100][NUM_VERSIONS][2] = { /* DE */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}}, /* DF */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}}, // CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_12_2000 C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C -/* E0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, C_B_E0}}, +/* E0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}}, /* E1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}}, /* E2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_B_E2, nullptr}}, /* E3 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}}, @@ -2317,7 +2307,8 @@ asio::awaitable on_proxy_command(shared_ptr c, bool from_server, u } } -asio::awaitable handle_proxy_server_commands(shared_ptr c, shared_ptr ses, shared_ptr channel) { +asio::awaitable handle_proxy_server_commands( + shared_ptr c, shared_ptr ses, shared_ptr channel) { std::string error_str; // server_channel can be changed by receiving a 19 command, hence the // exception handler is inside the loop here @@ -2326,6 +2317,9 @@ asio::awaitable handle_proxy_server_commands(shared_ptr c, shared_ try { msg = make_unique(co_await channel->recv()); if (c->proxy_session == ses) { + for (size_t z = 0; z < std::min(c->proxy_session->prev_server_command_bytes.size(), msg->data.size()); z++) { + c->proxy_session->prev_server_command_bytes[z] = msg->data[z]; + } asio::co_spawn(co_await asio::this_coro::executor, on_proxy_command(c, true, std::move(msg)), asio::detached); } } catch (const std::system_error& e) { diff --git a/src/ProxySession.cc b/src/ProxySession.cc index bf5b3d5d..74470fd5 100644 --- a/src/ProxySession.cc +++ b/src/ProxySession.cc @@ -10,8 +10,6 @@ ProxySession::ProxySession(shared_ptr server_channel, const PersistentC : server_channel(server_channel) { if (pc) { this->remote_guild_card_number = pc->remote_guild_card_number; - this->remote_bb_security_token = pc->remote_bb_security_token; - this->remote_client_config_data = pc->remote_client_config_data; this->enable_remote_ip_crc_patch = pc->enable_remote_ip_crc_patch; } else if (is_v4(this->server_channel->version)) { this->remote_guild_card_number = 0; diff --git a/src/ProxySession.hh b/src/ProxySession.hh index 83c8f0ec..22184d61 100644 --- a/src/ProxySession.hh +++ b/src/ProxySession.hh @@ -44,7 +44,6 @@ struct ProxySession { uint64_t server_ping_start_time = 0; int64_t remote_guild_card_number = -1; - uint32_t remote_bb_security_token = 0; parray remote_client_config_data; enum class DropMode { @@ -64,8 +63,6 @@ struct ProxySession { struct PersistentConfig { uint32_t account_id; uint32_t remote_guild_card_number; - uint32_t remote_bb_security_token; - parray remote_client_config_data; bool enable_remote_ip_crc_patch; std::unique_ptr expire_timer; }; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 482cf7bf..f4058896 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -454,7 +454,7 @@ asio::awaitable start_proxy_session(shared_ptr c, const string& ho send_proxy_destinations_menu(c); } } else { - // Get persistent config if client is BB + // Get persistent config if available ProxySession::PersistentConfig* pc = nullptr; if (use_persistent_config) { try { @@ -477,8 +477,8 @@ asio::awaitable start_proxy_session(shared_ptr c, const string& ho std::format("C-{} proxy remote server at {}", c->id, netloc_str), phosg::TerminalFormat::FG_YELLOW, phosg::TerminalFormat::FG_RED); - c->proxy_session = make_shared(channel, pc); + c->log.info_f("Server channel connected"); asio::co_spawn(*s->io_context, handle_proxy_server_commands(c, c->proxy_session, channel), asio::detached); } @@ -507,15 +507,6 @@ asio::awaitable end_proxy_session(shared_ptr c, const std::string& } catch (const out_of_range&) { } - bool is_in_game = c->proxy_session->is_in_game; - c->proxy_session->server_channel->disconnect(); - c->proxy_session.reset(); - - if (is_v4(c->version())) { - c->channel->disconnect(); - co_return; - } - // Delete all the other players for (size_t x = 0; x < c->proxy_session->lobby_players.size(); x++) { if (c->proxy_session->lobby_players[x].guild_card_number == 0) { @@ -527,6 +518,15 @@ asio::awaitable end_proxy_session(shared_ptr c, const std::string& c->channel->send(c->proxy_session->is_in_game ? 0x66 : 0x69, leaving_id, &cmd, sizeof(cmd)); } + bool is_in_game = c->proxy_session->is_in_game; + c->proxy_session->server_channel->disconnect(); + c->proxy_session.reset(); + + if (is_v4(c->version())) { + c->channel->disconnect(); + co_return; + } + if (is_in_game) { string msg = std::format("You cannot return\nto $C6{}$C7\nwhile in a game.\n\n{}", s->name, error_message); @@ -1480,24 +1480,34 @@ static asio::awaitable on_93_BB(shared_ptr c, Channel::Message& ms c->preferred_lobby_id = base_cmd.preferred_lobby_id; } - send_client_init_bb(c, 0); - - if (!c->bb_client_config.is_filled_with(0xFF)) { + if (base_cmd.guild_card_number == 0) { // There is a (bug? feature?) in the BB client such that it has to receive // a reconnect command during the data server phase, or else it won't know // where to connect to during character selection. It's not clear why they // didn't just make it use the initial connection address by default... + send_client_init_bb(c, 0); send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at("bb-data1")->port); + co_return; } else if (s->proxy_destination_bb.has_value()) { - // Start a proxy session if there's a destination configured Ignore the - // persistent config if this is the first data server connection, to - // prevent quick reconnects from incorrectly reusing the old session's - // state. + // Start a proxy session immediately if there's a destination set. Two + // things to watch out for: + // - Ignore the persistent config if this is the first data server + // connection, to prevent quick reconnects from incorrectly reusing the + // old session's state. + // - We don't send 00E6 (send_client_init_bb) in this case. This is because + // the login command is resent to the remote server, and we forward its + // response back to the client directly. const auto& [host, port] = *s->proxy_destination_bb; co_await start_proxy_session(c, host, port, c->bb_connection_phase != 0); + c->proxy_session->remote_client_config_data = c->bb_client_config; + co_return; - } else if (c->bb_connection_phase >= 0x04) { + } else { + send_client_init_bb(c, 0); + } + + if (c->bb_connection_phase >= 0x04) { // This means the client is done with the data server phase and is in the // game server phase; we should send the ship select menu or a lobby join // command. diff --git a/src/SendCommands.cc b/src/SendCommands.cc index f9f703ad..8fb14ff8 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -237,8 +237,6 @@ void send_server_init_bb(shared_ptr c, uint8_t flags) { auto cmd = prepare_server_init_contents_bb(server_key, client_key, flags); send_command_t(c, use_secondary_message ? 0x9B : 0x03, 0x00, cmd); - static const string primary_expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8); - static const string secondary_expected_first_data("\xDC\x00\xDB\x00\x00\x00\x00\x00", 8); c->bb_detector_crypt = make_shared( c->require_server_state()->bb_private_keys, bb_crypt_initial_client_commands, diff --git a/src/ShellCommands.cc b/src/ShellCommands.cc index 786702f0..9121a8ca 100644 --- a/src/ShellCommands.cc +++ b/src/ShellCommands.cc @@ -908,7 +908,11 @@ asio::awaitable> f_sc_ss(ShellCommand::Args& args) { auto c = args.get_client(); if (args.command[1] == 's') { - co_await on_command_with_header(c, data); + if (c->proxy_session) { + send_command_with_header(c->proxy_session->server_channel, data.data(), data.size()); + } else { + co_await on_command_with_header(c, data); + } } else { send_command_with_header(c->channel, data.data(), data.size()); }