From fb9d4afa5c5115b39afb06b09ffb8d8ccd11fba0 Mon Sep 17 00:00:00 2001 From: Rubidium Date: Thu, 14 Mar 2024 20:07:31 +0100 Subject: [PATCH 1/7] Codechange: add set of classes providing authentication and encryption --- src/network/CMakeLists.txt | 3 + src/network/network_crypto.cpp | 474 ++++++++++++++++++++++++++ src/network/network_crypto.h | 287 ++++++++++++++++ src/network/network_crypto_internal.h | 341 ++++++++++++++++++ src/tests/CMakeLists.txt | 1 + src/tests/test_network_crypto.cpp | 199 +++++++++++ 6 files changed, 1305 insertions(+) create mode 100644 src/network/network_crypto.cpp create mode 100644 src/network/network_crypto.h create mode 100644 src/network/network_crypto_internal.h create mode 100644 src/tests/test_network_crypto.cpp diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index 5b97de5cd2..b78142f457 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -16,6 +16,9 @@ add_files( network_content_gui.h network_coordinator.cpp network_coordinator.h + network_crypto.cpp + network_crypto.h + network_crypto_internal.h network_func.h network_gamelist.cpp network_gamelist.h diff --git a/src/network/network_crypto.cpp b/src/network/network_crypto.cpp new file mode 100644 index 0000000000..7c56a1d988 --- /dev/null +++ b/src/network/network_crypto.cpp @@ -0,0 +1,474 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file network_crypto.cpp Implementation of the network specific cryptography helpers. */ + +#include "../stdafx.h" + +#include "network_crypto_internal.h" +#include "core/packet.h" + +#include "../3rdparty/monocypher/monocypher.h" +#include "../core/random_func.hpp" +#include "../debug.h" +#include "../string_func.h" + +#include "../safeguards.h" + +/** + * Call \c crypto_wipe for all the data in the given span. + * @param span The span to cryptographically wipe. + */ +static void crypto_wipe(std::span span) +{ + crypto_wipe(span.data(), span.size()); +} + +/** Ensure the derived keys do not get leaked when we're done with it. */ +X25519DerivedKeys::~X25519DerivedKeys() +{ + crypto_wipe(keys); +} + +/** + * Get the key to encrypt or decrypt a message sent from the client to the server. + * @return The raw bytes of the key. + */ +std::span X25519DerivedKeys::ClientToServer() const +{ + return std::span(this->keys.data(), X25519_KEY_SIZE); +} + +/** + * Get the key to encrypt or decrypt a message sent from the server to the client. + * @return The raw bytes of the key. + */ +std::span X25519DerivedKeys::ServerToClient() const +{ + return std::span(this->keys.data() + X25519_KEY_SIZE, X25519_KEY_SIZE); +} + +/** + * Perform the actual key exchange. + * @param peer_public_key The public key chosen by the other participant of the key exchange. + * @param side Whether we are the client or server; used to hash the public key of us and the peer in the right order. + * @param our_secret_key The secret key of us. + * @param our_public_key The public key of us. + * @param extra_payload Extra payload to put into the hash function to create the derived keys. + * @return True when the key exchange has succeeded, false when an illegal public key was given. + */ +bool X25519DerivedKeys::Exchange(const X25519PublicKey &peer_public_key, X25519KeyExchangeSide side, + const X25519SecretKey &our_secret_key, const X25519PublicKey &our_public_key, std::string_view extra_payload) +{ + X25519Key shared_secret; + crypto_x25519(shared_secret.data(), our_secret_key.data(), peer_public_key.data()); + if (std::all_of(shared_secret.begin(), shared_secret.end(), [](auto v) { return v == 0; })) { + /* A shared secret of all zeros means that the peer tried to force the shared secret to a known constant. */ + return false; + } + + crypto_blake2b_ctx ctx; + crypto_blake2b_init(&ctx, this->keys.size()); + crypto_blake2b_update(&ctx, shared_secret.data(), shared_secret.size()); + switch (side) { + case X25519KeyExchangeSide::SERVER: + crypto_blake2b_update(&ctx, our_public_key.data(), our_public_key.size()); + crypto_blake2b_update(&ctx, peer_public_key.data(), peer_public_key.size()); + break; + case X25519KeyExchangeSide::CLIENT: + crypto_blake2b_update(&ctx, peer_public_key.data(), peer_public_key.size()); + crypto_blake2b_update(&ctx, our_public_key.data(), our_public_key.size()); + break; + default: + NOT_REACHED(); + } + crypto_blake2b_update(&ctx, reinterpret_cast(extra_payload.data()), extra_payload.size()); + crypto_blake2b_final(&ctx, this->keys.data()); + return true; +} + +/** + * Encryption handler implementation for monocypther encryption after a X25519 key exchange. + */ +class X25519EncryptionHandler : public NetworkEncryptionHandler { +private: + crypto_aead_ctx context; ///< The actual encryption context. + +public: + /** + * Create the encryption handler. + * @param key The key used for the encryption. + * @param nonce The nonce used for the encryption. + */ + X25519EncryptionHandler(const std::span key, const X25519Nonce &nonce) + { + assert(key.size() == X25519_KEY_SIZE); + crypto_aead_init_x(&this->context, key.data(), nonce.data()); + } + + /** Ensure the encryption context is wiped! */ + ~X25519EncryptionHandler() + { + crypto_wipe(&this->context, sizeof(this->context)); + } + + size_t MACSize() const override + { + return X25519_MAC_SIZE; + } + + bool Decrypt(std::span mac, std::span message) override + { + return crypto_aead_read(&this->context, message.data(), mac.data(), nullptr, 0, message.data(), message.size()) == 0; + } + + void Encrypt(std::span mac, std::span message) override + { + crypto_aead_write(&this->context, message.data(), mac.data(), nullptr, 0, message.data(), message.size()); + } +}; + +/** Ensure the key does not get leaked when we're done with it. */ +X25519Key::~X25519Key() +{ + crypto_wipe(*this); +} + +/** + * Create a new secret key that's filled with random bytes. + * @return The randomly filled key. + */ +/* static */ X25519SecretKey X25519SecretKey::CreateRandom() +{ + X25519SecretKey secret_key; + RandomBytesWithFallback(secret_key); + return secret_key; +} + +/** + * Create the public key associated with this secret key. + * @return The public key. + */ +X25519PublicKey X25519SecretKey::CreatePublicKey() const +{ + X25519PublicKey public_key; + crypto_x25519_public_key(public_key.data(), this->data()); + return public_key; +} + +/** + * Create a new nonce that's filled with random bytes. + * @return The randomly filled nonce. + */ +/* static */ X25519Nonce X25519Nonce::CreateRandom() +{ + X25519Nonce nonce; + RandomBytesWithFallback(nonce); + return nonce; +} + +/** Ensure the nonce does not get leaked when we're done with it. */ +X25519Nonce::~X25519Nonce() +{ + crypto_wipe(*this); +} + +/** + * Create the handler, and generate the public keys accordingly. + * @param secret_key The secret key to use for this handler. Defaults to secure random data. + */ +X25519AuthenticationHandler::X25519AuthenticationHandler(const X25519SecretKey &secret_key) : + our_secret_key(secret_key), our_public_key(secret_key.CreatePublicKey()), nonce(X25519Nonce::CreateRandom()) +{ +} + +/* virtual */ void X25519AuthenticationHandler::SendRequest(Packet &p) +{ + p.Send_bytes(this->our_public_key); + p.Send_bytes(this->nonce); +} + +/** + * Read the key exchange data from a \c Packet that came from the server, + * @param p The packet that has been received. + * @return True when the data seems correct. + */ +bool X25519AuthenticationHandler::ReceiveRequest(Packet &p) +{ + if (p.RemainingBytesToTransfer() != X25519_KEY_SIZE + X25519_NONCE_SIZE) { + Debug(net, 1, "[crypto] Received auth response of illegal size; authentication aborted."); + return false; + } + + p.Recv_bytes(this->peer_public_key); + p.Recv_bytes(this->nonce); + return true; +} + +/** + * Perform the key exchange, and when that is correct fill the \c Packet with the appropriate data. + * @param p The packet that has to be sent. + * @param derived_key_extra_payload The extra payload to pass to the key exchange. + * @return Whether the key exchange was successful or not. + */ +bool X25519AuthenticationHandler::SendResponse(Packet &p, std::string_view derived_key_extra_payload) +{ + if (!this->derived_keys.Exchange(this->peer_public_key, X25519KeyExchangeSide::CLIENT, + this->our_secret_key, this->our_public_key, derived_key_extra_payload)) { + Debug(net, 0, "[crypto] Peer sent an illegal public key; authentication aborted."); + return false; + } + + X25519KeyExchangeMessage message; + RandomBytesWithFallback(message); + X25519Mac mac; + + crypto_aead_lock(message.data(), mac.data(), this->derived_keys.ClientToServer().data(), nonce.data(), + this->our_public_key.data(), this->our_public_key.size(), message.data(), message.size()); + + p.Send_bytes(this->our_public_key); + p.Send_bytes(mac); + p.Send_bytes(message); + return true; +} + +/** + * Get the public key the peer provided for the key exchange. + * @return The hexadecimal string representation of the peer's public key. + */ +std::string X25519AuthenticationHandler::GetPeerPublicKey() const +{ + return FormatArrayAsHex(this->peer_public_key); +} + +std::unique_ptr X25519AuthenticationHandler::CreateClientToServerEncryptionHandler() const +{ + return std::make_unique(this->derived_keys.ClientToServer(), this->nonce); +} + +std::unique_ptr X25519AuthenticationHandler::CreateServerToClientEncryptionHandler() const +{ + return std::make_unique(this->derived_keys.ServerToClient(), this->nonce); +} + +/** + * Read the key exchange data from a \c Packet that came from the client, and check whether the client passes the key + * exchange successfully. + * @param p The packet that has been received. + * @param derived_key_extra_payload The extra payload to pass to the key exchange. + * @return Whether the authentication was successful or not. + */ +NetworkAuthenticationServerHandler::ResponseResult X25519AuthenticationHandler::ReceiveResponse(Packet &p, std::string_view derived_key_extra_payload) +{ + if (p.RemainingBytesToTransfer() != X25519_KEY_SIZE + X25519_MAC_SIZE + X25519_KEY_EXCHANGE_MESSAGE_SIZE) { + Debug(net, 1, "[crypto] Received auth response of illegal size; authentication aborted."); + return NetworkAuthenticationServerHandler::NOT_AUTHENTICATED; + } + + X25519KeyExchangeMessage message{}; + X25519Mac mac; + + p.Recv_bytes(this->peer_public_key); + p.Recv_bytes(mac); + p.Recv_bytes(message); + + if (!this->derived_keys.Exchange(this->peer_public_key, X25519KeyExchangeSide::SERVER, + this->our_secret_key, this->our_public_key, derived_key_extra_payload)) { + Debug(net, 0, "[crypto] Peer sent an illegal public key; authentication aborted."); + return NetworkAuthenticationServerHandler::NOT_AUTHENTICATED; + } + + if (crypto_aead_unlock(message.data(), mac.data(), this->derived_keys.ClientToServer().data(), nonce.data(), + this->peer_public_key.data(), this->peer_public_key.size(), message.data(), message.size()) != 0) { + /* + * The ciphertext and the message authentication code do not match with the encryption key. + * This is most likely an invalid password, or possibly a bug in the client. + */ + return NetworkAuthenticationServerHandler::NOT_AUTHENTICATED; + } + + return NetworkAuthenticationServerHandler::AUTHENTICATED; +} + + +/* virtual */ NetworkAuthenticationClientHandler::RequestResult X25519PAKEClientHandler::ReceiveRequest(struct Packet &p) +{ + bool success = this->X25519AuthenticationHandler::ReceiveRequest(p); + if (!success) return NetworkAuthenticationClientHandler::INVALID; + + this->handler->AskUserForPassword(this->handler); + return NetworkAuthenticationClientHandler::AWAIT_USER_INPUT; +} + +/** + * Get the secret key from the given string. If that is not a valid secret key, reset it with a random one. + * Furthermore update the public key so it is always in sync with the private key. + * @param secret_key The secret key to read/validate/fix. + * @param public_key The public key to update. + * @return The valid secret key. + */ +/* static */ X25519SecretKey X25519AuthorizedKeyClientHandler::GetValidSecretKeyAndUpdatePublicKey(std::string &secret_key, std::string &public_key) +{ + X25519SecretKey key{}; + if (!ConvertHexToBytes(secret_key, key)) { + if (secret_key.empty()) { + Debug(net, 3, "[crypto] Creating a new random key"); + } else { + Debug(net, 0, "[crypto] Found invalid secret key, creating a new random key"); + } + key = X25519SecretKey::CreateRandom(); + secret_key = FormatArrayAsHex(key); + } + + public_key = FormatArrayAsHex(key.CreatePublicKey()); + return key; +} + +/* virtual */ NetworkAuthenticationServerHandler::ResponseResult X25519AuthorizedKeyServerHandler::ReceiveResponse(Packet &p) +{ + ResponseResult result = this->X25519AuthenticationHandler::ReceiveResponse(p, {}); + if (result != AUTHENTICATED) return result; + + std::string peer_public_key = this->GetPeerPublicKey(); + return this->authorized_key_handler->IsAllowed(peer_public_key) ? AUTHENTICATED : NOT_AUTHENTICATED; +} + + +/* virtual */ NetworkAuthenticationClientHandler::RequestResult CombinedAuthenticationClientHandler::ReceiveRequest(struct Packet &p) +{ + NetworkAuthenticationMethod method = static_cast(p.Recv_uint8()); + + auto is_of_method = [method](Handler &handler) { return handler->GetAuthenticationMethod() == method; }; + auto it = std::find_if(handlers.begin(), handlers.end(), is_of_method); + if (it == handlers.end()) return INVALID; + + this->current_handler = it->get(); + + Debug(net, 9, "Received {} authentication request", this->GetName()); + return this->current_handler->ReceiveRequest(p); +} + +/* virtual */ bool CombinedAuthenticationClientHandler::SendResponse(struct Packet &p) +{ + Debug(net, 9, "Sending {} authentication response", this->GetName()); + + return this->current_handler->SendResponse(p); +} + +/* virtual */ std::string_view CombinedAuthenticationClientHandler::GetName() const +{ + return this->current_handler != nullptr ? this->current_handler->GetName() : "Unknown"; +} + +/* virtual */ NetworkAuthenticationMethod CombinedAuthenticationClientHandler::GetAuthenticationMethod() const +{ + return this->current_handler != nullptr ? this->current_handler->GetAuthenticationMethod() : NETWORK_AUTH_METHOD_END; +} + + +/** + * Add the given sub-handler to this handler, if the handler can be used (e.g. there are authorized keys or there is a password). + * @param handler The handler to add. + */ +void CombinedAuthenticationServerHandler::Add(CombinedAuthenticationServerHandler::Handler &&handler) +{ + /* Is the handler configured correctly, e.g. does it have a password? */ + if (!handler->CanBeUsed()) return; + + this->handlers.push_back(std::move(handler)); +} + +/* virtual */ void CombinedAuthenticationServerHandler::SendRequest(struct Packet &p) +{ + Debug(net, 9, "Sending {} authentication request", this->GetName()); + + p.Send_uint8(this->handlers.back()->GetAuthenticationMethod()); + this->handlers.back()->SendRequest(p); +} + +/* virtual */ NetworkAuthenticationServerHandler::ResponseResult CombinedAuthenticationServerHandler::ReceiveResponse(struct Packet &p) +{ + Debug(net, 9, "Receiving {} authentication response", this->GetName()); + + ResponseResult result = this->handlers.back()->ReceiveResponse(p); + if (result != NOT_AUTHENTICATED) return result; + + this->handlers.pop_back(); + return this->CanBeUsed() ? RETRY_NEXT_METHOD : NOT_AUTHENTICATED; +} + +/* virtual */ std::string_view CombinedAuthenticationServerHandler::GetName() const +{ + return this->CanBeUsed() ? this->handlers.back()->GetName() : "Unknown"; +} + +/* virtual */ NetworkAuthenticationMethod CombinedAuthenticationServerHandler::GetAuthenticationMethod() const +{ + return this->CanBeUsed() ? this->handlers.back()->GetAuthenticationMethod() : NETWORK_AUTH_METHOD_END; +} + +/* virtual */ bool CombinedAuthenticationServerHandler::CanBeUsed() const +{ + return !this->handlers.empty(); +} + + +/* virtual */ void NetworkAuthenticationPasswordRequestHandler::Reply(const std::string &password) +{ + this->password = password; + this->SendResponse(); +} + +/* virtual */ bool NetworkAuthenticationDefaultAuthorizedKeyHandler::IsAllowed(std::string_view peer_public_key) const +{ + for (const auto &allowed : *this->authorized_keys) { + if (StrEqualsIgnoreCase(allowed, peer_public_key)) return true; + } + return false; +} + + +/** + * Create a NetworkAuthenticationClientHandler. + * @param password_handler The handler for when a request for password needs to be passed on to the user. + * @param secret_key The location where the secret key is stored; can be overwritten when invalid. + * @param public_key The location where the public key is stored; can be overwritten when invalid. + */ +/* static */ std::unique_ptr NetworkAuthenticationClientHandler::Create(std::shared_ptr password_handler, std::string &secret_key, std::string &public_key) +{ + auto secret = X25519AuthorizedKeyClientHandler::GetValidSecretKeyAndUpdatePublicKey(secret_key, public_key); + auto handler = std::make_unique(); + handler->Add(std::make_unique(secret)); + handler->Add(std::make_unique(secret, std::move(password_handler))); + handler->Add(std::make_unique(secret)); + return handler; +} + +/** + * Create a NetworkAuthenticationServerHandler. + * @param password_provider Callback to provide the password handling. Must remain valid until the authentication has succeeded or failed. Can be \c nullptr to skip password checks. + * @param authorized_key_handler Callback to provide the authorized key handling. Must remain valid until the authentication has succeeded or failed. Can be \c nullptr to skip authorized key checks. + * @param client_supported_method_mask Bitmask of the methods that are supported by the client. Defaults to support of all methods. + */ +std::unique_ptr NetworkAuthenticationServerHandler::Create(const NetworkAuthenticationPasswordProvider *password_provider, const NetworkAuthenticationAuthorizedKeyHandler *authorized_key_handler, NetworkAuthenticationMethodMask client_supported_method_mask) +{ + auto secret = X25519SecretKey::CreateRandom(); + auto handler = std::make_unique(); + if (password_provider != nullptr && HasBit(client_supported_method_mask, NETWORK_AUTH_METHOD_X25519_PAKE)) { + handler->Add(std::make_unique(secret, password_provider)); + } + + if (authorized_key_handler != nullptr && HasBit(client_supported_method_mask, NETWORK_AUTH_METHOD_X25519_AUTHORIZED_KEY)) { + handler->Add(std::make_unique(secret, authorized_key_handler)); + } + + if (!handler->CanBeUsed() && HasBit(client_supported_method_mask, NETWORK_AUTH_METHOD_X25519_KEY_EXCHANGE_ONLY)) { + /* Fall back to the plain handler when neither password, nor authorized keys are configured. */ + handler->Add(std::make_unique(secret)); + } + return handler; +} diff --git a/src/network/network_crypto.h b/src/network/network_crypto.h new file mode 100644 index 0000000000..60e2b6b71b --- /dev/null +++ b/src/network/network_crypto.h @@ -0,0 +1,287 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** + * @file network_crypto.h Crypto specific bits of the network handling. + * + * This provides a set of functionality to perform authentication combined with a key exchange, + * to create a shared secret as well as encryption using those shared secrets. + * + * For the authentication/key exchange, the server determines the available methods and creates + * the appropriate \c NetworkAuthenticationServerHandler. This will be used to create a request + * for the client, which instantiates a \c NetworkAuthenticationClientHandler to handle that + * request. + * At the moment there are three types of request: key exchange only, password-authenticated key + * exchange (PAKE) and authorized keys. When the request is for a password, the user is asked + * for the password via an essentially asynchronous callback from the client handler. For the + * other requests no input from the user is needed, and these are immediately ready to generate + * the response for the server. + * + * The server will validate the response resulting in either the user being authenticated or not. + * When the user failed authentication, there might be a possibility to retry. For example when + * the server has configured authorized keys and passwords; when the client fails with the + * authorized keys, it will retry with the password. + * + * Once the key exchange/authentication has been done, the server can signal the client to + * upgrade the network connection to use encryption using the shared secret of the key exchange. + */ + +#ifndef NETWORK_CRYPTO_H +#define NETWORK_CRYPTO_H + +/** + * Base class for handling the encryption (or decryption) of a network connection. + */ +class NetworkEncryptionHandler { +public: + virtual ~NetworkEncryptionHandler() {} + + /** + * Get the size of the MAC (Message Authentication Code) used by the underlying encryption protocol. + * @return The size, in bytes, of the MACs. + */ + virtual size_t MACSize() const = 0; + + /** + * Decrypt the given message in-place, validating against the given MAC. + * @param mac The message authentication code (MAC). + * @param message The location of the message to decrypt. + * @return Whether decryption and authentication/validation of the message succeeded. + */ + virtual bool Decrypt(std::span mac, std::span message) = 0; + + /** + * Encrypt the given message in-place, and write the associated MAC. + * @param mac The location to write the message authentication code (MAC) to. + * @param message The location of the message to encrypt. + */ + virtual void Encrypt(std::span mac, std::span message) = 0; +}; + + +/** + * Callback interface for requests for passwords in the context of network authentication. + */ +class NetworkAuthenticationPasswordRequest { +public: + virtual ~NetworkAuthenticationPasswordRequest() {} + + /** + * Reply to the request with the given password. + */ + virtual void Reply(const std::string &password) = 0; +}; + +/** + * Callback interface for client implementations to provide the handling of the password requests. + */ +class NetworkAuthenticationPasswordRequestHandler : public NetworkAuthenticationPasswordRequest { +protected: + friend class X25519PAKEClientHandler; + + std::string password; ///< The entered password. +public: + + virtual void Reply(const std::string &password) override; + + /** + * Callback to trigger sending the response for the password request. + */ + virtual void SendResponse() = 0; + + /** + * Callback to trigger asking the user for the password. + * @param request The request to the user, to which it can reply with the password. + */ + virtual void AskUserForPassword(std::shared_ptr request) = 0; +}; + + +/** + * Callback interface for server implementations to provide the current password. + */ +class NetworkAuthenticationPasswordProvider { +public: + virtual ~NetworkAuthenticationPasswordProvider() {} + + /** + * Callback to return the password where to validate against. + * @return \c std::string_view of the current password; an empty view means no password check will be performed. + */ + virtual std::string_view GetPassword() const = 0; +}; + +/** + * Default implementation of the password provider. + */ +class NetworkAuthenticationDefaultPasswordProvider : public NetworkAuthenticationPasswordProvider { +private: + const std::string *password; ///< The password to check against. +public: + /** + * Create the provider with the pointer to the password that is to be used. A pointer, so this can handle + * situations where the password gets changed over time. + * @param password The reference to the configured password. + */ + NetworkAuthenticationDefaultPasswordProvider(const std::string &password) : password(&password) {} + + std::string_view GetPassword() const override { return *this->password; }; +}; + +/** + * Callback interface for server implementations to provide the authorized key validation. + */ +class NetworkAuthenticationAuthorizedKeyHandler { +public: + virtual ~NetworkAuthenticationAuthorizedKeyHandler() {} + + /** + * Check whether the key handler can be used, i.e. whether there are authorized keys to check against. + * @return \c true when it can be used, otherwise \c false. + */ + virtual bool CanBeUsed() const = 0; + + /** + * Check whether the given public key of the peer is allowed in. + * @param peer_public_key The public key of the peer to check against. + * @return \c true when the key is allowed, otherwise \c false. + */ + virtual bool IsAllowed(std::string_view peer_public_key) const = 0; +}; + +/** + * Default implementation for the authorized key handler. + */ +class NetworkAuthenticationDefaultAuthorizedKeyHandler : public NetworkAuthenticationAuthorizedKeyHandler { +private: + const std::vector *authorized_keys; ///< The authorized keys to check against. +public: + /** + * Create the handler that uses the given authorized keys to check against. + * @param authorized_keys The reference to the authorized keys to check against. + */ + NetworkAuthenticationDefaultAuthorizedKeyHandler(const std::vector &authorized_keys) : authorized_keys(&authorized_keys) {} + + bool CanBeUsed() const override { return !this->authorized_keys->empty(); } + bool IsAllowed(std::string_view peer_public_key) const override; +}; + + +/** 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_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) +}; + +/** The mask of authentication methods that can be used. */ +using NetworkAuthenticationMethodMask = uint16_t; + +/** + * Base class for cryptographic authentication handlers. + */ +class NetworkAuthenticationHandler { +public: + virtual ~NetworkAuthenticationHandler() {} + + /** + * Get the name of the handler for debug messages. + * @return The name of the handler. + */ + virtual std::string_view GetName() const = 0; + + /** + * Get the method this handler is providing functionality for. + * @return The \c NetworkAuthenticationMethod. + */ + virtual NetworkAuthenticationMethod GetAuthenticationMethod() const = 0; + + /** + * Create a \a NetworkEncryptionHandler to encrypt or decrypt messages from the client to the server. + * @return The handler for the client to server encryption. + */ + virtual std::unique_ptr CreateClientToServerEncryptionHandler() const = 0; + + /** + * Create a \a NetworkEncryptionHandler to encrypt or decrypt messages from the server to the client. + * @return The handler for the server to client encryption. + */ + virtual std::unique_ptr CreateServerToClientEncryptionHandler() const = 0; +}; + +/** + * Base class for client side cryptographic authentication handlers. + */ +class NetworkAuthenticationClientHandler : public NetworkAuthenticationHandler { +public: + /** The processing result of receiving a request. */ + enum RequestResult { + AWAIT_USER_INPUT, ///< We have requested some user input, but must wait on that. + READY_FOR_RESPONSE, ///< We do not have to wait for user input, and can immediately respond to the server. + INVALID, ///< We have received an invalid request. + }; + + /** + * Read a request from the server. + * @param p The packet to read the request from. + * @return True when valid, otherwise false. + */ + virtual RequestResult ReceiveRequest(struct Packet &p) = 0; + + /** + * Create the response to send to the server. + * @param p The packet to write the response from. + * @return True when a valid packet was made, otherwise false. + */ + virtual bool SendResponse(struct Packet &p) = 0; + + static std::unique_ptr Create(std::shared_ptr password_handler, std::string &secret_key, std::string &public_key); +}; + +/** + * Base class for server side cryptographic authentication handlers. + */ +class NetworkAuthenticationServerHandler : public NetworkAuthenticationHandler { +public: + /** The processing result of receiving a response. */ + enum ResponseResult { + AUTHENTICATED, ///< The client was authenticated successfully. + NOT_AUTHENTICATED, ///< All authentications for this handler have been exhausted. + RETRY_NEXT_METHOD, ///< The client failed to authenticate, but there is another method to try. + }; + + /** + * Create the request to send to the client. + * @param p The packet to write the request to. + */ + virtual void SendRequest(struct Packet &p) = 0; + + /** + * Read the response from the client. + * @param p The packet to read the response from. + * @return The \c ResponseResult describing the result. + */ + virtual ResponseResult ReceiveResponse(struct Packet &p) = 0; + + /** + * Checks whether this handler can be used with the current configuration. + * For example when there is no password, the handler cannot be used. + * @return True when this handler can be used. + */ + virtual bool CanBeUsed() const = 0; + + /** + * Get the public key the peer provided during the authentication. + * @return The hexadecimal string representation of the peer's public key. + */ + virtual std::string GetPeerPublicKey() const = 0; + + static std::unique_ptr Create(const NetworkAuthenticationPasswordProvider *password_provider, const NetworkAuthenticationAuthorizedKeyHandler *authorized_key_handler, NetworkAuthenticationMethodMask client_supported_method_mask = ~static_cast(0)); +}; + +#endif /* NETWORK_CRYPTO_H */ diff --git a/src/network/network_crypto_internal.h b/src/network/network_crypto_internal.h new file mode 100644 index 0000000000..a073e0931b --- /dev/null +++ b/src/network/network_crypto_internal.h @@ -0,0 +1,341 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file network_crypto_internal.h Internal bits to the crypto of the network handling. */ + +#ifndef NETWORK_CRYPTO_INTERNAL_H +#define NETWORK_CRYPTO_INTERNAL_H + +#include "network_crypto.h" + +/** The number of bytes the public and secret keys are in X25519. */ +constexpr size_t X25519_KEY_SIZE = 32; +/** The number of bytes the nonces are in X25519. */ +constexpr size_t X25519_NONCE_SIZE = 24; +/** The number of bytes the message authentication codes are in X25519. */ +constexpr size_t X25519_MAC_SIZE = 16; +/** The number of bytes the (random) payload of the authentication message has. */ +constexpr size_t X25519_KEY_EXCHANGE_MESSAGE_SIZE = 8; + +/** Container for a X25519 key that is automatically crypto-wiped when destructed. */ +struct X25519Key : std::array { + ~X25519Key(); +}; + +/** Container for a X25519 public key. */ +struct X25519PublicKey : X25519Key { +}; + +/** Container for a X25519 secret key. */ +struct X25519SecretKey : X25519Key { + static X25519SecretKey CreateRandom(); + X25519PublicKey CreatePublicKey() const; +}; + +/** Container for a X25519 nonce that is automatically crypto-wiped when destructed. */ +struct X25519Nonce : std::array { + static X25519Nonce CreateRandom(); + ~X25519Nonce(); +}; + +/** Container for a X25519 message authentication code. */ +using X25519Mac = std::array; + +/** Container for a X25519 key exchange message. */ +using X25519KeyExchangeMessage = std::array; + +/** The side of the key exchange. */ +enum class X25519KeyExchangeSide { + CLIENT, ///< We are the client. + SERVER, ///< We are the server. +}; + +/** + * Container for the keys that derived from the X25519 key exchange mechanism. This mechanism derives + * a key to encrypt both the client-to-server and a key to encrypt server-to-client communication. + */ +class X25519DerivedKeys { +private: + /** Single contiguous buffer to store the derived keys in, as they are generated as a single hash. */ + std::array keys; +public: + ~X25519DerivedKeys(); + std::span ClientToServer() const; + std::span ServerToClient() const; + bool Exchange(const X25519PublicKey &peer_public_key, X25519KeyExchangeSide side, + const X25519SecretKey &our_secret_key, const X25519PublicKey &our_public_key, std::string_view extra_payload); +}; + +/** + * Base for handlers using a X25519 key exchange to perform authentication. + * + * In general this works as follows: + * 1) the client and server have or generate a secret and public X25519 key. + * 2) the X25519 key exchange is performed at both the client and server, with their own secret key and their peer's public key. + * 3) a pair of derived keys is created by BLAKE2b-hashing the following into 64 bytes, in this particular order: + * - the shared secret from the key exchange; + * - the public key of the server; + * - the public key of the client; + * - optional extra payload, e.g. a password in the case of PAKE. + * The first of the pair of derived keys is usually used to encrypt client-to-server communication, and the second of the pair + * is usually used to encrypt server-to-client communication. + * 4) a XChaCha20-Poly1305 (authenticated) encryption is performed using: + * - the first of the pair of derived keys as encryption key; + * - a 24 byte nonce; + * - the public key of the client as additional authenticated data. + * - a 8 byte random number as content/message. + * + * The server initiates the request by sending its public key and a 24 byte nonce that is randomly generated. Normally the side + * that sends the encrypted data sends the nonce in their packet, which would be the client on our case. However, there are + * many implementations of clients due to the admin-protocol where this is used, and we cannot guarantee that they generate a + * good enough nonce. As such the server sends one instead. The server will create a new set of keys for each session. + * + * The client receives the request, performs the key exchange, generates the derived keys and then encrypts the message. This + * message must contain some content, so it has to be filled with 8 random bytes. Once the message has been encrypted, the + * client sends their public key, the encrypted message and the message authentication code (MAC) to the server in a response. + * + * The server receives the response, performs the key exchange, generates the derived keys, decrypts the message and validates the + * message authentication code, and finally the message. It is up to the sub class to perform the final authentication checks. + */ +class X25519AuthenticationHandler { +private: + X25519SecretKey our_secret_key; ///< The secret key used by us. + X25519PublicKey our_public_key; ///< The public key used by us. + X25519Nonce nonce; ///< The nonce to prevent replay attacks. + X25519DerivedKeys derived_keys; ///< Keys derived from the authentication process. + X25519PublicKey peer_public_key; ///< The public key used by our peer. + +protected: + X25519AuthenticationHandler(const X25519SecretKey &secret_key); + + void SendRequest(struct Packet &p); + bool ReceiveRequest(struct Packet &p); + bool SendResponse(struct Packet &p, std::string_view derived_key_extra_payload); + NetworkAuthenticationServerHandler::ResponseResult ReceiveResponse(struct Packet &p, std::string_view derived_key_extra_payload); + + std::string GetPeerPublicKey() const; + + std::unique_ptr CreateClientToServerEncryptionHandler() const; + std::unique_ptr CreateServerToClientEncryptionHandler() const; +}; + +/** + * Client side handler for using X25519 without actual authentication. + * + * This follows the method described in \c X25519AuthenticationHandler, without an extra payload. + */ +class X25519KeyExchangeOnlyClientHandler : protected X25519AuthenticationHandler, public NetworkAuthenticationClientHandler { +public: + /** + * Create the handler that that one does the key exchange. + * @param secret_key The secret key to initialize this handler with. + */ + X25519KeyExchangeOnlyClientHandler(const X25519SecretKey &secret_key) : X25519AuthenticationHandler(secret_key) {} + + virtual RequestResult ReceiveRequest(struct Packet &p) override { return this->X25519AuthenticationHandler::ReceiveRequest(p) ? READY_FOR_RESPONSE : INVALID; } + virtual bool SendResponse(struct Packet &p) override { return this->X25519AuthenticationHandler::SendResponse(p, {}); } + + virtual std::string_view GetName() const override { return "X25519-KeyExchangeOnly-client"; } + virtual NetworkAuthenticationMethod GetAuthenticationMethod() const override { return NETWORK_AUTH_METHOD_X25519_KEY_EXCHANGE_ONLY; } + + virtual std::unique_ptr CreateClientToServerEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateClientToServerEncryptionHandler(); } + virtual std::unique_ptr CreateServerToClientEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateServerToClientEncryptionHandler(); } +}; + +/** + * Server side handler for using X25519 without actual authentication. + * + * This follows the method described in \c X25519AuthenticationHandler, without an extra payload. + */ +class X25519KeyExchangeOnlyServerHandler : protected X25519AuthenticationHandler, public NetworkAuthenticationServerHandler { +public: + /** + * Create the handler that that one does the key exchange. + * @param secret_key The secret key to initialize this handler with. + */ + X25519KeyExchangeOnlyServerHandler(const X25519SecretKey &secret_key) : X25519AuthenticationHandler(secret_key) {} + + virtual void SendRequest(struct Packet &p) override { this->X25519AuthenticationHandler::SendRequest(p); } + virtual ResponseResult ReceiveResponse(struct Packet &p) override { return this->X25519AuthenticationHandler::ReceiveResponse(p, {}); } + + virtual std::string_view GetName() const override { return "X25519-KeyExchangeOnly-server"; } + virtual NetworkAuthenticationMethod GetAuthenticationMethod() const override { return NETWORK_AUTH_METHOD_X25519_KEY_EXCHANGE_ONLY; } + virtual bool CanBeUsed() const override { return true; } + + virtual std::string GetPeerPublicKey() const override { return this->X25519AuthenticationHandler::GetPeerPublicKey(); } + virtual std::unique_ptr CreateClientToServerEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateClientToServerEncryptionHandler(); } + virtual std::unique_ptr CreateServerToClientEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateServerToClientEncryptionHandler(); } +}; + +/** + * Client side handler for using X25519 with a password-authenticated key exchange. + * + * This follows the method described in \c X25519AuthenticationHandler, were the password is the extra payload. + */ +class X25519PAKEClientHandler : protected X25519AuthenticationHandler, public NetworkAuthenticationClientHandler { +private: + std::shared_ptr handler; + +public: + /** + * Create the handler with the given password handler. + * @param secret_key The secret key to initialize this handler with. + * @param handler The handler requesting the password from the user, if required. + */ + X25519PAKEClientHandler(const X25519SecretKey &secret_key, std::shared_ptr handler) : X25519AuthenticationHandler(secret_key), handler(handler) {} + + virtual RequestResult ReceiveRequest(struct Packet &p) override; + virtual bool SendResponse(struct Packet &p) override { return this->X25519AuthenticationHandler::SendResponse(p, this->handler->password); } + + virtual std::string_view GetName() const override { return "X25519-PAKE-client"; } + virtual NetworkAuthenticationMethod GetAuthenticationMethod() const override { return NETWORK_AUTH_METHOD_X25519_PAKE; } + + virtual std::unique_ptr CreateClientToServerEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateClientToServerEncryptionHandler(); } + virtual std::unique_ptr CreateServerToClientEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateServerToClientEncryptionHandler(); } +}; + +/** + * Server side handler for using X25519 with a password-authenticated key exchange. + * + * This follows the method described in \c X25519AuthenticationHandler, were the password is the extra payload. + */ +class X25519PAKEServerHandler : protected X25519AuthenticationHandler, public NetworkAuthenticationServerHandler { +private: + const NetworkAuthenticationPasswordProvider *password_provider; ///< The password to check against. +public: + /** + * Create the handler with the given password provider. + * @param secret_key The secret key to initialize this handler with. + * @param password_provider The provider for the passwords. + */ + X25519PAKEServerHandler(const X25519SecretKey &secret_key, const NetworkAuthenticationPasswordProvider *password_provider) : X25519AuthenticationHandler(secret_key), password_provider(password_provider) {} + + virtual void SendRequest(struct Packet &p) override { this->X25519AuthenticationHandler::SendRequest(p); } + virtual ResponseResult ReceiveResponse(struct Packet &p) override { return this->X25519AuthenticationHandler::ReceiveResponse(p, this->password_provider->GetPassword()); } + + virtual std::string_view GetName() const override { return "X25519-PAKE-server"; } + virtual NetworkAuthenticationMethod GetAuthenticationMethod() const override { return NETWORK_AUTH_METHOD_X25519_PAKE; } + virtual bool CanBeUsed() const override { return !this->password_provider->GetPassword().empty(); } + + virtual std::string GetPeerPublicKey() const override { return this->X25519AuthenticationHandler::GetPeerPublicKey(); } + virtual std::unique_ptr CreateClientToServerEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateClientToServerEncryptionHandler(); } + virtual std::unique_ptr CreateServerToClientEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateServerToClientEncryptionHandler(); } +}; + + +/** + * Handler for clients using a X25519 key exchange to perform authentication via a set of authorized (public) keys of clients. + * + * This follows the method described in \c X25519AuthenticationHandler. Once all these checks have succeeded, it will + * check whether the public key of the client is in the list of authorized keys to login. + */ +class X25519AuthorizedKeyClientHandler : protected X25519AuthenticationHandler, public NetworkAuthenticationClientHandler { +public: + /** + * Create the handler that uses the given password to check against. + * @param secret_key The secret key to initialize this handler with. + */ + X25519AuthorizedKeyClientHandler(const X25519SecretKey &secret_key) : X25519AuthenticationHandler(secret_key) {} + + virtual RequestResult ReceiveRequest(struct Packet &p) override { return this->X25519AuthenticationHandler::ReceiveRequest(p) ? READY_FOR_RESPONSE : INVALID; } + virtual bool SendResponse(struct Packet &p) override { return this->X25519AuthenticationHandler::SendResponse(p, {}); } + + virtual std::string_view GetName() const override { return "X25519-AuthorizedKey-client"; } + virtual NetworkAuthenticationMethod GetAuthenticationMethod() const override { return NETWORK_AUTH_METHOD_X25519_AUTHORIZED_KEY; } + + virtual std::unique_ptr CreateClientToServerEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateClientToServerEncryptionHandler(); } + virtual std::unique_ptr CreateServerToClientEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateServerToClientEncryptionHandler(); } + + static X25519SecretKey GetValidSecretKeyAndUpdatePublicKey(std::string &secret_key, std::string &public_key); +}; + +/** + * Handler for servers using a X25519 key exchange to perform authentication via a set of authorized (public) keys of clients. + * + * This follows the method described in \c X25519AuthenticationHandler. Once all these checks have succeeded, it will + * check whether the public key of the client is in the list of authorized keys to login. + */ +class X25519AuthorizedKeyServerHandler : protected X25519AuthenticationHandler, public NetworkAuthenticationServerHandler { +private: + const NetworkAuthenticationAuthorizedKeyHandler *authorized_key_handler; ///< The handler of the authorized keys. +public: + /** + * Create the handler that uses the given authorized keys to check against. + * @param secret_key The secret key to initialize this handler with. + * @param authorized_key_handler The handler of the authorized keys. + */ + X25519AuthorizedKeyServerHandler(const X25519SecretKey &secret_key, const NetworkAuthenticationAuthorizedKeyHandler *authorized_key_handler) : X25519AuthenticationHandler(secret_key), authorized_key_handler(authorized_key_handler) {} + + virtual void SendRequest(struct Packet &p) override { this->X25519AuthenticationHandler::SendRequest(p); } + virtual ResponseResult ReceiveResponse(struct Packet &p) override; + + virtual std::string_view GetName() const override { return "X25519-AuthorizedKey-server"; } + virtual NetworkAuthenticationMethod GetAuthenticationMethod() const override { return NETWORK_AUTH_METHOD_X25519_AUTHORIZED_KEY; } + virtual bool CanBeUsed() const override { return this->authorized_key_handler->CanBeUsed(); } + + virtual std::string GetPeerPublicKey() const override { return this->X25519AuthenticationHandler::GetPeerPublicKey(); } + virtual std::unique_ptr CreateClientToServerEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateClientToServerEncryptionHandler(); } + virtual std::unique_ptr CreateServerToClientEncryptionHandler() const override { return this->X25519AuthenticationHandler::CreateServerToClientEncryptionHandler(); } +}; + + +/** + * Handler for combining a number of authentication handlers, where the failure of one of the handlers will retry with + * another handler. For example when authorized keys fail, it can still fall back to a password. + */ +class CombinedAuthenticationClientHandler : public NetworkAuthenticationClientHandler { +public: + using Handler = std::unique_ptr; ///< The type of the inner handlers. + +private: + std::vector handlers; ///< The handlers that we can authenticate with. + NetworkAuthenticationClientHandler *current_handler = nullptr; ///< The currently active handler. + +public: + /** + * Add the given sub-handler to this handler. + * @param handler The handler to add. + */ + void Add(Handler &&handler) { this->handlers.push_back(std::move(handler)); } + + virtual RequestResult ReceiveRequest(struct Packet &p) override; + virtual bool SendResponse(struct Packet &p) override; + + virtual std::string_view GetName() const override; + virtual NetworkAuthenticationMethod GetAuthenticationMethod() const override; + + virtual std::unique_ptr CreateClientToServerEncryptionHandler() const override { return this->current_handler->CreateClientToServerEncryptionHandler(); } + virtual std::unique_ptr CreateServerToClientEncryptionHandler() const override { return this->current_handler->CreateServerToClientEncryptionHandler(); } +}; + +/** + * Handler for combining a number of authentication handlers, where the failure of one of the handlers will retry with + * another handler. For example when authorized keys fail, it can still fall back to a password. + */ +class CombinedAuthenticationServerHandler : public NetworkAuthenticationServerHandler { +public: + using Handler = std::unique_ptr; ///< The type of the inner handlers. + +private: + std::vector handlers; ///< The handlers that we can (still) authenticate with. + +public: + void Add(Handler &&handler); + + virtual void SendRequest(struct Packet &p) override; + virtual ResponseResult ReceiveResponse(struct Packet &p) override; + + virtual std::string_view GetName() const override; + virtual NetworkAuthenticationMethod GetAuthenticationMethod() const override; + virtual bool CanBeUsed() const override; + + virtual std::string GetPeerPublicKey() const override { return this->handlers.back()->GetPeerPublicKey(); } + virtual std::unique_ptr CreateClientToServerEncryptionHandler() const override { return this->handlers.back()->CreateClientToServerEncryptionHandler(); } + virtual std::unique_ptr CreateServerToClientEncryptionHandler() const override { return this->handlers.back()->CreateServerToClientEncryptionHandler(); } +}; + +#endif /* NETWORK_CRYPTO_INTERNAL_H */ diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 53884be7d7..475174379d 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -9,6 +9,7 @@ add_test_files( string_func.cpp strings_func.cpp test_main.cpp + test_network_crypto.cpp test_script_admin.cpp test_window_desc.cpp ) diff --git a/src/tests/test_network_crypto.cpp b/src/tests/test_network_crypto.cpp new file mode 100644 index 0000000000..0438a6ca65 --- /dev/null +++ b/src/tests/test_network_crypto.cpp @@ -0,0 +1,199 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file test_network_crypto.cpp Tests for network related crypto functions. */ + +#include "../stdafx.h" + +#include "../3rdparty/catch2/catch.hpp" + +#include "../core/format.hpp" +#include "../network/network_crypto_internal.h" +#include "../network/core/packet.h" +#include "../string_func.h" + +class MockNetworkSocketHandler : public NetworkSocketHandler { +}; + +static MockNetworkSocketHandler mock_socket_handler; + +static Packet CreatePacketForReading(Packet &source) +{ + source.PrepareToSend(); + + Packet dest(&mock_socket_handler, COMPAT_MTU, source.Size()); + + auto transfer_in = [](Packet &source, char *dest_data, size_t length) { + auto transfer_out = [](char *dest_data, const char *source_data, size_t length) { + std::copy(source_data, source_data + length, dest_data); + return length; + }; + return source.TransferOutWithLimit(transfer_out, length, dest_data); + }; + dest.TransferIn(transfer_in, source); + + dest.PrepareToRead(); + dest.Recv_uint8(); // Ignore the type + return dest; +} + +class TestPasswordRequestHandler : public NetworkAuthenticationPasswordRequestHandler { +private: + std::string password; +public: + TestPasswordRequestHandler(std::string &password) : password(password) {} + void SendResponse() override {} + void AskUserForPassword(std::shared_ptr request) override { request->Reply(this->password); } +}; + +static void TestAuthentication(NetworkAuthenticationServerHandler &server, NetworkAuthenticationClientHandler &client, + NetworkAuthenticationServerHandler::ResponseResult expected_response_result, + NetworkAuthenticationClientHandler::RequestResult expected_request_result) +{ + Packet request(&mock_socket_handler, PacketType{}); + server.SendRequest(request); + + request = CreatePacketForReading(request); + CHECK(client.ReceiveRequest(request) == expected_request_result); + + Packet response(&mock_socket_handler, PacketType{}); + client.SendResponse(response); + + response = CreatePacketForReading(response); + CHECK(server.ReceiveResponse(response) == expected_response_result); +} + + +TEST_CASE("Authentication_KeyExchangeOnly") +{ + X25519KeyExchangeOnlyServerHandler server(X25519SecretKey::CreateRandom()); + X25519KeyExchangeOnlyClientHandler client(X25519SecretKey::CreateRandom()); + + TestAuthentication(server, client, NetworkAuthenticationServerHandler::AUTHENTICATED, NetworkAuthenticationClientHandler::READY_FOR_RESPONSE); +} + + +static void TestAuthenticationPAKE(std::string server_password, std::string client_password, + NetworkAuthenticationServerHandler::ResponseResult expected_response_result) +{ + NetworkAuthenticationDefaultPasswordProvider server_password_provider(server_password); + X25519PAKEServerHandler server(X25519SecretKey::CreateRandom(), &server_password_provider); + X25519PAKEClientHandler client(X25519SecretKey::CreateRandom(), std::make_shared(client_password)); + + TestAuthentication(server, client, expected_response_result, NetworkAuthenticationClientHandler::AWAIT_USER_INPUT); +} + +TEST_CASE("Authentication_PAKE") +{ + SECTION("Correct password") { + TestAuthenticationPAKE("sikrit", "sikrit", NetworkAuthenticationServerHandler::AUTHENTICATED); + } + + SECTION("Empty password") { + TestAuthenticationPAKE("", "", NetworkAuthenticationServerHandler::AUTHENTICATED); + } + + SECTION("Wrong password") { + TestAuthenticationPAKE("sikrit", "secret", NetworkAuthenticationServerHandler::NOT_AUTHENTICATED); + } +} + + +static void TestAuthenticationAuthorizedKey(const X25519SecretKey &client_secret_key, const X25519PublicKey &server_expected_public_key, + NetworkAuthenticationServerHandler::ResponseResult expected_response_result) +{ + std::vector authorized_keys; + authorized_keys.emplace_back(FormatArrayAsHex(server_expected_public_key)); + + NetworkAuthenticationDefaultAuthorizedKeyHandler authorized_key_handler(authorized_keys); + X25519AuthorizedKeyServerHandler server(X25519SecretKey::CreateRandom(), &authorized_key_handler); + X25519AuthorizedKeyClientHandler client(client_secret_key); + + TestAuthentication(server, client, expected_response_result, NetworkAuthenticationClientHandler::READY_FOR_RESPONSE); +} + +TEST_CASE("Authentication_AuthorizedKey") +{ + auto client_secret_key = X25519SecretKey::CreateRandom(); + auto valid_client_public_key = client_secret_key.CreatePublicKey(); + auto invalid_client_public_key = X25519SecretKey::CreateRandom().CreatePublicKey(); + + SECTION("Correct public key") { + TestAuthenticationAuthorizedKey(client_secret_key, valid_client_public_key, NetworkAuthenticationServerHandler::AUTHENTICATED); + } + + SECTION("Incorrect public key") { + TestAuthenticationAuthorizedKey(client_secret_key, invalid_client_public_key, NetworkAuthenticationServerHandler::NOT_AUTHENTICATED); + } +} + + +TEST_CASE("Authentication_Combined") +{ + auto client_secret_key = X25519SecretKey::CreateRandom(); + std::string client_secret_key_str = FormatArrayAsHex(client_secret_key); + auto client_public_key = client_secret_key.CreatePublicKey(); + std::string client_public_key_str = FormatArrayAsHex(client_public_key); + + std::vector valid_authorized_keys; + valid_authorized_keys.emplace_back(client_public_key_str); + NetworkAuthenticationDefaultAuthorizedKeyHandler valid_authorized_key_handler(valid_authorized_keys); + + std::vector invalid_authorized_keys; + invalid_authorized_keys.emplace_back("not-a-valid-authorized-key"); + NetworkAuthenticationDefaultAuthorizedKeyHandler invalid_authorized_key_handler(invalid_authorized_keys); + + std::vector no_authorized_keys; + NetworkAuthenticationDefaultAuthorizedKeyHandler no_authorized_key_handler(no_authorized_keys); + + std::string no_password = ""; + NetworkAuthenticationDefaultPasswordProvider no_password_provider(no_password); + std::string valid_password = "sikrit"; + NetworkAuthenticationDefaultPasswordProvider valid_password_provider(valid_password); + std::string invalid_password = "secret"; + NetworkAuthenticationDefaultPasswordProvider invalid_password_provider(invalid_password); + + auto client = NetworkAuthenticationClientHandler::Create(std::make_shared(valid_password), client_secret_key_str, client_public_key_str); + + SECTION("Invalid authorized keys, invalid password") { + auto server = NetworkAuthenticationServerHandler::Create(&invalid_password_provider, &invalid_authorized_key_handler); + + TestAuthentication(*server, *client, NetworkAuthenticationServerHandler::RETRY_NEXT_METHOD, NetworkAuthenticationClientHandler::READY_FOR_RESPONSE); + TestAuthentication(*server, *client, NetworkAuthenticationServerHandler::NOT_AUTHENTICATED, NetworkAuthenticationClientHandler::AWAIT_USER_INPUT); + } + + SECTION("Invalid authorized keys, valid password") { + auto server = NetworkAuthenticationServerHandler::Create(&valid_password_provider, &invalid_authorized_key_handler); + + TestAuthentication(*server, *client, NetworkAuthenticationServerHandler::RETRY_NEXT_METHOD, NetworkAuthenticationClientHandler::READY_FOR_RESPONSE); + TestAuthentication(*server, *client, NetworkAuthenticationServerHandler::AUTHENTICATED, NetworkAuthenticationClientHandler::AWAIT_USER_INPUT); + } + + SECTION("Valid authorized keys, valid password") { + auto server = NetworkAuthenticationServerHandler::Create(&valid_password_provider, &valid_authorized_key_handler); + + TestAuthentication(*server, *client, NetworkAuthenticationServerHandler::AUTHENTICATED, NetworkAuthenticationClientHandler::READY_FOR_RESPONSE); + } + + SECTION("No authorized keys, invalid password") { + auto server = NetworkAuthenticationServerHandler::Create(&invalid_password_provider, &no_authorized_key_handler); + + TestAuthentication(*server, *client, NetworkAuthenticationServerHandler::NOT_AUTHENTICATED, NetworkAuthenticationClientHandler::AWAIT_USER_INPUT); + } + + SECTION("No authorized keys, valid password") { + auto server = NetworkAuthenticationServerHandler::Create(&valid_password_provider, &no_authorized_key_handler); + + TestAuthentication(*server, *client, NetworkAuthenticationServerHandler::AUTHENTICATED, NetworkAuthenticationClientHandler::AWAIT_USER_INPUT); + } + + SECTION("No authorized keys, no password") { + auto server = NetworkAuthenticationServerHandler::Create(&no_password_provider, &no_authorized_key_handler); + + TestAuthentication(*server, *client, NetworkAuthenticationServerHandler::AUTHENTICATED, NetworkAuthenticationClientHandler::READY_FOR_RESPONSE); + } +} From dd532cbc77a70c49078522827052931daee7a5e8 Mon Sep 17 00:00:00 2001 From: Rubidium Date: Fri, 15 Mar 2024 22:36:58 +0100 Subject: [PATCH 2/7] Codechange: add setting for authorized/secret/public keys --- src/network/core/config.h | 6 ++++++ src/settings.cpp | 2 ++ src/settings_type.h | 3 +++ .../settings/network_secrets_settings.ini | 18 ++++++++++++++++++ src/tests/test_network_crypto.cpp | 3 +++ 5 files changed, 32 insertions(+) diff --git a/src/network/core/config.h b/src/network/core/config.h index b03f96cdaa..e74cfb2fff 100644 --- a/src/network/core/config.h +++ b/src/network/core/config.h @@ -91,4 +91,10 @@ static const uint NETWORK_GRF_NAME_LENGTH = 80; ///< Maxim */ static const uint NETWORK_MAX_GRF_COUNT = 255; +/** + * The maximum length of the hexadecimal encoded secret keys, in bytes including '\0'. + * This is related to \c X25519_KEY_SIZE in the network crypto internals. + */ +static const uint NETWORK_SECRET_KEY_LENGTH = 32 * 2 + 1; + #endif /* NETWORK_CORE_CONFIG_H */ diff --git a/src/settings.cpp b/src/settings.cpp index 840196347c..29e168bf19 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -137,6 +137,7 @@ private: "newgrf", "servers", "server_bind_addresses", + "server_authorized_keys", }; public: @@ -1285,6 +1286,7 @@ static void HandleSettingDescs(IniFile &generic_ini, IniFile &private_ini, IniFi proc_list(private_ini, "server_bind_addresses", _network_bind_list); proc_list(private_ini, "servers", _network_host_list); proc_list(private_ini, "bans", _network_ban_list); + proc_list(private_ini, "server_authorized_keys", _settings_client.network.server_authorized_keys); } } diff --git a/src/settings_type.h b/src/settings_type.h index 12100ac9ec..9181e59dbe 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -313,9 +313,12 @@ struct NetworkSettings { std::string server_invite_code_secret; ///< Secret to proof we got this invite code from the Game Coordinator. std::string server_name; ///< name of the server std::string server_password; ///< password for joining this server + std::vector server_authorized_keys; ///< Public keys of clients that are authorized to connect to the game. std::string rcon_password; ///< password for rconsole (server side) std::string admin_password; ///< password for 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. std::string default_company_pass; ///< default password for new companies in encrypted form std::string connect_to_ip; ///< default for the "Add server" query std::string network_id; ///< network ID for servers diff --git a/src/table/settings/network_secrets_settings.ini b/src/table/settings/network_secrets_settings.ini index 4613636a86..3d7908e75a 100644 --- a/src/table/settings/network_secrets_settings.ini +++ b/src/table/settings/network_secrets_settings.ini @@ -61,6 +61,24 @@ flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC | SF_NETWORK_ONLY def = nullptr cat = SC_BASIC +[SDTC_SSTR] +var = network.client_secret_key +type = SLE_STR +length = NETWORK_SECRET_KEY_LENGTH +flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC +def = nullptr +; Prevent the user from setting the secret key from the console using 'setting' +pre_cb = [](auto) { return false; } + +[SDTC_SSTR] +var = network.client_public_key +type = SLE_STR +length = NETWORK_SECRET_KEY_LENGTH +flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC +def = nullptr +; Prevent the user from setting the public key from the console using 'setting' +pre_cb = [](auto) { return false; } + [SDTC_SSTR] var = network.default_company_pass type = SLE_STR diff --git a/src/tests/test_network_crypto.cpp b/src/tests/test_network_crypto.cpp index 0438a6ca65..7258c09150 100644 --- a/src/tests/test_network_crypto.cpp +++ b/src/tests/test_network_crypto.cpp @@ -16,6 +16,9 @@ #include "../network/core/packet.h" #include "../string_func.h" +/* 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); + class MockNetworkSocketHandler : public NetworkSocketHandler { }; From 5706801ea7fa08b4e7e60538122f90a010ea6af4 Mon Sep 17 00:00:00 2001 From: Rubidium Date: Thu, 14 Mar 2024 21:08:21 +0100 Subject: [PATCH 3/7] Feature: authenticate to the server without sending the password Either using password-authentication key exchange or via authorized keys --- src/lang/english.txt | 4 +- src/network/core/tcp_game.cpp | 10 ++-- src/network/core/tcp_game.h | 34 +++++++---- src/network/network.cpp | 1 + src/network/network_client.cpp | 75 +++++++++++++++++++----- src/network/network_client.h | 9 ++- src/network/network_gui.cpp | 14 ++--- src/network/network_gui.h | 2 +- src/network/network_server.cpp | 104 +++++++++++++++++++++++---------- src/network/network_server.h | 9 ++- src/network/network_type.h | 1 + 11 files changed, 184 insertions(+), 79 deletions(-) diff --git a/src/lang/english.txt b/src/lang/english.txt index 6832c13636..3fff8c6939 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2574,6 +2574,7 @@ STR_NETWORK_ERROR_BAD_PLAYER_NAME :{WHITE}Your pla STR_NETWORK_ERROR_BAD_SERVER_NAME :{WHITE}Your server name has not been set. The name can be set at the top of the Multiplayer window STR_NETWORK_ERROR_WRONG_REVISION :{WHITE}The revision of this client does not match the server's revision STR_NETWORK_ERROR_WRONG_PASSWORD :{WHITE}Wrong password +STR_NETWORK_ERROR_NOT_ON_ALLOW_LIST :{WHITE}You are not on the list of allowed clients STR_NETWORK_ERROR_SERVER_FULL :{WHITE}The server is full STR_NETWORK_ERROR_SERVER_BANNED :{WHITE}You are banned from this server STR_NETWORK_ERROR_KICKED :{WHITE}You were kicked out of the game @@ -2589,7 +2590,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 21 +###length 22 STR_NETWORK_ERROR_CLIENT_GENERAL :general error STR_NETWORK_ERROR_CLIENT_DESYNC :desync error STR_NETWORK_ERROR_CLIENT_SAVEGAME :could not load map @@ -2601,6 +2602,7 @@ STR_NETWORK_ERROR_CLIENT_NOT_EXPECTED :received invali 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_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_game.cpp b/src/network/core/tcp_game.cpp index f1b4b05e2a..9e179362bc 100644 --- a/src/network/core/tcp_game.cpp +++ b/src/network/core/tcp_game.cpp @@ -82,9 +82,10 @@ NetworkRecvStatus NetworkGameSocketHandler::HandlePacket(Packet &p) case PACKET_SERVER_GAME_INFO: return this->Receive_SERVER_GAME_INFO(p); case PACKET_SERVER_CLIENT_INFO: return this->Receive_SERVER_CLIENT_INFO(p); case PACKET_CLIENT_IDENTIFY: return this->Receive_CLIENT_IDENTIFY(p); - case PACKET_SERVER_NEED_GAME_PASSWORD: return this->Receive_SERVER_NEED_GAME_PASSWORD(p); + case PACKET_SERVER_AUTH_REQUEST: return this->Receive_SERVER_AUTH_REQUEST(p); case PACKET_SERVER_NEED_COMPANY_PASSWORD: return this->Receive_SERVER_NEED_COMPANY_PASSWORD(p); - case PACKET_CLIENT_GAME_PASSWORD: return this->Receive_CLIENT_GAME_PASSWORD(p); + case PACKET_CLIENT_AUTH_RESPONSE: return this->Receive_CLIENT_AUTH_RESPONSE(p); + case PACKET_SERVER_AUTH_COMPLETED: return this->Receive_SERVER_AUTH_COMPLETED(p); case PACKET_CLIENT_COMPANY_PASSWORD: return this->Receive_CLIENT_COMPANY_PASSWORD(p); case PACKET_SERVER_WELCOME: return this->Receive_SERVER_WELCOME(p); case PACKET_CLIENT_GETMAP: return this->Receive_CLIENT_GETMAP(p); @@ -164,9 +165,10 @@ NetworkRecvStatus NetworkGameSocketHandler::Receive_CLIENT_GAME_INFO(Packet &) { NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_GAME_INFO(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_GAME_INFO); } NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_CLIENT_INFO(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_CLIENT_INFO); } NetworkRecvStatus NetworkGameSocketHandler::Receive_CLIENT_IDENTIFY(Packet &) { return this->ReceiveInvalidPacket(PACKET_CLIENT_IDENTIFY); } -NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_NEED_GAME_PASSWORD(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_NEED_GAME_PASSWORD); } +NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_AUTH_REQUEST(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_AUTH_REQUEST); } NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_NEED_COMPANY_PASSWORD(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_NEED_COMPANY_PASSWORD); } -NetworkRecvStatus NetworkGameSocketHandler::Receive_CLIENT_GAME_PASSWORD(Packet &) { return this->ReceiveInvalidPacket(PACKET_CLIENT_GAME_PASSWORD); } +NetworkRecvStatus NetworkGameSocketHandler::Receive_CLIENT_AUTH_RESPONSE(Packet &) { return this->ReceiveInvalidPacket(PACKET_CLIENT_AUTH_RESPONSE); } +NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_AUTH_COMPLETED(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_AUTH_COMPLETED); } NetworkRecvStatus NetworkGameSocketHandler::Receive_CLIENT_COMPANY_PASSWORD(Packet &) { return this->ReceiveInvalidPacket(PACKET_CLIENT_COMPANY_PASSWORD); } NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_WELCOME(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_WELCOME); } NetworkRecvStatus NetworkGameSocketHandler::Receive_CLIENT_GETMAP(Packet &) { return this->ReceiveInvalidPacket(PACKET_CLIENT_GETMAP); } diff --git a/src/network/core/tcp_game.h b/src/network/core/tcp_game.h index 14310f0017..2e5f33dfdb 100644 --- a/src/network/core/tcp_game.h +++ b/src/network/core/tcp_game.h @@ -15,6 +15,7 @@ #include "os_abstraction.h" #include "tcp.h" #include "../network_type.h" +#include "../network_crypto.h" #include "../../core/pool_type.hpp" #include @@ -56,16 +57,19 @@ enum PacketGameType : uint8_t { * the map and other important data. */ - /* After the initial join, the next step is identification. */ + /* After the join step, the first perform game authentication and enabling encryption. */ + PACKET_SERVER_AUTH_REQUEST, ///< The server requests the client to authenticate using a number of methods. + PACKET_CLIENT_AUTH_RESPONSE, ///< The client responds to the authentication request. + PACKET_SERVER_AUTH_COMPLETED, ///< The server indicates the authentication is completed. + + /* After the authentication is done, the next step is identification. */ PACKET_CLIENT_IDENTIFY, ///< Client telling the server the client's name and requested company. /* After the identify step, the next is checking NewGRFs. */ PACKET_SERVER_CHECK_NEWGRFS, ///< Server sends NewGRF IDs and MD5 checksums for the client to check. PACKET_CLIENT_NEWGRFS_CHECKED, ///< Client acknowledges that it has all required NewGRFs. - /* Checking the game, and then company passwords. */ - PACKET_SERVER_NEED_GAME_PASSWORD, ///< Server requests the (hashed) game password. - PACKET_CLIENT_GAME_PASSWORD, ///< Clients sends the (hashed) game password. + /* Checking the company passwords. */ PACKET_SERVER_NEED_COMPANY_PASSWORD, ///< Server requests the (hashed) company password. PACKET_CLIENT_COMPANY_PASSWORD, ///< Client sends the (hashed) company password. @@ -214,10 +218,13 @@ protected: virtual NetworkRecvStatus Receive_CLIENT_IDENTIFY(Packet &p); /** - * Indication to the client that the server needs a game password. + * Indication to the client that it needs to authenticate: + * bool Whether to use the password in the key exchange. + * 32 * uint8_t Public key of the server. + * 24 * uint8_t Nonce for the key exchange. * @param p The packet that was just received. */ - virtual NetworkRecvStatus Receive_SERVER_NEED_GAME_PASSWORD(Packet &p); + virtual NetworkRecvStatus Receive_SERVER_AUTH_REQUEST(Packet &p); /** * Indication to the client that the server needs a company password: @@ -228,12 +235,19 @@ protected: virtual NetworkRecvStatus Receive_SERVER_NEED_COMPANY_PASSWORD(Packet &p); /** - * Send a password to the server to authorize: - * uint8_t Password type (see NetworkPasswordType). - * string The password. + * Send the response to the authentication request: + * 32 * uint8_t Public key of the client. + * 8 * uint8_t Random message that got encoded and signed. + * 16 * uint8_t Message authentication code. * @param p The packet that was just received. */ - virtual NetworkRecvStatus Receive_CLIENT_GAME_PASSWORD(Packet &p); + virtual NetworkRecvStatus Receive_CLIENT_AUTH_RESPONSE(Packet &p); + + /** + * Indication to the client that authentication has completed. + * @param p The packet that was just received. + */ + virtual NetworkRecvStatus Receive_SERVER_AUTH_COMPLETED(Packet &p); /** * Send a password to the server to authorize diff --git a/src/network/network.cpp b/src/network/network.cpp index 9d47c2f964..784c5a8f37 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -321,6 +321,7 @@ StringID GetNetworkErrorMsg(NetworkErrorCode err) STR_NETWORK_ERROR_CLIENT_TIMEOUT_MAP, STR_NETWORK_ERROR_CLIENT_TIMEOUT_JOIN, STR_NETWORK_ERROR_CLIENT_INVALID_CLIENT_NAME, + STR_NETWORK_ERROR_CLIENT_NOT_ON_ALLOW_LIST, }; static_assert(lengthof(network_error_strings) == NETWORK_ERROR_END); diff --git a/src/network/network_client.cpp b/src/network/network_client.cpp index 43eb46cb5c..f3fd5fe6f0 100644 --- a/src/network/network_client.cpp +++ b/src/network/network_client.cpp @@ -349,7 +349,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::SendJoin() p->Send_uint32(_openttd_newgrf_version); my_client->SendPacket(std::move(p)); - return ClientNetworkGameSocketHandler::SendIdentify(); + return NETWORK_RECV_STATUS_OKAY; } NetworkRecvStatus ClientNetworkGameSocketHandler::SendIdentify() @@ -377,13 +377,14 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::SendNewGRFsOk() * Set the game password as requested. * @param password The game password. */ -NetworkRecvStatus ClientNetworkGameSocketHandler::SendGamePassword(const std::string &password) +NetworkRecvStatus ClientNetworkGameSocketHandler::SendAuthResponse() { - Debug(net, 9, "Client::SendGamePassword()"); + Debug(net, 9, "Client::SendAuthResponse()"); - auto p = std::make_unique(my_client, PACKET_CLIENT_GAME_PASSWORD); - p->Send_string(password); + auto p = std::make_unique(my_client, PACKET_CLIENT_AUTH_RESPONSE); + my_client->authentication_handler->SendResponse(*p); my_client->SendPacket(std::move(p)); + return NETWORK_RECV_STATUS_OKAY; } @@ -680,6 +681,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_ERROR(Packet &p STR_NETWORK_ERROR_TIMEOUT_MAP, // NETWORK_ERROR_TIMEOUT_MAP 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 }; static_assert(lengthof(network_error_strings) == NETWORK_ERROR_END); @@ -705,7 +707,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_ERROR(Packet &p NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_CHECK_NEWGRFS(Packet &p) { - if (this->status != STATUS_JOIN) return NETWORK_RECV_STATUS_MALFORMED_PACKET; + if (this->status != STATUS_AUTHENTICATED) return NETWORK_RECV_STATUS_MALFORMED_PACKET; uint grf_count = p.Recv_uint8(); NetworkRecvStatus ret = NETWORK_RECV_STATUS_OKAY; @@ -736,26 +738,67 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_CHECK_NEWGRFS(P return ret; } -NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_NEED_GAME_PASSWORD(Packet &) +class ClientGamePasswordRequestHandler : public NetworkAuthenticationPasswordRequestHandler { + virtual void SendResponse() override { MyClient::SendAuthResponse(); } + virtual void AskUserForPassword(std::shared_ptr request) override + { + if (!_network_join.server_password.empty()) { + request->Reply(_network_join.server_password); + } else { + ShowNetworkNeedPassword(NETWORK_GAME_PASSWORD, request); + } + } +}; + +NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_AUTH_REQUEST(Packet &p) { - if (this->status < STATUS_JOIN || this->status >= STATUS_AUTH_GAME) return NETWORK_RECV_STATUS_MALFORMED_PACKET; + if (this->status != STATUS_JOIN && this->status != STATUS_AUTH_GAME) return NETWORK_RECV_STATUS_MALFORMED_PACKET; Debug(net, 9, "Client::status = AUTH_GAME"); this->status = STATUS_AUTH_GAME; - Debug(net, 9, "Client::Receive_SERVER_NEED_GAME_PASSWORD()"); + Debug(net, 9, "Client::Receive_SERVER_AUTH_REQUEST()"); - if (!_network_join.server_password.empty()) { - return SendGamePassword(_network_join.server_password); + if (this->authentication_handler == nullptr) { + this->authentication_handler = NetworkAuthenticationClientHandler::Create(std::make_shared(), + _settings_client.network.client_secret_key, _settings_client.network.client_public_key); } + switch (this->authentication_handler->ReceiveRequest(p)) { + case NetworkAuthenticationClientHandler::READY_FOR_RESPONSE: + return SendAuthResponse(); - ShowNetworkNeedPassword(NETWORK_GAME_PASSWORD); + case NetworkAuthenticationClientHandler::AWAIT_USER_INPUT: + return NETWORK_RECV_STATUS_OKAY; - return NETWORK_RECV_STATUS_OKAY; + case NetworkAuthenticationClientHandler::INVALID: + default: + return NETWORK_RECV_STATUS_MALFORMED_PACKET; + } } +NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_AUTH_COMPLETED(Packet &) +{ + if (this->status != STATUS_AUTH_GAME || this->authentication_handler == nullptr) return NETWORK_RECV_STATUS_MALFORMED_PACKET; + + Debug(net, 9, "Client::Receive_SERVER_AUTH_COMPLETED()"); + + this->authentication_handler = nullptr; + + Debug(net, 9, "Client::status = AUTHENTICATED"); + this->status = STATUS_AUTHENTICATED; + + return this->SendIdentify(); +} + +class CompanyPasswordRequest : public NetworkAuthenticationPasswordRequest { + virtual void Reply(const std::string &password) override + { + MyClient::SendCompanyPassword(password); + } +}; + NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_NEED_COMPANY_PASSWORD(Packet &p) { - if (this->status < STATUS_JOIN || this->status >= STATUS_AUTH_COMPANY) return NETWORK_RECV_STATUS_MALFORMED_PACKET; + if (this->status < STATUS_AUTHENTICATED || this->status >= STATUS_AUTH_COMPANY) return NETWORK_RECV_STATUS_MALFORMED_PACKET; Debug(net, 9, "Client::status = AUTH_COMPANY"); this->status = STATUS_AUTH_COMPANY; @@ -769,14 +812,14 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_NEED_COMPANY_PA return SendCompanyPassword(_network_join.company_password); } - ShowNetworkNeedPassword(NETWORK_COMPANY_PASSWORD); + ShowNetworkNeedPassword(NETWORK_COMPANY_PASSWORD, std::make_shared()); return NETWORK_RECV_STATUS_OKAY; } NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_WELCOME(Packet &p) { - if (this->status < STATUS_JOIN || this->status >= STATUS_AUTHORIZED) return NETWORK_RECV_STATUS_MALFORMED_PACKET; + if (this->status < STATUS_AUTHENTICATED || this->status >= STATUS_AUTHORIZED) return NETWORK_RECV_STATUS_MALFORMED_PACKET; Debug(net, 9, "Client::status = AUTHORIZED"); this->status = STATUS_AUTHORIZED; diff --git a/src/network/network_client.h b/src/network/network_client.h index f6c0eb8a61..ac80393651 100644 --- a/src/network/network_client.h +++ b/src/network/network_client.h @@ -15,6 +15,7 @@ /** Class for handling the client side of the game connection. */ class ClientNetworkGameSocketHandler : public ZeroedMemoryAllocator, public NetworkGameSocketHandler { private: + std::unique_ptr authentication_handler; ///< The handler for the authentication. std::string connection_string; ///< Address we are connected to. std::shared_ptr savegame; ///< Packet reader for reading the savegame. uint8_t token; ///< The token we need to send back to the server to prove we're the right client. @@ -23,8 +24,9 @@ private: enum ServerStatus { STATUS_INACTIVE, ///< The client is not connected nor active. STATUS_JOIN, ///< We are trying to join a server. - STATUS_NEWGRFS_CHECK, ///< Last action was checking NewGRFs. STATUS_AUTH_GAME, ///< Last action was requesting game (server) password. + STATUS_AUTHENTICATED, ///< The game authentication has completed. + STATUS_NEWGRFS_CHECK, ///< Last action was checking NewGRFs. STATUS_AUTH_COMPANY, ///< Last action was requesting company password. STATUS_AUTHORIZED, ///< The client is authorized at the server. STATUS_MAP_WAIT, ///< The client is waiting as someone else is downloading the map. @@ -44,7 +46,8 @@ protected: NetworkRecvStatus Receive_SERVER_BANNED(Packet &p) override; NetworkRecvStatus Receive_SERVER_ERROR(Packet &p) override; NetworkRecvStatus Receive_SERVER_CLIENT_INFO(Packet &p) override; - NetworkRecvStatus Receive_SERVER_NEED_GAME_PASSWORD(Packet &p) override; + NetworkRecvStatus Receive_SERVER_AUTH_REQUEST(Packet &p) override; + NetworkRecvStatus Receive_SERVER_AUTH_COMPLETED(Packet &p) override; NetworkRecvStatus Receive_SERVER_NEED_COMPANY_PASSWORD(Packet &p) override; NetworkRecvStatus Receive_SERVER_WELCOME(Packet &p) override; NetworkRecvStatus Receive_SERVER_WAIT(Packet &p) override; @@ -86,7 +89,7 @@ public: static NetworkRecvStatus SendQuit(); static NetworkRecvStatus SendAck(); - static NetworkRecvStatus SendGamePassword(const std::string &password); + static NetworkRecvStatus SendAuthResponse(); static NetworkRecvStatus SendCompanyPassword(const std::string &password); static NetworkRecvStatus SendChat(NetworkAction action, DestType type, int dest, const std::string &msg, int64_t data); diff --git a/src/network/network_gui.cpp b/src/network/network_gui.cpp index 4c21dec535..29b6c18433 100644 --- a/src/network/network_gui.cpp +++ b/src/network/network_gui.cpp @@ -2103,7 +2103,7 @@ uint32_t _network_join_bytes; ///< The number of bytes we already do uint32_t _network_join_bytes_total; ///< The total number of bytes to download. struct NetworkJoinStatusWindow : Window { - NetworkPasswordType password_type; + std::shared_ptr request; NetworkJoinStatusWindow(WindowDesc *desc) : Window(desc) { @@ -2199,16 +2199,12 @@ struct NetworkJoinStatusWindow : Window { void OnQueryTextFinished(char *str) override { - if (StrEmpty(str)) { + if (StrEmpty(str) || this->request == nullptr) { NetworkDisconnect(); return; } - switch (this->password_type) { - case NETWORK_GAME_PASSWORD: MyClient::SendGamePassword (str); break; - case NETWORK_COMPANY_PASSWORD: MyClient::SendCompanyPassword(str); break; - default: NOT_REACHED(); - } + this->request->Reply(str); } }; @@ -2236,11 +2232,11 @@ void ShowJoinStatusWindow() new NetworkJoinStatusWindow(&_network_join_status_window_desc); } -void ShowNetworkNeedPassword(NetworkPasswordType npt) +void ShowNetworkNeedPassword(NetworkPasswordType npt, std::shared_ptr request) { NetworkJoinStatusWindow *w = (NetworkJoinStatusWindow *)FindWindowById(WC_NETWORK_STATUS_WINDOW, WN_NETWORK_STATUS_WINDOW_JOIN); if (w == nullptr) return; - w->password_type = npt; + w->request = request; StringID caption; switch (npt) { diff --git a/src/network/network_gui.h b/src/network/network_gui.h index 74d6d153e6..02283e9ae7 100644 --- a/src/network/network_gui.h +++ b/src/network/network_gui.h @@ -17,7 +17,7 @@ #include "network_type.h" #include "network_gamelist.h" -void ShowNetworkNeedPassword(NetworkPasswordType npt); +void ShowNetworkNeedPassword(NetworkPasswordType npt, std::shared_ptr request); void ShowNetworkChatQueryWindow(DestType type, int dest); void ShowJoinStatusWindow(); void ShowNetworkGameWindow(); diff --git a/src/network/network_server.cpp b/src/network/network_server.cpp index eb3a1e3b18..6c7d4192ee 100644 --- a/src/network/network_server.cpp +++ b/src/network/network_server.cpp @@ -58,6 +58,10 @@ INSTANTIATE_POOL_METHODS(NetworkClientSocket) /** Instantiate the listen sockets. */ template SocketList TCPListenHandler::sockets; +static NetworkAuthenticationDefaultPasswordProvider _password_provider(_settings_client.network.server_password); ///< Provides the password validation for the game's password. +static NetworkAuthenticationDefaultAuthorizedKeyHandler _authorized_key_handler(_settings_client.network.server_authorized_keys); ///< Provides the authorized key handling for the game authentication. + + /** Writing a savegame directly to a number of packets. */ struct PacketWriter : SaveFilter { ServerNetworkGameSocketHandler *cs; ///< Socket we are associated with. @@ -407,8 +411,8 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::SendNewGRFCheck() this->status = STATUS_NEWGRFS_CHECK; if (_grfconfig == nullptr) { - /* There are no NewGRFs, continue with the game password. */ - return this->SendNeedGamePassword(); + /* There are no NewGRFs, continue with the company password. */ + return this->SendNeedCompanyPassword(); } auto p = std::make_unique(this, PACKET_SERVER_CHECK_NEWGRFS, TCP_MTU); @@ -429,25 +433,39 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::SendNewGRFCheck() } /** Request the game password. */ -NetworkRecvStatus ServerNetworkGameSocketHandler::SendNeedGamePassword() +NetworkRecvStatus ServerNetworkGameSocketHandler::SendAuthRequest() { - Debug(net, 9, "client[{}] SendNeedGamePassword()", this->client_id); + Debug(net, 9, "client[{}] SendAuthRequest()", this->client_id); - /* Invalid packet when status is anything but STATUS_NEWGRFS_CHECK. */ - if (this->status != STATUS_NEWGRFS_CHECK) return this->CloseConnection(NETWORK_RECV_STATUS_MALFORMED_PACKET); + /* Invalid packet when status is anything but STATUS_INACTIVE or STATUS_AUTH_GAME. */ + if (this->status != STATUS_INACTIVE && status != STATUS_AUTH_GAME) return this->CloseConnection(NETWORK_RECV_STATUS_MALFORMED_PACKET); Debug(net, 9, "client[{}] status = AUTH_GAME", this->client_id); this->status = STATUS_AUTH_GAME; - if (_settings_client.network.server_password.empty()) { - /* Do not actually need a game password, continue with the company password. */ - return this->SendNeedCompanyPassword(); - } - /* Reset 'lag' counters */ this->last_frame = this->last_frame_server = _frame_counter; - auto p = std::make_unique(this, PACKET_SERVER_NEED_GAME_PASSWORD); + if (this->authentication_handler == nullptr) { + this->authentication_handler = NetworkAuthenticationServerHandler::Create(&_password_provider, &_authorized_key_handler); + } + + auto p = std::make_unique(this, PACKET_SERVER_AUTH_REQUEST); + this->authentication_handler->SendRequest(*p); + + this->SendPacket(std::move(p)); + return NETWORK_RECV_STATUS_OKAY; +} + +/** Notify the client that the authentication has completed. */ +NetworkRecvStatus ServerNetworkGameSocketHandler::SendAuthCompleted() +{ + Debug(net, 9, "client[{}] SendAuthCompleted()", this->client_id); + + /* Invalid packet when status is anything but STATUS_AUTH_GAME. */ + if (this->status != STATUS_AUTH_GAME) return this->CloseConnection(NETWORK_RECV_STATUS_MALFORMED_PACKET); + + auto p = std::make_unique(this, PACKET_SERVER_AUTH_COMPLETED); this->SendPacket(std::move(p)); return NETWORK_RECV_STATUS_OKAY; } @@ -457,8 +475,8 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::SendNeedCompanyPassword() { Debug(net, 9, "client[{}] SendNeedCompanyPassword()", this->client_id); - /* Invalid packet when status is anything but STATUS_AUTH_GAME. */ - if (this->status != STATUS_AUTH_GAME) return this->CloseConnection(NETWORK_RECV_STATUS_MALFORMED_PACKET); + /* Invalid packet when status is anything but STATUS_NEWGRFS_CHECK. */ + if (this->status != STATUS_NEWGRFS_CHECK) return this->CloseConnection(NETWORK_RECV_STATUS_MALFORMED_PACKET); Debug(net, 9, "client[{}] status = AUTH_COMPANY", this->client_id); this->status = STATUS_AUTH_COMPANY; @@ -865,7 +883,7 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_NEWGRFS_CHECKED Debug(net, 9, "client[{}] Receive_CLIENT_NEWGRFS_CHECKED()", this->client_id); - return this->SendNeedGamePassword(); + return this->SendNeedCompanyPassword(); } NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_JOIN(Packet &p) @@ -891,13 +909,7 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_JOIN(Packet &p) return this->SendError(NETWORK_ERROR_WRONG_REVISION); } - Debug(net, 9, "client[{}] status = IDENTIFY", this->client_id); - this->status = STATUS_IDENTIFY; - - /* Reset 'lag' counters */ - this->last_frame = this->last_frame_server = _frame_counter; - - return NETWORK_RECV_STATUS_OKAY; + return this->SendAuthRequest(); } NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_IDENTIFY(Packet &p) @@ -953,24 +965,52 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_IDENTIFY(Packet return this->SendNewGRFCheck(); } -NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_GAME_PASSWORD(Packet &p) +static NetworkErrorCode GetErrorForAuthenticationMethod(NetworkAuthenticationMethod method) +{ + switch (method) { + case NETWORK_AUTH_METHOD_X25519_PAKE: + return NETWORK_ERROR_WRONG_PASSWORD; + case NETWORK_AUTH_METHOD_X25519_AUTHORIZED_KEY: + return NETWORK_ERROR_NOT_ON_ALLOW_LIST; + + default: + NOT_REACHED(); + } +} + +NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_AUTH_RESPONSE(Packet &p) { if (this->status != STATUS_AUTH_GAME) { return this->SendError(NETWORK_ERROR_NOT_EXPECTED); } - Debug(net, 9, "client[{}] Receive_CLIENT_GAME_PASSWORD()", this->client_id); + Debug(net, 9, "client[{}] Receive_CLIENT_AUTH_RESPONSE()", this->client_id); - std::string password = p.Recv_string(NETWORK_PASSWORD_LENGTH); + auto authentication_method = this->authentication_handler->GetAuthenticationMethod(); + switch (this->authentication_handler->ReceiveResponse(p)) { + case NetworkAuthenticationServerHandler::AUTHENTICATED: + break; - /* Check game password. Allow joining if we cleared the password meanwhile */ - if (!_settings_client.network.server_password.empty() && - _settings_client.network.server_password.compare(password) != 0) { - /* Password is invalid */ - return this->SendError(NETWORK_ERROR_WRONG_PASSWORD); + case NetworkAuthenticationServerHandler::RETRY_NEXT_METHOD: + return this->SendAuthRequest(); + + case NetworkAuthenticationServerHandler::NOT_AUTHENTICATED: + default: + return this->SendError(GetErrorForAuthenticationMethod(authentication_method)); } - return this->SendNeedCompanyPassword(); + NetworkRecvStatus status = this->SendAuthCompleted(); + if (status != NETWORK_RECV_STATUS_OKAY) return status; + + this->authentication_handler = nullptr; + + Debug(net, 9, "client[{}] status = IDENTIFY", this->client_id); + this->status = STATUS_IDENTIFY; + + /* Reset 'lag' counters */ + this->last_frame = this->last_frame_server = _frame_counter; + + return NETWORK_RECV_STATUS_OKAY; } NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_COMPANY_PASSWORD(Packet &p) @@ -1978,9 +2018,9 @@ void NetworkServerShowStatusToConsole() { static const char * const stat_str[] = { "inactive", + "authorizing (server password)", "identifing client", "checking NewGRFs", - "authorizing (server password)", "authorizing (company password)", "authorized", "waiting", diff --git a/src/network/network_server.h b/src/network/network_server.h index f4ce50842a..b0e21f26f6 100644 --- a/src/network/network_server.h +++ b/src/network/network_server.h @@ -23,10 +23,12 @@ extern NetworkClientSocketPool _networkclientsocket_pool; /** Class for handling the server side of the game connection. */ class ServerNetworkGameSocketHandler : public NetworkClientSocketPool::PoolItem<&_networkclientsocket_pool>, public NetworkGameSocketHandler, public TCPListenHandler { protected: + std::unique_ptr authentication_handler; ///< The handler for the authentication. + NetworkRecvStatus Receive_CLIENT_JOIN(Packet &p) override; NetworkRecvStatus Receive_CLIENT_IDENTIFY(Packet &p) override; NetworkRecvStatus Receive_CLIENT_GAME_INFO(Packet &p) override; - NetworkRecvStatus Receive_CLIENT_GAME_PASSWORD(Packet &p) override; + NetworkRecvStatus Receive_CLIENT_AUTH_RESPONSE(Packet &p) override; NetworkRecvStatus Receive_CLIENT_COMPANY_PASSWORD(Packet &p) override; NetworkRecvStatus Receive_CLIENT_GETMAP(Packet &p) override; NetworkRecvStatus Receive_CLIENT_MAP_OK(Packet &p) override; @@ -44,16 +46,17 @@ protected: NetworkRecvStatus SendGameInfo(); NetworkRecvStatus SendNewGRFCheck(); NetworkRecvStatus SendWelcome(); - NetworkRecvStatus SendNeedGamePassword(); + NetworkRecvStatus SendAuthRequest(); + NetworkRecvStatus SendAuthCompleted(); NetworkRecvStatus SendNeedCompanyPassword(); public: /** Status of a client */ enum ClientStatus { STATUS_INACTIVE, ///< The client is not connected nor active. + STATUS_AUTH_GAME, ///< The client is authorizing with game (server) password. STATUS_IDENTIFY, ///< The client is identifying itself. STATUS_NEWGRFS_CHECK, ///< The client is checking NewGRFs. - STATUS_AUTH_GAME, ///< The client is authorizing with game (server) password. STATUS_AUTH_COMPANY, ///< The client is authorizing with company password. STATUS_AUTHORIZED, ///< The client is authorized. STATUS_MAP_WAIT, ///< The client is waiting as someone else is downloading the map. diff --git a/src/network/network_type.h b/src/network/network_type.h index 40a990d472..faa81a8d35 100644 --- a/src/network/network_type.h +++ b/src/network/network_type.h @@ -145,6 +145,7 @@ enum NetworkErrorCode { NETWORK_ERROR_TIMEOUT_MAP, NETWORK_ERROR_TIMEOUT_JOIN, NETWORK_ERROR_INVALID_CLIENT_NAME, + NETWORK_ERROR_NOT_ON_ALLOW_LIST, NETWORK_ERROR_END, }; From d26629c15bd8b1f412964a3d973a51e2fd492656 Mon Sep 17 00:00:00 2001 From: Rubidium Date: Sun, 17 Mar 2024 00:20:25 +0100 Subject: [PATCH 4/7] Codechange: make encoded length of packet size and type more explicit --- src/network/core/packet.cpp | 12 ++++++------ src/network/core/packet.h | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/network/core/packet.cpp b/src/network/core/packet.cpp index 2319b8adb1..d2fe0b4cde 100644 --- a/src/network/core/packet.cpp +++ b/src/network/core/packet.cpp @@ -223,7 +223,7 @@ bool Packet::CanReadFromPacket(size_t bytes_to_read, bool close_connection) */ bool Packet::HasPacketSizeData() const { - return this->pos >= sizeof(PacketSize); + return this->pos >= EncodedLengthOfPacketSize(); } /** @@ -250,10 +250,10 @@ bool Packet::ParsePacketSize() /* If the size of the packet is less than the bytes required for the size and type of * the packet, or more than the allowed limit, then something is wrong with the packet. * In those cases the packet can generally be regarded as containing garbage data. */ - if (size < sizeof(PacketSize) + sizeof(PacketType) || size > this->limit) return false; + if (size < EncodedLengthOfPacketSize() + EncodedLengthOfPacketType() || size > this->limit) return false; this->buffer.resize(size); - this->pos = sizeof(PacketSize); + this->pos = static_cast(EncodedLengthOfPacketSize()); return true; } @@ -263,7 +263,7 @@ bool Packet::ParsePacketSize() void Packet::PrepareToRead() { /* Put the position on the right place */ - this->pos = sizeof(PacketSize); + this->pos = static_cast(EncodedLengthOfPacketSize()); } /** @@ -272,8 +272,8 @@ void Packet::PrepareToRead() */ PacketType Packet::GetPacketType() const { - assert(this->Size() >= sizeof(PacketSize) + sizeof(PacketType)); - return static_cast(buffer[sizeof(PacketSize)]); + assert(this->Size() >= EncodedLengthOfPacketSize() + EncodedLengthOfPacketType()); + return static_cast(buffer[EncodedLengthOfPacketSize()]); } /** diff --git a/src/network/core/packet.h b/src/network/core/packet.h index beff95431d..2631173cce 100644 --- a/src/network/core/packet.h +++ b/src/network/core/packet.h @@ -40,6 +40,8 @@ typedef uint8_t PacketType; ///< Identifier for the packet * (year % 4 == 0) and ((year % 100 != 0) or (year % 400 == 0)) */ struct Packet { + static constexpr size_t EncodedLengthOfPacketSize() { return sizeof(PacketSize); } + static constexpr size_t EncodedLengthOfPacketType() { return sizeof(PacketType); } private: /** The current read/write position in the packet */ PacketSize pos; @@ -52,7 +54,7 @@ private: NetworkSocketHandler *cs; public: - Packet(NetworkSocketHandler *cs, size_t limit, size_t initial_read_size = sizeof(PacketSize)); + Packet(NetworkSocketHandler *cs, size_t limit, size_t initial_read_size = EncodedLengthOfPacketSize()); Packet(NetworkSocketHandler *cs, PacketType type, size_t limit = COMPAT_MTU); /* Sending/writing of packets */ From 1cf8799810277e544f12c4159d00016634225938 Mon Sep 17 00:00:00 2001 From: Rubidium Date: Thu, 14 Mar 2024 21:36:28 +0100 Subject: [PATCH 5/7] Feature: encrypt the connection between game server and client --- src/network/core/core.h | 6 +++ src/network/core/packet.cpp | 32 +++++++++++-- src/network/core/packet.h | 2 +- src/network/core/tcp.cpp | 6 ++- src/network/core/tcp_game.cpp | 4 +- src/network/core/tcp_game.h | 7 ++- src/network/core/udp.cpp | 5 +- src/network/network_client.cpp | 16 ++++--- src/network/network_client.h | 4 +- src/network/network_server.cpp | 12 +++-- src/network/network_server.h | 2 +- src/tests/test_network_crypto.cpp | 80 ++++++++++++++++++++++++++++--- 12 files changed, 143 insertions(+), 33 deletions(-) diff --git a/src/network/core/core.h b/src/network/core/core.h index 712687022a..a9d12b1f11 100644 --- a/src/network/core/core.h +++ b/src/network/core/core.h @@ -13,6 +13,7 @@ #define NETWORK_CORE_CORE_H #include "../../newgrf_config.h" +#include "../network_crypto.h" #include "config.h" bool NetworkCoreInitialize(); @@ -43,6 +44,11 @@ class NetworkSocketHandler { private: bool has_quit; ///< Whether the current client has quit/send a bad packet +protected: + friend struct Packet; + std::unique_ptr receive_encryption_handler; ///< The handler for decrypting received packets. + std::unique_ptr send_encryption_handler; ///< The handler for encrypting sent packets. + public: /** Create a new unbound socket */ NetworkSocketHandler() { this->has_quit = false; } diff --git a/src/network/core/packet.cpp b/src/network/core/packet.cpp index d2fe0b4cde..974630c63d 100644 --- a/src/network/core/packet.cpp +++ b/src/network/core/packet.cpp @@ -48,7 +48,14 @@ Packet::Packet(NetworkSocketHandler *cs, size_t limit, size_t initial_read_size) Packet::Packet(NetworkSocketHandler *cs, PacketType type, size_t limit) : pos(0), limit(limit), cs(cs) { /* Allocate space for the the size so we can write that in just before sending the packet. */ - this->Send_uint16(0); + size_t size = EncodedLengthOfPacketSize(); + if (cs != nullptr && cs->send_encryption_handler != nullptr) { + /* Allocate some space for the message authentication code of the encryption. */ + size += cs->send_encryption_handler->MACSize(); + } + assert(this->CanWriteToPacket(size)); + this->buffer.resize(size, 0); + this->Send_uint8(type); } @@ -64,6 +71,13 @@ void Packet::PrepareToSend() this->buffer[0] = GB(this->Size(), 0, 8); this->buffer[1] = GB(this->Size(), 8, 8); + if (cs != nullptr && cs->send_encryption_handler != nullptr) { + size_t offset = EncodedLengthOfPacketSize(); + size_t mac_size = cs->send_encryption_handler->MACSize(); + size_t message_offset = offset + mac_size; + cs->send_encryption_handler->Encrypt(std::span(&this->buffer[offset], mac_size), std::span(&this->buffer[message_offset], this->buffer.size() - message_offset)); + } + this->pos = 0; // We start reading from here this->buffer.shrink_to_fit(); } @@ -259,11 +273,21 @@ bool Packet::ParsePacketSize() /** * Prepares the packet so it can be read + * @return True when the packet was valid, otherwise false. */ -void Packet::PrepareToRead() +bool Packet::PrepareToRead() { /* Put the position on the right place */ this->pos = static_cast(EncodedLengthOfPacketSize()); + + if (cs == nullptr || cs->receive_encryption_handler == nullptr) return true; + + size_t mac_size = cs->receive_encryption_handler->MACSize(); + if (this->buffer.size() <= pos + mac_size) return false; + + bool valid = cs->receive_encryption_handler->Decrypt(std::span(&this->buffer[pos], mac_size), std::span(&this->buffer[pos + mac_size], this->buffer.size() - pos - mac_size)); + this->pos += static_cast(mac_size); + return valid; } /** @@ -273,7 +297,9 @@ void Packet::PrepareToRead() PacketType Packet::GetPacketType() const { assert(this->Size() >= EncodedLengthOfPacketSize() + EncodedLengthOfPacketType()); - return static_cast(buffer[EncodedLengthOfPacketSize()]); + size_t offset = EncodedLengthOfPacketSize(); + if (cs != nullptr && cs->send_encryption_handler != nullptr) offset += cs->send_encryption_handler->MACSize(); + return static_cast(buffer[offset]); } /** diff --git a/src/network/core/packet.h b/src/network/core/packet.h index 2631173cce..839f8d4740 100644 --- a/src/network/core/packet.h +++ b/src/network/core/packet.h @@ -74,7 +74,7 @@ public: bool HasPacketSizeData() const; bool ParsePacketSize(); size_t Size() const; - void PrepareToRead(); + [[nodiscard]] bool PrepareToRead(); PacketType GetPacketType() const; bool CanReadFromPacket(size_t bytes_to_read, bool close_connection = false); diff --git a/src/network/core/tcp.cpp b/src/network/core/tcp.cpp index b01b8cd075..8bd7b44f2f 100644 --- a/src/network/core/tcp.cpp +++ b/src/network/core/tcp.cpp @@ -188,7 +188,11 @@ std::unique_ptr NetworkTCPSocketHandler::ReceivePacket() } } - p.PrepareToRead(); + if (!p.PrepareToRead()) { + Debug(net, 0, "Invalid packet received (too small / decryption error)"); + this->CloseConnection(); + return nullptr; + } return std::move(this->packet_recv); } diff --git a/src/network/core/tcp_game.cpp b/src/network/core/tcp_game.cpp index 9e179362bc..5b7ef1dfca 100644 --- a/src/network/core/tcp_game.cpp +++ b/src/network/core/tcp_game.cpp @@ -85,7 +85,7 @@ NetworkRecvStatus NetworkGameSocketHandler::HandlePacket(Packet &p) case PACKET_SERVER_AUTH_REQUEST: return this->Receive_SERVER_AUTH_REQUEST(p); case PACKET_SERVER_NEED_COMPANY_PASSWORD: return this->Receive_SERVER_NEED_COMPANY_PASSWORD(p); case PACKET_CLIENT_AUTH_RESPONSE: return this->Receive_CLIENT_AUTH_RESPONSE(p); - case PACKET_SERVER_AUTH_COMPLETED: return this->Receive_SERVER_AUTH_COMPLETED(p); + case PACKET_SERVER_ENABLE_ENCRYPTION: return this->Receive_SERVER_ENABLE_ENCRYPTION(p); case PACKET_CLIENT_COMPANY_PASSWORD: return this->Receive_CLIENT_COMPANY_PASSWORD(p); case PACKET_SERVER_WELCOME: return this->Receive_SERVER_WELCOME(p); case PACKET_CLIENT_GETMAP: return this->Receive_CLIENT_GETMAP(p); @@ -168,7 +168,7 @@ NetworkRecvStatus NetworkGameSocketHandler::Receive_CLIENT_IDENTIFY(Packet &) { NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_AUTH_REQUEST(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_AUTH_REQUEST); } NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_NEED_COMPANY_PASSWORD(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_NEED_COMPANY_PASSWORD); } NetworkRecvStatus NetworkGameSocketHandler::Receive_CLIENT_AUTH_RESPONSE(Packet &) { return this->ReceiveInvalidPacket(PACKET_CLIENT_AUTH_RESPONSE); } -NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_AUTH_COMPLETED(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_AUTH_COMPLETED); } +NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_ENABLE_ENCRYPTION(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_ENABLE_ENCRYPTION); } NetworkRecvStatus NetworkGameSocketHandler::Receive_CLIENT_COMPANY_PASSWORD(Packet &) { return this->ReceiveInvalidPacket(PACKET_CLIENT_COMPANY_PASSWORD); } NetworkRecvStatus NetworkGameSocketHandler::Receive_SERVER_WELCOME(Packet &) { return this->ReceiveInvalidPacket(PACKET_SERVER_WELCOME); } NetworkRecvStatus NetworkGameSocketHandler::Receive_CLIENT_GETMAP(Packet &) { return this->ReceiveInvalidPacket(PACKET_CLIENT_GETMAP); } diff --git a/src/network/core/tcp_game.h b/src/network/core/tcp_game.h index 2e5f33dfdb..cf1b8b92b2 100644 --- a/src/network/core/tcp_game.h +++ b/src/network/core/tcp_game.h @@ -15,7 +15,6 @@ #include "os_abstraction.h" #include "tcp.h" #include "../network_type.h" -#include "../network_crypto.h" #include "../../core/pool_type.hpp" #include @@ -60,7 +59,7 @@ enum PacketGameType : uint8_t { /* After the join step, the first perform game authentication and enabling encryption. */ PACKET_SERVER_AUTH_REQUEST, ///< The server requests the client to authenticate using a number of methods. PACKET_CLIENT_AUTH_RESPONSE, ///< The client responds to the authentication request. - PACKET_SERVER_AUTH_COMPLETED, ///< The server indicates the authentication is completed. + PACKET_SERVER_ENABLE_ENCRYPTION, ///< The server tells that authentication has completed and requests to enable encryption with the keys of the last \c PACKET_CLIENT_AUTH_RESPONSE. /* After the authentication is done, the next step is identification. */ PACKET_CLIENT_IDENTIFY, ///< Client telling the server the client's name and requested company. @@ -244,10 +243,10 @@ protected: virtual NetworkRecvStatus Receive_CLIENT_AUTH_RESPONSE(Packet &p); /** - * Indication to the client that authentication has completed. + * Indication to the client that authentication is complete and encryption has to be used from here on forward. * @param p The packet that was just received. */ - virtual NetworkRecvStatus Receive_SERVER_AUTH_COMPLETED(Packet &p); + virtual NetworkRecvStatus Receive_SERVER_ENABLE_ENCRYPTION(Packet &p); /** * Send a password to the server to authorize diff --git a/src/network/core/udp.cpp b/src/network/core/udp.cpp index 04324e0098..c7acbd21a1 100644 --- a/src/network/core/udp.cpp +++ b/src/network/core/udp.cpp @@ -137,7 +137,10 @@ void NetworkUDPSocketHandler::ReceivePackets() Debug(net, 1, "Received a packet with mismatching size from {}", address.GetAddressAsString()); continue; } - p.PrepareToRead(); + if (!p.PrepareToRead()) { + Debug(net, 1, "Invalid packet received (too small / decryption error)"); + continue; + } /* Handle the packet */ this->HandleUDPPacket(p, address); diff --git a/src/network/network_client.cpp b/src/network/network_client.cpp index f3fd5fe6f0..698b2975dd 100644 --- a/src/network/network_client.cpp +++ b/src/network/network_client.cpp @@ -707,7 +707,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_ERROR(Packet &p NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_CHECK_NEWGRFS(Packet &p) { - if (this->status != STATUS_AUTHENTICATED) return NETWORK_RECV_STATUS_MALFORMED_PACKET; + if (this->status != STATUS_ENCRYPTED) return NETWORK_RECV_STATUS_MALFORMED_PACKET; uint grf_count = p.Recv_uint8(); NetworkRecvStatus ret = NETWORK_RECV_STATUS_OKAY; @@ -775,16 +775,18 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_AUTH_REQUEST(Pa } } -NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_AUTH_COMPLETED(Packet &) +NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_ENABLE_ENCRYPTION(Packet &) { if (this->status != STATUS_AUTH_GAME || this->authentication_handler == nullptr) return NETWORK_RECV_STATUS_MALFORMED_PACKET; - Debug(net, 9, "Client::Receive_SERVER_AUTH_COMPLETED()"); + Debug(net, 9, "Client::Receive_SERVER_ENABLE_ENCRYPTION()"); + this->receive_encryption_handler = this->authentication_handler->CreateServerToClientEncryptionHandler(); + this->send_encryption_handler = this->authentication_handler->CreateClientToServerEncryptionHandler(); this->authentication_handler = nullptr; - Debug(net, 9, "Client::status = AUTHENTICATED"); - this->status = STATUS_AUTHENTICATED; + Debug(net, 9, "Client::status = ENCRYPTED"); + this->status = STATUS_ENCRYPTED; return this->SendIdentify(); } @@ -798,7 +800,7 @@ class CompanyPasswordRequest : public NetworkAuthenticationPasswordRequest { NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_NEED_COMPANY_PASSWORD(Packet &p) { - if (this->status < STATUS_AUTHENTICATED || this->status >= STATUS_AUTH_COMPANY) return NETWORK_RECV_STATUS_MALFORMED_PACKET; + if (this->status < STATUS_ENCRYPTED || this->status >= STATUS_AUTH_COMPANY) return NETWORK_RECV_STATUS_MALFORMED_PACKET; Debug(net, 9, "Client::status = AUTH_COMPANY"); this->status = STATUS_AUTH_COMPANY; @@ -819,7 +821,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_NEED_COMPANY_PA NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_WELCOME(Packet &p) { - if (this->status < STATUS_AUTHENTICATED || this->status >= STATUS_AUTHORIZED) return NETWORK_RECV_STATUS_MALFORMED_PACKET; + if (this->status < STATUS_ENCRYPTED || this->status >= STATUS_AUTHORIZED) return NETWORK_RECV_STATUS_MALFORMED_PACKET; Debug(net, 9, "Client::status = AUTHORIZED"); this->status = STATUS_AUTHORIZED; diff --git a/src/network/network_client.h b/src/network/network_client.h index ac80393651..8d68b8d8ba 100644 --- a/src/network/network_client.h +++ b/src/network/network_client.h @@ -25,7 +25,7 @@ private: STATUS_INACTIVE, ///< The client is not connected nor active. STATUS_JOIN, ///< We are trying to join a server. STATUS_AUTH_GAME, ///< Last action was requesting game (server) password. - STATUS_AUTHENTICATED, ///< The game authentication has completed. + STATUS_ENCRYPTED, ///< The game authentication has completed and from here on the connection to the server is encrypted. STATUS_NEWGRFS_CHECK, ///< Last action was checking NewGRFs. STATUS_AUTH_COMPANY, ///< Last action was requesting company password. STATUS_AUTHORIZED, ///< The client is authorized at the server. @@ -47,7 +47,7 @@ protected: NetworkRecvStatus Receive_SERVER_ERROR(Packet &p) override; NetworkRecvStatus Receive_SERVER_CLIENT_INFO(Packet &p) override; NetworkRecvStatus Receive_SERVER_AUTH_REQUEST(Packet &p) override; - NetworkRecvStatus Receive_SERVER_AUTH_COMPLETED(Packet &p) override; + NetworkRecvStatus Receive_SERVER_ENABLE_ENCRYPTION(Packet &p) override; NetworkRecvStatus Receive_SERVER_NEED_COMPANY_PASSWORD(Packet &p) override; NetworkRecvStatus Receive_SERVER_WELCOME(Packet &p) override; NetworkRecvStatus Receive_SERVER_WAIT(Packet &p) override; diff --git a/src/network/network_server.cpp b/src/network/network_server.cpp index 6c7d4192ee..3a792504dd 100644 --- a/src/network/network_server.cpp +++ b/src/network/network_server.cpp @@ -457,15 +457,15 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::SendAuthRequest() return NETWORK_RECV_STATUS_OKAY; } -/** Notify the client that the authentication has completed. */ -NetworkRecvStatus ServerNetworkGameSocketHandler::SendAuthCompleted() +/** Notify the client that the authentication has completed and tell that for the remainder of this socket encryption is enabled. */ +NetworkRecvStatus ServerNetworkGameSocketHandler::SendEnableEncryption() { - Debug(net, 9, "client[{}] SendAuthCompleted()", this->client_id); + Debug(net, 9, "client[{}] SendEnableEncryption()", this->client_id); /* Invalid packet when status is anything but STATUS_AUTH_GAME. */ if (this->status != STATUS_AUTH_GAME) return this->CloseConnection(NETWORK_RECV_STATUS_MALFORMED_PACKET); - auto p = std::make_unique(this, PACKET_SERVER_AUTH_COMPLETED); + auto p = std::make_unique(this, PACKET_SERVER_ENABLE_ENCRYPTION); this->SendPacket(std::move(p)); return NETWORK_RECV_STATUS_OKAY; } @@ -999,9 +999,11 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_AUTH_RESPONSE(P return this->SendError(GetErrorForAuthenticationMethod(authentication_method)); } - NetworkRecvStatus status = this->SendAuthCompleted(); + NetworkRecvStatus status = this->SendEnableEncryption(); if (status != NETWORK_RECV_STATUS_OKAY) return status; + this->receive_encryption_handler = this->authentication_handler->CreateClientToServerEncryptionHandler(); + this->send_encryption_handler = this->authentication_handler->CreateServerToClientEncryptionHandler(); this->authentication_handler = nullptr; Debug(net, 9, "client[{}] status = IDENTIFY", this->client_id); diff --git a/src/network/network_server.h b/src/network/network_server.h index b0e21f26f6..47b8777578 100644 --- a/src/network/network_server.h +++ b/src/network/network_server.h @@ -47,7 +47,7 @@ protected: NetworkRecvStatus SendNewGRFCheck(); NetworkRecvStatus SendWelcome(); NetworkRecvStatus SendAuthRequest(); - NetworkRecvStatus SendAuthCompleted(); + NetworkRecvStatus SendEnableEncryption(); NetworkRecvStatus SendNeedCompanyPassword(); public: diff --git a/src/tests/test_network_crypto.cpp b/src/tests/test_network_crypto.cpp index 7258c09150..34cbefaf27 100644 --- a/src/tests/test_network_crypto.cpp +++ b/src/tests/test_network_crypto.cpp @@ -20,15 +20,21 @@ static_assert(NETWORK_SECRET_KEY_LENGTH >= X25519_KEY_SIZE * 2 + 1); class MockNetworkSocketHandler : public NetworkSocketHandler { +public: + MockNetworkSocketHandler(std::unique_ptr &&receive = {}, std::unique_ptr &&send = {}) + { + this->receive_encryption_handler = std::move(receive); + this->send_encryption_handler = std::move(send); + } }; static MockNetworkSocketHandler mock_socket_handler; -static Packet CreatePacketForReading(Packet &source) +static std::tuple CreatePacketForReading(Packet &source, MockNetworkSocketHandler *socket_handler) { source.PrepareToSend(); - Packet dest(&mock_socket_handler, COMPAT_MTU, source.Size()); + Packet dest(socket_handler, COMPAT_MTU, source.Size()); auto transfer_in = [](Packet &source, char *dest_data, size_t length) { auto transfer_out = [](char *dest_data, const char *source_data, size_t length) { @@ -39,9 +45,9 @@ static Packet CreatePacketForReading(Packet &source) }; dest.TransferIn(transfer_in, source); - dest.PrepareToRead(); + bool valid = dest.PrepareToRead(); dest.Recv_uint8(); // Ignore the type - return dest; + return { dest, valid }; } class TestPasswordRequestHandler : public NetworkAuthenticationPasswordRequestHandler { @@ -60,13 +66,16 @@ static void TestAuthentication(NetworkAuthenticationServerHandler &server, Netwo Packet request(&mock_socket_handler, PacketType{}); server.SendRequest(request); - request = CreatePacketForReading(request); + bool valid; + std::tie(request, valid) = CreatePacketForReading(request, &mock_socket_handler); + CHECK(valid); CHECK(client.ReceiveRequest(request) == expected_request_result); Packet response(&mock_socket_handler, PacketType{}); client.SendResponse(response); - response = CreatePacketForReading(response); + std::tie(response, valid) = CreatePacketForReading(response, &mock_socket_handler); + CHECK(valid); CHECK(server.ReceiveResponse(response) == expected_response_result); } @@ -200,3 +209,62 @@ TEST_CASE("Authentication_Combined") TestAuthentication(*server, *client, NetworkAuthenticationServerHandler::AUTHENTICATED, NetworkAuthenticationClientHandler::READY_FOR_RESPONSE); } } + + +static void CheckEncryption(MockNetworkSocketHandler *sending_socket_handler, MockNetworkSocketHandler *receiving_socket_handler) +{ + PacketType sent_packet_type{ 1 }; + uint64_t sent_value = 0x1234567890ABCDEF; + std::set encrypted_packet_types; + + for (int i = 0; i < 10; i++) { + Packet request(sending_socket_handler, sent_packet_type); + request.Send_uint64(sent_value); + + auto [response, valid] = CreatePacketForReading(request, receiving_socket_handler); + CHECK(valid); + CHECK(response.Recv_uint64() == sent_value); + + encrypted_packet_types.insert(request.GetPacketType()); + } + /* + * Check whether it looks like encryption has happened. This is done by checking the value + * of the packet type after encryption. If after a few iterations more than one encrypted + * value has been seen, then we know that some type of encryption/scrambling is happening. + * + * Technically this check could fail erroneously when 16 subsequent encryptions yield the + * same encrypted packet type. However, with encryption that byte should have random value + * value, so the chance of this happening are tiny given enough iterations. + * Roughly in the order of 2**((iterations - 1) * 8), which with 10 iterations is in the + * one-in-sextillion (10**21) order of magnitude. + */ + CHECK(encrypted_packet_types.size() != 1); + +} + +TEST_CASE("Encryption handling") +{ + X25519KeyExchangeOnlyServerHandler server(X25519SecretKey::CreateRandom()); + X25519KeyExchangeOnlyClientHandler client(X25519SecretKey::CreateRandom()); + + TestAuthentication(server, client, NetworkAuthenticationServerHandler::AUTHENTICATED, NetworkAuthenticationClientHandler::READY_FOR_RESPONSE); + + MockNetworkSocketHandler server_socket_handler(server.CreateClientToServerEncryptionHandler(), server.CreateServerToClientEncryptionHandler()); + MockNetworkSocketHandler client_socket_handler(client.CreateServerToClientEncryptionHandler(), client.CreateClientToServerEncryptionHandler()); + + SECTION("Encyption happening client -> server") { + CheckEncryption(&client_socket_handler, &server_socket_handler); + } + + SECTION("Encyption happening server -> client") { + CheckEncryption(&server_socket_handler, &client_socket_handler); + } + + SECTION("Unencrypted packet sent causes invalid read packet") { + Packet request(&mock_socket_handler, PacketType{}); + request.Send_uint64(0); + + auto [response, valid] = CreatePacketForReading(request, &client_socket_handler); + CHECK(!valid); + } +} From b7dfa3eb900afa3494882bb6deb8597dc61fe81f Mon Sep 17 00:00:00 2001 From: Rubidium Date: Sun, 17 Mar 2024 11:18:37 +0100 Subject: [PATCH 6/7] Feature: authorized key authentication for rcon --- src/console_cmds.cpp | 1 + src/network/network_server.cpp | 10 +++++++--- src/network/network_server.h | 1 + src/settings.cpp | 2 ++ src/settings_type.h | 1 + 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index d74446be5a..3b797005cc 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -836,6 +836,7 @@ DEF_CONSOLE_CMD(ConRcon) if (argc == 0) { IConsolePrint(CC_HELP, "Remote control the server from another client. Usage: 'rcon '."); IConsolePrint(CC_HELP, "Remember to enclose the command in quotes, otherwise only the first parameter is sent."); + IConsolePrint(CC_HELP, "When your client's public key is in the 'authorized keys' for 'rcon', the password is not checked and may be '*'."); return true; } diff --git a/src/network/network_server.cpp b/src/network/network_server.cpp index 3a792504dd..6166376b11 100644 --- a/src/network/network_server.cpp +++ b/src/network/network_server.cpp @@ -60,6 +60,7 @@ template SocketList TCPListenHandlerSendEnableEncryption(); if (status != NETWORK_RECV_STATUS_OKAY) return status; + this->peer_public_key = this->authentication_handler->GetPeerPublicKey(); this->receive_encryption_handler = this->authentication_handler->CreateClientToServerEncryptionHandler(); this->send_encryption_handler = this->authentication_handler->CreateServerToClientEncryptionHandler(); this->authentication_handler = nullptr; @@ -1503,14 +1505,16 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_RCON(Packet &p) { if (this->status != STATUS_ACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); - if (_settings_client.network.rcon_password.empty()) return NETWORK_RECV_STATUS_OKAY; - Debug(net, 9, "client[{}] Receive_CLIENT_RCON()", this->client_id); std::string password = p.Recv_string(NETWORK_PASSWORD_LENGTH); std::string command = p.Recv_string(NETWORK_RCONCOMMAND_LENGTH); - if (_settings_client.network.rcon_password.compare(password) != 0) { + if (_rcon_authorized_key_handler.IsAllowed(this->peer_public_key)) { + /* We are allowed, nothing more to validate. */ + } else if (_settings_client.network.rcon_password.empty()) { + return NETWORK_RECV_STATUS_OKAY; + } else if (_settings_client.network.rcon_password.compare(password) != 0) { Debug(net, 1, "[rcon] Wrong password from client-id {}", this->client_id); return NETWORK_RECV_STATUS_OKAY; } diff --git a/src/network/network_server.h b/src/network/network_server.h index 47b8777578..d41ef4756c 100644 --- a/src/network/network_server.h +++ b/src/network/network_server.h @@ -24,6 +24,7 @@ extern NetworkClientSocketPool _networkclientsocket_pool; class ServerNetworkGameSocketHandler : public NetworkClientSocketPool::PoolItem<&_networkclientsocket_pool>, public NetworkGameSocketHandler, public TCPListenHandler { protected: std::unique_ptr authentication_handler; ///< The handler for the authentication. + std::string peer_public_key; ///< The public key of our client. NetworkRecvStatus Receive_CLIENT_JOIN(Packet &p) override; NetworkRecvStatus Receive_CLIENT_IDENTIFY(Packet &p) override; diff --git a/src/settings.cpp b/src/settings.cpp index 29e168bf19..88d96c0b48 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -138,6 +138,7 @@ private: "servers", "server_bind_addresses", "server_authorized_keys", + "rcon_authorized_keys", }; public: @@ -1287,6 +1288,7 @@ static void HandleSettingDescs(IniFile &generic_ini, IniFile &private_ini, IniFi proc_list(private_ini, "servers", _network_host_list); 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); } } diff --git a/src/settings_type.h b/src/settings_type.h index 9181e59dbe..17941ae0a4 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -315,6 +315,7 @@ struct NetworkSettings { std::string server_password; ///< password for joining this server std::vector server_authorized_keys; ///< Public keys of clients that are authorized to connect to the game. std::string rcon_password; ///< password for rconsole (server side) + std::vector 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 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. From 4af089b9be8f56418cc96447b3ca26f037f7888b Mon Sep 17 00:00:00 2001 From: Rubidium Date: Sun, 17 Mar 2024 19:11:55 +0100 Subject: [PATCH 7/7] Feature: console command to change authorized keys --- src/console_cmds.cpp | 98 ++++++++++++++++++++++++++++++++++ src/network/network_func.h | 1 + src/network/network_server.cpp | 12 +++++ src/network/network_server.h | 1 + 4 files changed, 112 insertions(+) diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index 3b797005cc..10e6810f14 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -1956,6 +1956,101 @@ DEF_CONSOLE_CMD(ConCompanyPassword) return true; } +/** All the known authorized keys with their name. */ +static std::vector *>> _console_cmd_authorized_keys{ + { "rcon", &_settings_client.network.rcon_authorized_keys }, + { "server", &_settings_client.network.server_authorized_keys }, +}; + +/** + * Simple helper to find the location of the given authorized key in the authorized keys. + * @param authorized_keys The keys to look through. + * @param authorized_key The key to look for. + * @return The iterator to the location of the authorized key, or \c authorized_keys.end(). + */ +static auto FindKey(std::vector *authorized_keys, std::string_view authorized_key) +{ + return std::find_if(authorized_keys->begin(), authorized_keys->end(), [authorized_key](auto &value) { return StrEqualsIgnoreCase(value, authorized_key); }); +} + +DEF_CONSOLE_CMD(ConNetworkAuthorizedKey) +{ + if (argc <= 2) { + IConsolePrint(CC_HELP, "List and update authorized keys. Usage: 'authorized_key list [type]|add [type] [key]|remove [type] [key]'."); + IConsolePrint(CC_HELP, " list: list all the authorized keys of the given type."); + IConsolePrint(CC_HELP, " add: add the given key to the authorized keys of the given type."); + IConsolePrint(CC_HELP, " remove: remove the given key from the authorized keys of the given type; use 'all' to remove all authorized keys."); + IConsolePrint(CC_HELP, "Instead of a key, use 'client:' to add/remove the key of that given client."); + + std::string buffer; + for (auto [name, _] : _console_cmd_authorized_keys) fmt::format_to(std::back_inserter(buffer), ", {}", name); + IConsolePrint(CC_HELP, "The supported types are: all{}.", buffer); + return true; + } + + bool valid_type = false; ///< Whether a valid type was given. + + for (auto [name, authorized_keys] : _console_cmd_authorized_keys) { + if (!StrEqualsIgnoreCase(argv[2], name) && !StrEqualsIgnoreCase(argv[2], "all")) continue; + + valid_type = true; + + if (StrEqualsIgnoreCase(argv[1], "list")) { + IConsolePrint(CC_WHITE, "The authorized keys for {} are:", name); + for (auto &authorized_key : *authorized_keys) IConsolePrint(CC_INFO, " {}", authorized_key); + continue; + } + + if (argc <= 3) { + IConsolePrint(CC_ERROR, "You must enter the key."); + return false; + } + + std::string authorized_key = argv[3]; + if (StrStartsWithIgnoreCase(authorized_key, "client:")) { + std::string id_string(authorized_key.substr(7)); + authorized_key = NetworkGetPublicKeyOfClient(static_cast(std::stoi(id_string))); + if (authorized_key.empty()) { + IConsolePrint(CC_ERROR, "You must enter a valid client id; see 'clients'."); + return false; + } + } + + auto iter = FindKey(authorized_keys, authorized_key); + + if (StrEqualsIgnoreCase(argv[1], "add")) { + if (iter == authorized_keys->end()) { + authorized_keys->push_back(authorized_key); + IConsolePrint(CC_INFO, "Added {} to {}.", authorized_key, name); + } else { + IConsolePrint(CC_WARNING, "Not added {} to {} as it already exists.", authorized_key, name); + } + continue; + } + + if (StrEqualsIgnoreCase(argv[1], "remove")) { + if (iter != authorized_keys->end()) { + authorized_keys->erase(iter); + IConsolePrint(CC_INFO, "Removed {} from {}.", authorized_key, name); + } else { + IConsolePrint(CC_WARNING, "Not removed {} from {} as it does not exist.", authorized_key, name); + } + continue; + } + + IConsolePrint(CC_WARNING, "No valid action was given."); + return false; + } + + if (!valid_type) { + IConsolePrint(CC_WARNING, "No valid type was given."); + return false; + } + + return true; +} + + /* Content downloading only is available with ZLIB */ #if defined(WITH_ZLIB) #include "network/network_content.h" @@ -2723,6 +2818,9 @@ void IConsoleStdLibRegister() IConsole::CmdRegister("pause", ConPauseGame, ConHookServerOrNoNetwork); IConsole::CmdRegister("unpause", ConUnpauseGame, ConHookServerOrNoNetwork); + IConsole::CmdRegister("authorized_key", ConNetworkAuthorizedKey, ConHookServerOnly); + IConsole::AliasRegister("ak", "authorized_key %+"); + IConsole::CmdRegister("company_pw", ConCompanyPassword, ConHookNeedNetwork); IConsole::AliasRegister("company_password", "company_pw %+"); diff --git a/src/network/network_func.h b/src/network/network_func.h index 37a4a81fd6..66b4660169 100644 --- a/src/network/network_func.h +++ b/src/network/network_func.h @@ -61,6 +61,7 @@ bool NetworkCompanyIsPassworded(CompanyID company_id); uint NetworkMaxCompaniesAllowed(); bool NetworkMaxCompaniesReached(); void NetworkPrintClients(); +std::string_view NetworkGetPublicKeyOfClient(ClientID client_id); void NetworkHandlePauseChange(PauseMode prev_mode, PauseMode changed_mode); /*** Commands ran by the server ***/ diff --git a/src/network/network_server.cpp b/src/network/network_server.cpp index 6166376b11..39bd2aa540 100644 --- a/src/network/network_server.cpp +++ b/src/network/network_server.cpp @@ -2243,6 +2243,18 @@ void NetworkPrintClients() } } +/** + * Get the public key of the client with the given id. + * @param client_id The id of the client. + * @return View of the public key, which is empty when the client does not exist. + */ +std::string_view NetworkGetPublicKeyOfClient(ClientID client_id) +{ + auto socket = NetworkClientSocket::GetByClientID(client_id); + return socket == nullptr ? "" : socket->GetPeerPublicKey(); +} + + /** * Perform all the server specific administration of a new company. * @param c The newly created company; can't be nullptr. diff --git a/src/network/network_server.h b/src/network/network_server.h index d41ef4756c..6b75a123f9 100644 --- a/src/network/network_server.h +++ b/src/network/network_server.h @@ -121,6 +121,7 @@ public: } const std::string &GetClientIP(); + std::string_view GetPeerPublicKey() const { return this->peer_public_key; } static ServerNetworkGameSocketHandler *GetByClientID(ClientID client_id); };