diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index 5b370cc0f5..f2e410492c 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -1956,6 +1956,7 @@ DEF_CONSOLE_CMD(ConSayClient) /** All the known authorized keys with their name. */ static std::vector> _console_cmd_authorized_keys{ + { "admin", &_settings_client.network.admin_authorized_keys }, { "rcon", &_settings_client.network.rcon_authorized_keys }, { "server", &_settings_client.network.server_authorized_keys }, }; diff --git a/src/lang/english.txt b/src/lang/english.txt index 75ffad4c95..87a8d3c594 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2575,7 +2575,7 @@ STR_NETWORK_ERROR_INVALID_CLIENT_NAME :{WHITE}Your pla STR_NETWORK_ERROR_CLIENT_GUI_LOST_CONNECTION_CAPTION :{WHITE}Possible connection loss STR_NETWORK_ERROR_CLIENT_GUI_LOST_CONNECTION :{WHITE}The last {NUM} second{P "" s} no data has arrived from the server -###length 22 +###length 23 STR_NETWORK_ERROR_CLIENT_GENERAL :general error STR_NETWORK_ERROR_CLIENT_DESYNC :desync error STR_NETWORK_ERROR_CLIENT_SAVEGAME :could not load map @@ -2588,6 +2588,7 @@ STR_NETWORK_ERROR_CLIENT_WRONG_REVISION :wrong revision STR_NETWORK_ERROR_CLIENT_NAME_IN_USE :name already in use STR_NETWORK_ERROR_CLIENT_WRONG_PASSWORD :wrong password STR_NETWORK_ERROR_CLIENT_NOT_ON_ALLOW_LIST :not on allow list +STR_NETWORK_ERROR_CLIENT_NO_AUTHENTICATION_METHOD_AVAILABLE :none of the requested authentication methods is available STR_NETWORK_ERROR_CLIENT_COMPANY_MISMATCH :wrong company in DoCommand STR_NETWORK_ERROR_CLIENT_KICKED :kicked by server STR_NETWORK_ERROR_CLIENT_CHEATER :was trying to use a cheat diff --git a/src/network/core/tcp_admin.cpp b/src/network/core/tcp_admin.cpp index d29e08ba63..fedb83ca2c 100644 --- a/src/network/core/tcp_admin.cpp +++ b/src/network/core/tcp_admin.cpp @@ -63,6 +63,8 @@ NetworkRecvStatus NetworkAdminSocketHandler::HandlePacket(Packet &p) case ADMIN_PACKET_ADMIN_RCON: return this->Receive_ADMIN_RCON(p); case ADMIN_PACKET_ADMIN_GAMESCRIPT: return this->Receive_ADMIN_GAMESCRIPT(p); case ADMIN_PACKET_ADMIN_PING: return this->Receive_ADMIN_PING(p); + case ADMIN_PACKET_ADMIN_JOIN_SECURE: return this->Receive_ADMIN_JOIN_SECURE(p); + case ADMIN_PACKET_ADMIN_AUTH_RESPONSE: return this->Receive_ADMIN_AUTH_RESPONSE(p); case ADMIN_PACKET_SERVER_FULL: return this->Receive_SERVER_FULL(p); case ADMIN_PACKET_SERVER_BANNED: return this->Receive_SERVER_BANNED(p); @@ -91,6 +93,7 @@ NetworkRecvStatus NetworkAdminSocketHandler::HandlePacket(Packet &p) case ADMIN_PACKET_SERVER_CMD_LOGGING: return this->Receive_SERVER_CMD_LOGGING(p); case ADMIN_PACKET_SERVER_RCON_END: return this->Receive_SERVER_RCON_END(p); case ADMIN_PACKET_SERVER_PONG: return this->Receive_SERVER_PONG(p); + case ADMIN_PACKET_SERVER_AUTH_REQUEST: return this->Receive_SERVER_AUTH_REQUEST(p); default: Debug(net, 0, "[tcp/admin] Received invalid packet type {} from '{}' ({})", type, this->admin_name, this->admin_version); @@ -137,6 +140,8 @@ NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_EXTERNAL_CHAT(Packet NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_RCON(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_ADMIN_RCON); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_GAMESCRIPT(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_ADMIN_GAMESCRIPT); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_PING(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_ADMIN_PING); } +NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_JOIN_SECURE(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_ADMIN_JOIN_SECURE); } +NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_AUTH_RESPONSE(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_ADMIN_AUTH_RESPONSE); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_FULL(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_FULL); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_BANNED(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_BANNED); } @@ -165,3 +170,4 @@ NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_CMD_NAMES(Packet &) NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_CMD_LOGGING(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_CMD_LOGGING); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_RCON_END(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_RCON_END); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_PONG(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_PONG); } +NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_AUTH_REQUEST(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_AUTH_REQUEST); } diff --git a/src/network/core/tcp_admin.h b/src/network/core/tcp_admin.h index c65856c062..0c2137227f 100644 --- a/src/network/core/tcp_admin.h +++ b/src/network/core/tcp_admin.h @@ -22,7 +22,7 @@ * This protocol may only be extended to ensure stability. */ enum PacketAdminType : uint8_t { - ADMIN_PACKET_ADMIN_JOIN, ///< The admin announces and authenticates itself to the server. + ADMIN_PACKET_ADMIN_JOIN, ///< The admin announces and authenticates itself to the server using an unsecured passwords. ADMIN_PACKET_ADMIN_QUIT, ///< The admin tells the server that it is quitting. ADMIN_PACKET_ADMIN_UPDATE_FREQUENCY, ///< The admin tells the server the update frequency of a particular piece of information. ADMIN_PACKET_ADMIN_POLL, ///< The admin explicitly polls for a piece of information. @@ -31,6 +31,8 @@ enum PacketAdminType : uint8_t { ADMIN_PACKET_ADMIN_GAMESCRIPT, ///< The admin sends a JSON string for the GameScript. ADMIN_PACKET_ADMIN_PING, ///< The admin sends a ping to the server, expecting a ping-reply (PONG) packet. ADMIN_PACKET_ADMIN_EXTERNAL_CHAT, ///< The admin sends a chat message from external source. + ADMIN_PACKET_ADMIN_JOIN_SECURE, ///< The admin announces and starts a secure authentication handshake. + ADMIN_PACKET_ADMIN_AUTH_RESPONSE, ///< The admin responds to the authentication request. ADMIN_PACKET_SERVER_FULL = 100, ///< The server tells the admin it cannot accept the admin. ADMIN_PACKET_SERVER_BANNED, ///< The server tells the admin it is banned. @@ -61,6 +63,7 @@ enum PacketAdminType : uint8_t { ADMIN_PACKET_SERVER_RCON_END, ///< The server indicates that the remote console command has completed. ADMIN_PACKET_SERVER_PONG, ///< The server replies to a ping request from the admin. ADMIN_PACKET_SERVER_CMD_LOGGING, ///< The server gives the admin copies of incoming command packets. + ADMIN_PACKET_SERVER_AUTH_REQUEST, ///< The server gives the admin the used authentication method and required parameters. INVALID_ADMIN_PACKET = 0xFF, ///< An invalid marker for admin packets. }; @@ -119,8 +122,8 @@ protected: NetworkRecvStatus ReceiveInvalidPacket(PacketAdminType type); /** - * Join the admin network: - * string Password the server is expecting for this network. + * Join the admin network using an unsecured password exchange: + * string Unsecured password the server is expecting for this network. * string Name of the application being used to connect. * string Version string of the application being used to connect. * @param p The packet that was just received. @@ -201,6 +204,32 @@ protected: */ virtual NetworkRecvStatus Receive_ADMIN_PING(Packet &p); + /** + * Join the admin network using a secure authentication method: + * string Name of the application being used to connect. + * string Version string of the application being used to connect. + * uint16_t Bitmask of supported authentication methods. See \c NetworkAuthenticationMethod for the supported methods. + * + * The server will determine which of the authentication methods supplied by the client will be used. + * When there is no supported authentication method, an \c ADMIN_PACKET_SERVER_ERROR packet will be + * sent with \c NETWORK_ERROR_NO_AUTHENTICATION_METHOD_AVAILABLE as error. + * @param p The packet that was just received. + * @return The state the network should have. + */ + virtual NetworkRecvStatus Receive_ADMIN_JOIN_SECURE(Packet &p); + + /** + * Admin responds to \c ADMIN_PACKET_SERVER_AUTH_REQUEST with the appropriate + * data given the agreed upon \c NetworkAuthenticationMethod. + * With \c NETWORK_AUTH_METHOD_X25519_PAKE and \c NETWORK_AUTH_METHOD_X25519_AUTHORIZED_KEY: + * 32 * uint8_t Public key of the client. + * 16 * uint8_t Message authentication code (mac). + * 8 * uint8_t Encrypted message of the authentication (just random bytes). + * @param p The packet that was just received. + * @return The state the network should have. + */ + virtual NetworkRecvStatus Receive_ADMIN_AUTH_RESPONSE(Packet &p); + /** * The server is full (connection gets closed). * @param p The packet that was just received. @@ -473,6 +502,17 @@ protected: */ virtual NetworkRecvStatus Receive_SERVER_CMD_LOGGING(Packet &p); + /** + * Server requests authentication challenge from the admin. + * uint8_t The chosen authentication method from \c NetworkAuthenticationMethod. + * With \c NETWORK_AUTH_METHOD_X25519_PAKE and \c NETWORK_AUTH_METHOD_X25519_AUTHORIZED_KEY: + * 32 * uint8_t Public key of the server. + * 24 * uint8_t Nonce to use for the encryption. + * @param p The packet that was just received. + * @return The state the network should have. + */ + virtual NetworkRecvStatus Receive_SERVER_AUTH_REQUEST(Packet &p); + /** * Send a ping-reply (pong) to the admin that sent us the ping packet. * uint32_t Integer identifier - should be the same as read from the admins ping packet. diff --git a/src/network/network.cpp b/src/network/network.cpp index c0c13efb1d..717480d7ed 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -339,6 +339,7 @@ StringID GetNetworkErrorMsg(NetworkErrorCode err) STR_NETWORK_ERROR_CLIENT_TIMEOUT_JOIN, STR_NETWORK_ERROR_CLIENT_INVALID_CLIENT_NAME, STR_NETWORK_ERROR_CLIENT_NOT_ON_ALLOW_LIST, + STR_NETWORK_ERROR_CLIENT_NO_AUTHENTICATION_METHOD_AVAILABLE, }; static_assert(lengthof(network_error_strings) == NETWORK_ERROR_END); @@ -905,8 +906,8 @@ bool NetworkServerStart() Debug(net, 5, "Starting listeners for clients"); if (!ServerNetworkGameSocketHandler::Listen(_settings_client.network.server_port)) return false; - /* Only listen for admins when the password isn't empty. */ - if (!_settings_client.network.admin_password.empty()) { + /* Only listen for admins when the authentication is configured. */ + if (_settings_client.network.AdminAuthenticationConfigured()) { Debug(net, 5, "Starting listeners for admins"); if (!ServerNetworkAdminSocketHandler::Listen(_settings_client.network.server_admin_port)) return false; } diff --git a/src/network/network_admin.cpp b/src/network/network_admin.cpp index 6f72e5f7d1..c8771bf0ea 100644 --- a/src/network/network_admin.cpp +++ b/src/network/network_admin.cpp @@ -38,6 +38,9 @@ uint8_t _network_admins_connected = 0; NetworkAdminSocketPool _networkadminsocket_pool("NetworkAdminSocket"); INSTANTIATE_POOL_METHODS(NetworkAdminSocket) +static NetworkAuthenticationDefaultPasswordProvider _admin_password_provider(_settings_client.network.admin_password); ///< Provides the password validation for the game's password. +static NetworkAuthenticationDefaultAuthorizedKeyHandler _admin_authorized_key_handler(_settings_client.network.admin_authorized_keys); ///< Provides the authorized key handling for the game authentication. + /** The timeout for authorisation of the client. */ static const std::chrono::seconds ADMIN_AUTHORISATION_TIMEOUT(10); @@ -90,7 +93,7 @@ ServerNetworkAdminSocketHandler::~ServerNetworkAdminSocketHandler() */ /* static */ bool ServerNetworkAdminSocketHandler::AllowConnection() { - bool accept = !_settings_client.network.admin_password.empty() && _network_admins_connected < MAX_ADMINS; + bool accept = _settings_client.network.AdminAuthenticationConfigured() && _network_admins_connected < MAX_ADMINS; /* We can't go over the MAX_ADMINS limit here. However, if we accept * the connection, there has to be space in the pool. */ static_assert(NetworkAdminSocketPool::MAX_SIZE == MAX_ADMINS); @@ -134,6 +137,9 @@ ServerNetworkAdminSocketHandler::~ServerNetworkAdminSocketHandler() */ NetworkRecvStatus ServerNetworkAdminSocketHandler::SendError(NetworkErrorCode error) { + /* Whatever the error might be, authentication (keys) must be released as soon as possible. */ + this->authentication_handler = nullptr; + auto p = std::make_unique(this, ADMIN_PACKET_SERVER_ERROR); p->Send_uint8(error); @@ -791,6 +797,71 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_EXTERNAL_CHAT(P return NETWORK_RECV_STATUS_OKAY; } +/* + * Secure authentication send and receive methods. + */ + +NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_JOIN_SECURE(Packet &p) +{ + if (this->status != ADMIN_STATUS_INACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + + this->admin_name = p.Recv_string(NETWORK_CLIENT_NAME_LENGTH); + this->admin_version = p.Recv_string(NETWORK_REVISION_LENGTH); + NetworkAuthenticationMethodMask method_mask = p.Recv_uint16(); + + /* Always exclude key exchange only, as that provides no credential checking. */ + ClrBit(method_mask, NETWORK_AUTH_METHOD_X25519_KEY_EXCHANGE_ONLY); + + if (this->admin_name.empty() || this->admin_version.empty()) { + /* No name or version supplied. */ + return this->SendError(NETWORK_ERROR_ILLEGAL_PACKET); + } + + auto handler = NetworkAuthenticationServerHandler::Create(&_admin_password_provider, &_admin_authorized_key_handler, method_mask); + if (!handler->CanBeUsed()) return this->SendError(NETWORK_ERROR_NO_AUTHENTICATION_METHOD_AVAILABLE); + + this->authentication_handler = std::move(handler); + Debug(net, 3, "[admin] '{}' ({}) has connected", this->admin_name, this->admin_version); + + return this->SendAuthRequest(); +} + +NetworkRecvStatus ServerNetworkAdminSocketHandler::SendAuthRequest() +{ + this->status = ADMIN_STATUS_AUTHENTICATE; + + Debug(net, 6, "[admin] '{}' ({}) authenticating using {}", this->admin_name, this->admin_version, this->authentication_handler->GetName()); + + auto p = std::make_unique(this, ADMIN_PACKET_SERVER_AUTH_REQUEST); + this->authentication_handler->SendRequest(*p); + + this->SendPacket(std::move(p)); + + return NETWORK_RECV_STATUS_OKAY; +} + +NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_AUTH_RESPONSE(Packet &p) +{ + if (this->status != ADMIN_STATUS_AUTHENTICATE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + + switch (this->authentication_handler->ReceiveResponse(p)) { + case NetworkAuthenticationServerHandler::AUTHENTICATED: + Debug(net, 3, "[admin] '{}' ({}) authenticated", this->admin_name, this->admin_version); + + this->authentication_handler = nullptr; + return this->SendProtocol(); + + case NetworkAuthenticationServerHandler::RETRY_NEXT_METHOD: + Debug(net, 6, "[admin] '{}' ({}) authentication failed, trying next method", this->admin_name, this->admin_version); + return this->SendAuthRequest(); + + case NetworkAuthenticationServerHandler::NOT_AUTHENTICATED: + default: + Debug(net, 3, "[admin] '{}' ({}) authentication failed", this->admin_name, this->admin_version); + return this->SendError(NETWORK_ERROR_WRONG_PASSWORD); + } +} + /* * Useful wrapper functions */ diff --git a/src/network/network_admin.h b/src/network/network_admin.h index 766bee1d7e..91863d05b2 100644 --- a/src/network/network_admin.h +++ b/src/network/network_admin.h @@ -23,6 +23,8 @@ extern NetworkAdminSocketPool _networkadminsocket_pool; /** Class for handling the server side of the game connection. */ class ServerNetworkAdminSocketHandler : public NetworkAdminSocketPool::PoolItem<&_networkadminsocket_pool>, public NetworkAdminSocketHandler, public TCPListenHandler { +private: + std::unique_ptr authentication_handler; ///< The handler for the authentication. protected: NetworkRecvStatus Receive_ADMIN_JOIN(Packet &p) override; NetworkRecvStatus Receive_ADMIN_QUIT(Packet &p) override; @@ -33,9 +35,12 @@ protected: NetworkRecvStatus Receive_ADMIN_RCON(Packet &p) override; NetworkRecvStatus Receive_ADMIN_GAMESCRIPT(Packet &p) override; NetworkRecvStatus Receive_ADMIN_PING(Packet &p) override; + NetworkRecvStatus Receive_ADMIN_JOIN_SECURE(Packet &p) override; + NetworkRecvStatus Receive_ADMIN_AUTH_RESPONSE(Packet &p) override; NetworkRecvStatus SendProtocol(); NetworkRecvStatus SendPong(uint32_t d1); + NetworkRecvStatus SendAuthRequest(); public: AdminUpdateFrequency update_frequency[ADMIN_UPDATE_END]; ///< Admin requested update intervals. std::chrono::steady_clock::time_point connect_time; ///< Time of connection. diff --git a/src/network/network_client.cpp b/src/network/network_client.cpp index 5e85deaefe..001e757193 100644 --- a/src/network/network_client.cpp +++ b/src/network/network_client.cpp @@ -646,6 +646,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_ERROR(Packet &p STR_NETWORK_ERROR_TIMEOUT_JOIN, // NETWORK_ERROR_TIMEOUT_JOIN STR_NETWORK_ERROR_INVALID_CLIENT_NAME, // NETWORK_ERROR_INVALID_CLIENT_NAME STR_NETWORK_ERROR_NOT_ON_ALLOW_LIST, // NETWORK_ERROR_NOT_ON_ALLOW_LIST + STR_NETWORK_ERROR_SERVER_ERROR, // NETWORK_ERROR_NO_AUTHENTICATION_METHOD_AVAILABLE }; static_assert(lengthof(network_error_strings) == NETWORK_ERROR_END); diff --git a/src/network/network_crypto.h b/src/network/network_crypto.h index 74424860a9..ad8b74ca37 100644 --- a/src/network/network_crypto.h +++ b/src/network/network_crypto.h @@ -175,7 +175,7 @@ public: /** The authentication method that can be used. */ enum NetworkAuthenticationMethod : uint8_t { - NETWORK_AUTH_METHOD_X25519_KEY_EXCHANGE_ONLY, ///< No actual authentication is taking place, just perform a x25519 key exchange. + NETWORK_AUTH_METHOD_X25519_KEY_EXCHANGE_ONLY, ///< No actual authentication is taking place, just perform a x25519 key exchange. This method is not supported for the admin connection. NETWORK_AUTH_METHOD_X25519_PAKE, ///< Authentication using x25519 password-authenticated key agreement. NETWORK_AUTH_METHOD_X25519_AUTHORIZED_KEY, ///< Authentication using x22519 key exchange and authorized keys. NETWORK_AUTH_METHOD_END, ///< Must ALWAYS be on the end of this list!! (period) diff --git a/src/network/network_type.h b/src/network/network_type.h index 911a163de0..3e6fb79c20 100644 --- a/src/network/network_type.h +++ b/src/network/network_type.h @@ -134,6 +134,7 @@ enum NetworkErrorCode { NETWORK_ERROR_TIMEOUT_JOIN, NETWORK_ERROR_INVALID_CLIENT_NAME, NETWORK_ERROR_NOT_ON_ALLOW_LIST, + NETWORK_ERROR_NO_AUTHENTICATION_METHOD_AVAILABLE, NETWORK_ERROR_END, }; diff --git a/src/settings.cpp b/src/settings.cpp index 33f6b1d0bf..0576af3883 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -142,6 +142,7 @@ private: "server_bind_addresses", "server_authorized_keys", "rcon_authorized_keys", + "admin_authorized_keys" }; public: @@ -1268,6 +1269,7 @@ static void HandleSettingDescs(IniFile &generic_ini, IniFile &private_ini, IniFi proc_list(private_ini, "bans", _network_ban_list); proc_list(private_ini, "server_authorized_keys", _settings_client.network.server_authorized_keys); proc_list(private_ini, "rcon_authorized_keys", _settings_client.network.rcon_authorized_keys); + proc_list(private_ini, "admin_authorized_keys", _settings_client.network.admin_authorized_keys); } } diff --git a/src/settings_type.h b/src/settings_type.h index 9b1a7b6000..618c497a50 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -325,6 +325,7 @@ struct NetworkSettings { std::string rcon_password; ///< password for rconsole (server side) NetworkAuthorizedKeys rcon_authorized_keys; ///< Public keys of clients that are authorized to use the rconsole (server side). std::string admin_password; ///< password for the admin network + NetworkAuthorizedKeys admin_authorized_keys; ///< Public keys of clients that are authorized to use the admin network. std::string client_name; ///< name of the player (as client) std::string client_secret_key; ///< The secret key of the client for authorized key logins. std::string client_public_key; ///< The public key of the client for authorized key logins. @@ -341,6 +342,8 @@ struct NetworkSettings { std::string last_joined; ///< Last joined server UseRelayService use_relay_service; ///< Use relay service? ParticipateSurvey participate_survey; ///< Participate in the automated survey + + bool AdminAuthenticationConfigured() const { return !this->admin_password.empty() || !this->admin_authorized_keys.empty(); } }; /** Settings related to the creation of games. */