diff --git a/src/command_type.h b/src/command_type.h index dae768104b..8f3cbedd93 100644 --- a/src/command_type.h +++ b/src/command_type.h @@ -294,6 +294,7 @@ enum Commands : uint16_t { CMD_CREATE_SUBSIDY, ///< create a new subsidy CMD_COMPANY_CTRL, ///< used in multiplayer to create a new companies etc. + CMD_COMPANY_ADD_ALLOW_LIST, ///< Used in multiplayer to add a client's public key to the company's allow list. CMD_CUSTOM_NEWS_ITEM, ///< create a custom news message CMD_CREATE_GOAL, ///< create a new goal CMD_REMOVE_GOAL, ///< remove a goal diff --git a/src/company_base.h b/src/company_base.h index 9d4a3dc87b..d6ab4a8fd1 100644 --- a/src/company_base.h +++ b/src/company_base.h @@ -75,6 +75,8 @@ struct CompanyProperties { uint32_t president_name_2; ///< Parameter of #president_name_1 std::string president_name; ///< Name of the president if the user changed it. + NetworkAuthorizedKeys allow_list; ///< Public keys of clients that are allowed to join this company. + CompanyManagerFace face; ///< Face description of the president. Money money; ///< Money owned by the company. diff --git a/src/company_cmd.cpp b/src/company_cmd.cpp index 6fa44f4bc5..d937479419 100644 --- a/src/company_cmd.cpp +++ b/src/company_cmd.cpp @@ -980,6 +980,24 @@ CommandCost CmdCompanyCtrl(DoCommandFlag flags, CompanyCtrlAction cca, CompanyID return CommandCost(); } +/** + * Add the given public key to the allow list of this company. + * @param flags Operation to perform. + * @param public_key The public key of the client to add. + * @return The cost of this operation or an error. + */ +CommandCost CmdCompanyAddAllowList(DoCommandFlag flags, const std::string &public_key) +{ + if (flags & DC_EXEC) { + if (Company::Get(_current_company)->allow_list.Add(public_key)) { + InvalidateWindowData(WC_CLIENT_LIST, 0); + SetWindowDirty(WC_COMPANY, _current_company); + } + } + + return CommandCost(); +} + /** * Change the company manager's face. * @param flags operation to perform diff --git a/src/company_cmd.h b/src/company_cmd.h index 8493549c58..5f818c4b8f 100644 --- a/src/company_cmd.h +++ b/src/company_cmd.h @@ -18,6 +18,7 @@ enum ClientID : uint32_t; enum Colours : uint8_t; CommandCost CmdCompanyCtrl(DoCommandFlag flags, CompanyCtrlAction cca, CompanyID company_id, CompanyRemoveReason reason, ClientID client_id); +CommandCost CmdCompanyAddAllowList(DoCommandFlag flags, const std::string &public_key); CommandCost CmdGiveMoney(DoCommandFlag flags, Money money, CompanyID dest_company); CommandCost CmdRenameCompany(DoCommandFlag flags, const std::string &text); CommandCost CmdRenamePresident(DoCommandFlag flags, const std::string &text); @@ -25,6 +26,7 @@ CommandCost CmdSetCompanyManagerFace(DoCommandFlag flags, CompanyManagerFace cmf CommandCost CmdSetCompanyColour(DoCommandFlag flags, LiveryScheme scheme, bool primary, Colours colour); DEF_CMD_TRAIT(CMD_COMPANY_CTRL, CmdCompanyCtrl, CMD_SPECTATOR | CMD_CLIENT_ID | CMD_NO_EST, CMDT_SERVER_SETTING) +DEF_CMD_TRAIT(CMD_COMPANY_ADD_ALLOW_LIST, CmdCompanyAddAllowList, CMD_NO_EST, CMDT_SERVER_SETTING) DEF_CMD_TRAIT(CMD_GIVE_MONEY, CmdGiveMoney, 0, CMDT_MONEY_MANAGEMENT) DEF_CMD_TRAIT(CMD_RENAME_COMPANY, CmdRenameCompany, 0, CMDT_OTHER_MANAGEMENT) DEF_CMD_TRAIT(CMD_RENAME_PRESIDENT, CmdRenamePresident, 0, CMDT_OTHER_MANAGEMENT) diff --git a/src/network/core/config.h b/src/network/core/config.h index e74cfb2fff..99e6a3bd94 100644 --- a/src/network/core/config.h +++ b/src/network/core/config.h @@ -96,5 +96,10 @@ static const uint NETWORK_MAX_GRF_COUNT = 255; * This is related to \c X25519_KEY_SIZE in the network crypto internals. */ static const uint NETWORK_SECRET_KEY_LENGTH = 32 * 2 + 1; +/** + * The maximum length of the hexadecimal encoded public keys, in bytes including '\0'. + * This is related to \c X25519_KEY_SIZE in the network crypto internals. + */ +static const uint NETWORK_PUBLIC_KEY_LENGTH = 32 * 2 + 1; #endif /* NETWORK_CORE_CONFIG_H */ diff --git a/src/network/core/tcp_game.h b/src/network/core/tcp_game.h index ce6f9bdcea..d29a17d8e0 100644 --- a/src/network/core/tcp_game.h +++ b/src/network/core/tcp_game.h @@ -202,7 +202,8 @@ protected: * Send information about a client: * uint32_t ID of the client (always unique on a server. 1 = server, 0 is invalid). * uint8_t ID of the company the client is playing as (255 for spectators). - * string Name of the client. + * string Name of the client. + * string Public key of the client. * @param p The packet that was just received. */ virtual NetworkRecvStatus Receive_SERVER_CLIENT_INFO(Packet &p); diff --git a/src/network/network.cpp b/src/network/network.cpp index b37b1aaf68..5e16ae49fb 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -165,10 +165,12 @@ bool NetworkAuthorizedKeys::Contains(std::string_view key) const /** * Add the given key to the authorized keys, when it is not already contained. * @param key The key to add. - * @return \c true when the key was added, \c false when the key already existed. + * @return \c true when the key was added, \c false when the key already existed or the key was empty. */ bool NetworkAuthorizedKeys::Add(std::string_view key) { + if (key.empty()) return false; + auto iter = FindKey(this, key); if (iter != this->end()) return false; diff --git a/src/network/network_base.h b/src/network/network_base.h index 0b717163f7..0b3cbde7d2 100644 --- a/src/network/network_base.h +++ b/src/network/network_base.h @@ -24,6 +24,7 @@ extern NetworkClientInfoPool _networkclientinfo_pool; struct NetworkClientInfo : NetworkClientInfoPool::PoolItem<&_networkclientinfo_pool> { ClientID client_id; ///< Client identifier (same as ClientState->client_id) std::string client_name; ///< Name of the client + std::string public_key; ///< The public key of the client. CompanyID client_playas; ///< As which company is this client playing (CompanyID) TimerGameEconomy::Date join_date; ///< Gamedate the client has joined diff --git a/src/network/network_client.cpp b/src/network/network_client.cpp index f6302a40e3..1e6dda3b57 100644 --- a/src/network/network_client.cpp +++ b/src/network/network_client.cpp @@ -608,6 +608,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_CLIENT_INFO(Pac Debug(net, 9, "Client::Receive_SERVER_CLIENT_INFO(): client_id={}, playas={}", client_id, playas); std::string name = p.Recv_string(NETWORK_NAME_LENGTH); + std::string public_key = p.Recv_string(NETWORK_PUBLIC_KEY_LENGTH); if (this->status < STATUS_AUTHORIZED) return NETWORK_RECV_STATUS_MALFORMED_PACKET; if (this->HasClientQuit()) return NETWORK_RECV_STATUS_CLIENT_QUIT; @@ -632,6 +633,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_CLIENT_INFO(Pac ci->client_playas = playas; ci->client_name = name; + ci->public_key = public_key; InvalidateWindowData(WC_CLIENT_LIST, 0); @@ -651,6 +653,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_CLIENT_INFO(Pac if (client_id == _network_own_client_id) this->SetInfo(ci); ci->client_name = name; + ci->public_key = public_key; InvalidateWindowData(WC_CLIENT_LIST, 0); diff --git a/src/network/network_server.cpp b/src/network/network_server.cpp index 8bd2f4c7e4..c1bfdde98c 100644 --- a/src/network/network_server.cpp +++ b/src/network/network_server.cpp @@ -332,6 +332,7 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::SendClientInfo(NetworkClientIn p->Send_uint32(ci->client_id); p->Send_uint8 (ci->client_playas); p->Send_string(ci->client_name); + p->Send_string(ci->public_key); this->SendPacket(std::move(p)); } @@ -959,6 +960,7 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_IDENTIFY(Packet ci->join_date = TimerGameEconomy::date; ci->client_name = client_name; ci->client_playas = playas; + ci->public_key = this->peer_public_key; Debug(desync, 1, "client: {:08x}; {:02x}; {:02x}; {:02x}", TimerGameEconomy::date, TimerGameEconomy::date_fract, (int)ci->client_playas, (int)ci->index); /* Make sure companies to which people try to join are not autocleaned */ @@ -1177,6 +1179,23 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_COMMAND(Packet } } + if (cp.cmd == CMD_COMPANY_ADD_ALLOW_LIST) { + /* Maybe the client just got moved before allowing? */ + if (ci->client_id != CLIENT_ID_SERVER && ci->client_playas != cp.company) return NETWORK_RECV_STATUS_OKAY; + + std::string public_key = std::get<0>(EndianBufferReader::ToValue::Args>(cp.data)); + bool found = false; + for (const NetworkClientInfo *info : NetworkClientInfo::Iterate()) { + if (info->public_key == public_key) { + found = true; + break; + } + } + + /* Maybe the client just left? */ + if (!found) return NETWORK_RECV_STATUS_OKAY; + } + if (GetCommandFlags(cp.cmd) & CMD_CLIENT_ID) NetworkReplaceCommandClientId(cp, this->client_id); this->incoming_queue.push_back(cp); @@ -1541,7 +1560,7 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_MOVE(Packet &p) if (company_id != COMPANY_SPECTATOR && !Company::IsValidHumanID(company_id)) return NETWORK_RECV_STATUS_OKAY; /* Check if we require a password for this company */ - if (company_id != COMPANY_SPECTATOR && !_network_company_states[company_id].password.empty()) { + if (company_id != COMPANY_SPECTATOR && !Company::Get(company_id)->allow_list.Contains(this->peer_public_key) && !_network_company_states[company_id].password.empty()) { /* we need a password from the client - should be in this packet */ std::string password = p.Recv_string(NETWORK_PASSWORD_LENGTH); @@ -2275,13 +2294,9 @@ void NetworkServerNewCompany(const Company *c, NetworkClientInfo *ci) /* ci is nullptr when replaying, or for AIs. In neither case there is a client. */ ci->client_playas = c->index; NetworkUpdateClientInfo(ci->client_id); + Command::SendNet(STR_NULL, c->index, ci->public_key); Command::SendNet(STR_NULL, c->index, ci->client_name); - } - if (ci != nullptr) { - /* ci is nullptr when replaying, or for AIs. In neither case there is a client. - We need to send Admin port update here so that they first know about the new company - and then learn about a possibly joining client (see FS#6025) */ NetworkServerSendChat(NETWORK_ACTION_COMPANY_NEW, DESTTYPE_BROADCAST, 0, "", ci->client_id, c->index + 1); } } diff --git a/src/saveload/company_sl.cpp b/src/saveload/company_sl.cpp index 50a247da54..651daa1bcb 100644 --- a/src/saveload/company_sl.cpp +++ b/src/saveload/company_sl.cpp @@ -450,6 +450,8 @@ static const SaveLoad _company_desc[] = { SLE_VAR(CompanyProperties, president_name_2, SLE_UINT32), SLE_CONDSSTR(CompanyProperties, president_name, SLE_STR | SLF_ALLOW_CONTROL, SLV_84, SL_MAX_VERSION), + SLE_CONDVECTOR(CompanyProperties, allow_list, SLE_STR, SLV_COMPANY_ALLOW_LIST, SL_MAX_VERSION), + SLE_VAR(CompanyProperties, face, SLE_UINT32), /* money was changed to a 64 bit field in savegame version 1. */ diff --git a/src/saveload/saveload.h b/src/saveload/saveload.h index db9de245c9..94c1303acc 100644 --- a/src/saveload/saveload.h +++ b/src/saveload/saveload.h @@ -379,6 +379,8 @@ enum SaveLoadVersion : uint16_t { SLV_SCRIPT_RANDOMIZER, ///< 333 PR#12063 v14.0-RC1 Save script randomizers. SLV_VEHICLE_ECONOMY_AGE, ///< 334 PR#12141 v14.0 Add vehicle age in economy year, for profit stats minimum age + SLV_COMPANY_ALLOW_LIST, ///< 335 PR#12337 Saving of list of client keys that are allowed to join this company. + SL_MAX_VERSION, ///< Highest possible saveload version }; diff --git a/src/tests/test_network_crypto.cpp b/src/tests/test_network_crypto.cpp index 2c8734e11b..1cba244c9b 100644 --- a/src/tests/test_network_crypto.cpp +++ b/src/tests/test_network_crypto.cpp @@ -18,6 +18,7 @@ /* The length of the hexadecimal representation of a X25519 key must fit in the key length. */ static_assert(NETWORK_SECRET_KEY_LENGTH >= X25519_KEY_SIZE * 2 + 1); +static_assert(NETWORK_PUBLIC_KEY_LENGTH >= X25519_KEY_SIZE * 2 + 1); class MockNetworkSocketHandler : public NetworkSocketHandler { public: