From a9cf98a24fe1d3a7443694e13ed564e2c60d9c64 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Fri, 23 Sep 2022 00:33:03 -0700 Subject: [PATCH] implement some parts of DC NTE (but not all of it) --- src/CatSession.cc | 8 ++-- src/Channel.cc | 15 ++++--- src/CommandFormats.hh | 68 ++++++++++++++++++++++++++++---- src/ProxyCommands.cc | 8 ++-- src/ReceiveCommands.cc | 88 ++++++++++++++++++++++++++++++++++++++---- src/ReplaySession.cc | 16 ++++---- src/SendCommands.cc | 14 ++++--- src/SendCommands.hh | 5 ++- src/Version.cc | 3 ++ 9 files changed, 178 insertions(+), 47 deletions(-) diff --git a/src/CatSession.cc b/src/CatSession.cc index f9cd9691..3869506c 100644 --- a/src/CatSession.cc +++ b/src/CatSession.cc @@ -77,8 +77,8 @@ void CatSession::on_channel_input( uint16_t command, uint32_t flag, std::string& data) { if (this->channel.version != GameVersion::BB) { if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) { - const auto& cmd = check_size_t(data, - offsetof(S_ServerInit_DC_PC_V3_02_17_91_9B, after_message), 0xFFFF); + const auto& cmd = check_size_t(data, + sizeof(S_ServerInitDefault_DC_PC_V3_02_17_91_9B), 0xFFFF); if ((this->channel.version == GameVersion::GC) || (this->channel.version == GameVersion::XB)) { this->channel.crypt_in.reset(new PSOV3Encryption(cmd.server_key)); @@ -97,8 +97,8 @@ void CatSession::on_channel_input( if (!this->bb_key_file) { throw runtime_error("BB encryption requires a key file"); } - const auto& cmd = check_size_t(data, - offsetof(S_ServerInit_DC_PC_V3_02_17_91_9B, after_message), 0xFFFF); + const auto& cmd = check_size_t(data, + sizeof(S_ServerInitDefault_BB_03_9B), 0xFFFF); this->channel.crypt_in.reset(new PSOBBEncryption(*this->bb_key_file, &cmd.server_key[0], sizeof(cmd.server_key))); this->channel.crypt_out.reset(new PSOBBEncryption(*this->bb_key_file, &cmd.client_key[0], sizeof(cmd.client_key))); this->log.info("Enabled BB encryption"); diff --git a/src/Channel.cc b/src/Channel.cc index 611223ec..bfb3a80c 100644 --- a/src/Channel.cc +++ b/src/Channel.cc @@ -204,16 +204,15 @@ Channel::Message Channel::recv(bool print_contents) { throw logic_error("enough bytes available, but could not remove them"); } - // Some versions of PSO DC can send commands whose sizes are not a multiple of - // 4, but the server is expected to always use a multiple of 4 bytes when - // decrypting (the extra cipher bytes are lost). To emulate this behavior, we - // have to round up the size for DC commands here. - if (version == GameVersion::DC) { - command_data.resize((command_data.size() + 3) & (~3)); - } - if (this->crypt_in.get()) { + // Some versions of PSO DC can send commands whose sizes are not a multiple + // of 4, but the server is expected to always use a multiple of 4 bytes when + // decrypting (the extra cipher bytes are lost). To emulate this behavior, + // we have to round up the size for DC commands here. + size_t orig_size = command_data.size(); + command_data.resize((orig_size + 3) & (~3), 0); this->crypt_in->decrypt(command_data.data(), command_data.size()); + command_data.resize(orig_size); } command_data.resize(command_logical_size - header_size); diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index df883781..ef89898b 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -312,13 +312,17 @@ struct SC_TextHeader_01_06_11_B0_EE { // (The above text is required on all versions that use this command, including // those versions that don't run on the DreamCast.) -struct S_ServerInit_DC_PC_V3_02_17_91_9B { +struct S_ServerInitDefault_DC_PC_V3_02_17_91_9B { ptext copyright; le_uint32_t server_key; // Key for data sent by server le_uint32_t client_key; // Key for data sent by client +}; + +template +struct S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B : S_ServerInitDefault_DC_PC_V3_02_17_91_9B { // This field is not part of SEGA's implementation; the client ignores it. // newserv sends a message here disavowing the preceding copyright notice. - ptext after_message; + ptext after_message; }; // 03 (C->S): Legacy login (non-BB) @@ -352,10 +356,13 @@ struct C_LegacyLogin_PC_V3_03 { // The copyright field in the below structure must contain the following text: // "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM." -struct S_ServerInit_BB_03_9B { +struct S_ServerInitDefault_BB_03_9B { ptext copyright; parray server_key; parray client_key; +}; + +struct S_ServerInitWithAfterMessage_BB_03_9B : S_ServerInitDefault_BB_03_9B { // As in 02, this field is not part of SEGA's implementation. ptext after_message; }; @@ -1144,6 +1151,19 @@ struct C_LobbySelection_84 { // 86: Invalid command // 87: Invalid command +// 88 (C->S): License check (DC NTE only) +// The server should respond with an 88 command. + +struct C_Login_DCNTE_88 { + ptext serial_number; + ptext access_key; +}; + +// 88 (S->C): License check result (DC NTE only) +// No arguemnts except header.flag. +// If header.flag is zero, client will respond with an 8A command. Otherwise, it +// will respond with an 8B command. + // 88 (S->C): Update lobby arrows // If this command is sent while a client is joining a lobby, the client may // ignore it. For this reason, the server should wait a few seconds after a @@ -1178,17 +1198,51 @@ struct S_ArrowUpdateEntry_88 { // header.flag = arrow color number (see above); no other arguments. // Server should send an 88 command to all players in the lobby. -// 8A (C->S): Request lobby/game name +// 8A (C->S): Connection information (DC NTE only) +// The server should respond with an 8A command. + +struct C_ConnectionInfo_DCNTE_8A { + ptext hardware_id; + le_uint32_t sub_version; // 0x20 + le_uint32_t unknown_a1; + ptext username; + ptext password; + ptext email_address; // From Sylverant documentation +}; + +// 8A (S->C): Connection information result (DC NTE only) +// header.flag is a success flag. If 0 is sent, the client shows an error +// message and disconnects. Otherwise, the client responds with an 8B command. + +// 8A (C->S): Request lobby/game name (except DC NTE) // No arguments -// 8A (S->C): Lobby/game name +// 8A (S->C): Lobby/game name (except DC NTE) // Contents is a string (char16_t on PC/BB, char on DC/V3) containing the lobby // or game name. The client generally only sends this immediately after joining // a game, but Sega's servers also replied to it if it was sent in a lobby. They // would return a string like "LOBBY01" even though this would never be used // under normal circumstances. -// 8B: Invalid command +// 8B: Log in (DC NTE only) + +struct C_Login_DCNTE_8B { + le_uint32_t player_tag; + le_uint32_t guild_card_number; + parray hardware_id; + le_uint32_t sub_version; + uint8_t is_extended; + uint8_t language; + parray unused1; + ptext serial_number; + ptext access_key; + ptext username; + ptext password; + ptext name; + parray unused; + SC_MeetUserExtension extension; +}; + // 8C: Invalid command // 8D: Invalid command // 8E: Invalid command @@ -1752,7 +1806,7 @@ struct S_RankUpdate_GC_Ep3_B7 { struct S_Unknown_GC_Ep3_B9 { le_uint32_t unknown_a1; // Must be 1-4 (inclusive) le_uint32_t unknown_a2; - le_uint16_t unknown_a3; + le_uint16_t unknown_a3; // Looks like a size of some sort le_uint16_t unused; parray unknown_a5; }; diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 6b3a3701..18f14618 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -171,8 +171,8 @@ static HandlerResult on_server_dc_pc_v3_patch_02_17( // Most servers don't include after_message or have a shorter // after_message than newserv does, so don't require it - const auto& cmd = check_size_t(data, - offsetof(S_ServerInit_DC_PC_V3_02_17_91_9B, after_message), 0xFFFF); + const auto& cmd = check_size_t(data, + sizeof(S_ServerInitDefault_DC_PC_V3_02_17_91_9B), 0xFFFF); if (!session.license) { session.log.info("No license in linked session"); @@ -335,8 +335,8 @@ static HandlerResult on_server_bb_03(shared_ptr s, ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { // Most servers don't include after_message or have a shorter after_message // than newserv does, so don't require it - const auto& cmd = check_size_t(data, - offsetof(S_ServerInit_BB_03_9B, after_message), 0xFFFF); + const auto& cmd = check_size_t(data, + sizeof(S_ServerInitDefault_BB_03_9B), 0xFFFF); // If the session has a detector crypt, then it was resumed from an unlinked // session, during which we already sent an 03 command. diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index bbeeda6a..17275c3a 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -212,6 +212,70 @@ static void on_verify_license_v3(shared_ptr s, shared_ptr c } } +static void on_login_8_dcnte(shared_ptr s, shared_ptr c, + uint16_t, uint32_t, const string& data) { // 88 + const auto& cmd = check_size_t(data); + c->channel.version = GameVersion::DC; + c->flags |= flags_for_version(c->version(), -1); + c->flags |= Client::Flag::IS_DC_V1 | Client::Flag::IS_TRIAL_EDITION; + + uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16); + try { + shared_ptr l = s->license_manager->verify_pc( + serial_number, cmd.access_key); + c->set_license(l); + send_command(c, 0x88, 0x00); + + } catch (const incorrect_access_key& e) { + send_message_box(c, u"Incorrect access key"); + c->should_disconnect = true; + + } catch (const missing_license& e) { + if (!s->allow_unregistered_users) { + send_message_box(c, u"Incorrect serial number"); + c->should_disconnect = true; + } else { + auto l = LicenseManager::create_license_pc( + serial_number, cmd.access_key, true); + s->license_manager->add(l); + c->set_license(l); + send_command(c, 0x88, 0x00); + } + } +} + +static void on_login_b_dcnte(shared_ptr s, shared_ptr c, + uint16_t, uint32_t, const string& data) { // 8B + const auto& cmd = check_size_t(data); + c->channel.version = GameVersion::DC; + c->flags |= flags_for_version(c->version(), -1); + c->flags |= Client::Flag::IS_DC_V1 | Client::Flag::IS_TRIAL_EDITION; + + uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16); + try { + shared_ptr l = s->license_manager->verify_pc( + serial_number, cmd.access_key); + c->set_license(l); + send_command(c, 0x8B, 0x01); + + } catch (const incorrect_access_key& e) { + send_message_box(c, u"Incorrect access key"); + c->should_disconnect = true; + + } catch (const missing_license& e) { + if (!s->allow_unregistered_users) { + send_message_box(c, u"Incorrect serial number"); + c->should_disconnect = true; + } else { + auto l = LicenseManager::create_license_pc( + serial_number, cmd.access_key, true); + s->license_manager->add(l); + c->set_license(l); + send_command(c, 0x8B, 0x01); + } + } +} + static void on_login_0_dc_pc_v3(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 90 const auto& cmd = check_size_t(data); @@ -2364,12 +2428,19 @@ static void on_create_game_bb(shared_ptr s, shared_ptr c, static void on_lobby_name_request(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 8A - check_size_v(data.size(), 0); - auto l = s->find_lobby(c->lobby_id); - if (!l) { - throw invalid_argument("client not in any lobby"); + if ((c->version() == GameVersion::DC) && (c->flags & Client::Flag::IS_TRIAL_EDITION)) { + const auto& cmd = check_size_t(data); + set_console_client_flags(c, cmd.sub_version); + send_command(c, 0x8A, 0x01); + + } else { + check_size_v(data.size(), 0); + auto l = s->find_lobby(c->lobby_id); + if (!l) { + throw invalid_argument("client not in any lobby"); + } + send_lobby_name(c, l->name.c_str()); } - send_lobby_name(c, l->name.c_str()); } static void on_client_ready(shared_ptr s, shared_ptr c, @@ -2822,10 +2893,10 @@ static on_command_t handlers[0x100][6] = { /* 85 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 85 */ /* 86 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 86 */ /* 87 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 87 */ - /* 88 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 88 */ + /* 88 */ {nullptr, on_login_8_dcnte, nullptr, on_login_8_dcnte, nullptr, nullptr, }, /* 88 */ /* 89 */ {nullptr, on_change_arrow_color, on_change_arrow_color, on_change_arrow_color, on_change_arrow_color, on_change_arrow_color, }, /* 89 */ /* 8A */ {nullptr, on_lobby_name_request, on_lobby_name_request, on_lobby_name_request, on_lobby_name_request, on_lobby_name_request, }, /* 8A */ - /* 8B */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 8B */ + /* 8B */ {nullptr, on_login_b_dcnte, nullptr, nullptr, nullptr, nullptr, }, /* 8B */ /* 8C */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 8C */ /* 8D */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 8D */ /* 8E */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 8E */ @@ -2963,7 +3034,8 @@ static void check_unlicensed_command(GameVersion version, uint8_t command) { case GameVersion::GC: case GameVersion::XB: // See comment in the DC case above for why DC commands are included here. - if (command != 0x90 && // DC v1 + if (command != 0x88 && // DC NTE + command != 0x90 && // DC v1 command != 0x93 && // DC v1 command != 0x9A && // DC v2 command != 0x9D && // DC v2, GC trial edition diff --git a/src/ReplaySession.cc b/src/ReplaySession.cc index 2dd22d69..6bb33827 100644 --- a/src/ReplaySession.cc +++ b/src/ReplaySession.cc @@ -218,8 +218,8 @@ void ReplaySession::apply_default_mask(shared_ptr ev) { case 0x17: case 0x91: case 0x9B: { - auto& cmd_mask = check_size_t( - cmd_data, cmd_size, sizeof(S_ServerInit_DC_PC_V3_02_17_91_9B), 0xFFFF); + auto& cmd_mask = check_size_t( + cmd_data, cmd_size, sizeof(S_ServerInitDefault_DC_PC_V3_02_17_91_9B), 0xFFFF); cmd_mask.server_key = 0; cmd_mask.client_key = 0; break; @@ -275,8 +275,8 @@ void ReplaySession::apply_default_mask(shared_ptr ev) { ev->data, sizeof(PSOCommandHeaderBB), 0xFFFF).command; switch (command) { case 0x0003: { - auto& cmd_mask = check_size_t( - cmd_data, cmd_size, sizeof(S_ServerInit_BB_03_9B), 0xFFFF); + auto& cmd_mask = check_size_t( + cmd_data, cmd_size, sizeof(S_ServerInitDefault_BB_03_9B), 0xFFFF); cmd_mask.server_key.clear(0); cmd_mask.client_key.clear(0); break; @@ -593,8 +593,8 @@ void ReplaySession::on_command_received( case GameVersion::GC: case GameVersion::XB: if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) { - auto& cmd = check_size_t(data, - offsetof(S_ServerInit_DC_PC_V3_02_17_91_9B, after_message), 0xFFFF); + auto& cmd = check_size_t(data, + sizeof(S_ServerInitDefault_DC_PC_V3_02_17_91_9B), 0xFFFF); if ((c->version == GameVersion::DC) || (c->version == GameVersion::PC)) { c->channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key)); c->channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key)); @@ -606,8 +606,8 @@ void ReplaySession::on_command_received( break; case GameVersion::BB: if (command == 0x03 || command == 0x9B) { - auto& cmd = check_size_t(data, - sizeof(S_ServerInit_BB_03_9B), 0xFFFF); + auto& cmd = check_size_t(data, + sizeof(S_ServerInitDefault_BB_03_9B), 0xFFFF); // TODO: At some point it may matter which BB private key file we use. // Don't just blindly use the first one here. c->channel.crypt_in.reset(new PSOBBEncryption( diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 232fb432..b148afa8 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -27,6 +27,7 @@ extern FileContentsCache file_cache; const unordered_set v2_crypt_initial_client_commands({ + 0x00260088, // (17) DCNTE license check 0x00280090, // (17) DCv1 license check 0x00B00093, // (02) DCv1 login 0x01140093, // (02) DCv1 extended login @@ -111,17 +112,18 @@ void send_command_with_header(Channel& ch, const void* data, size_t size) { -static const char* anti_copyright = "This server is in no way affiliated, sponsored, or supported by SEGA Enterprises or SONICTEAM. The preceding message exists only in order to remain compatible with programs that expect it."; +static const char* anti_copyright = "This server is in no way affiliated, sponsored, or supported by SEGA Enterprises or SONICTEAM. The preceding message exists only to remain compatible with programs that expect it."; static const char* dc_port_map_copyright = "DreamCast Port Map. Copyright SEGA Enterprises. 1999"; static const char* dc_lobby_server_copyright = "DreamCast Lobby Server. Copyright SEGA Enterprises. 1999"; static const char* bb_game_server_copyright = "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM."; static const char* bb_pm_server_copyright = "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM."; static const char* patch_server_copyright = "Patch Server. Copyright SonicTeam, LTD. 2001"; -S_ServerInit_DC_PC_V3_02_17_91_9B prepare_server_init_contents_console( +S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B<0xCC> +prepare_server_init_contents_console( uint32_t server_key, uint32_t client_key, uint8_t flags) { bool initial_connection = (flags & SendServerInitFlag::IS_INITIAL_CONNECTION); - S_ServerInit_DC_PC_V3_02_17_91_9B cmd; + S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B<0xCC> cmd; cmd.copyright = initial_connection ? dc_port_map_copyright : dc_lobby_server_copyright; cmd.server_key = server_key; @@ -138,7 +140,7 @@ void send_server_init_dc_pc_v3(shared_ptr c, uint8_t flags) { auto cmd = prepare_server_init_contents_console( server_key, client_key, initial_connection); - send_command_t(c, command, 0x00, cmd); + send_command_t(c, command, 0x01, cmd); switch (c->version()) { case GameVersion::PC: @@ -159,12 +161,12 @@ void send_server_init_dc_pc_v3(shared_ptr c, uint8_t flags) { } } -S_ServerInit_BB_03_9B prepare_server_init_contents_bb( +S_ServerInitWithAfterMessage_BB_03_9B prepare_server_init_contents_bb( const parray& server_key, const parray& client_key, uint8_t flags) { bool use_secondary_message = (flags & SendServerInitFlag::USE_SECONDARY_MESSAGE); - S_ServerInit_BB_03_9B cmd; + S_ServerInitWithAfterMessage_BB_03_9B cmd; cmd.copyright = use_secondary_message ? bb_pm_server_copyright : bb_game_server_copyright; cmd.server_key = server_key; cmd.client_key = client_key; diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 63d6fe62..a182240d 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -102,9 +102,10 @@ enum SendServerInitFlag { USE_SECONDARY_MESSAGE = 0x02, }; -S_ServerInit_DC_PC_V3_02_17_91_9B prepare_server_init_contents_console( +S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B<0xCC> +prepare_server_init_contents_console( uint32_t server_key, uint32_t client_key, uint8_t flags); -S_ServerInit_BB_03_9B prepare_server_init_contents_bb( +S_ServerInitWithAfterMessage_BB_03_9B prepare_server_init_contents_bb( const parray& server_key, const parray& client_key, uint8_t flags); diff --git a/src/Version.cc b/src/Version.cc index 4b79665b..3e892383 100644 --- a/src/Version.cc +++ b/src/Version.cc @@ -32,6 +32,9 @@ uint16_t flags_for_version(GameVersion version, int64_t sub_version) { break; // TODO: Which other sub_versions of DC v1 and v2 exist? + case 0x20: // DCNTE + // In the case of DCNTE, the IS_TRIAL_EDITION flag is already set when we + // get here, so the remaining flags are the same as DCv1 case 0x21: // DCv1 US return Client::Flag::IS_DC_V1 | Client::Flag::NO_D6 |