From e993afcd9909159f1a3842fd7dee470cb0e2f948 Mon Sep 17 00:00:00 2001 From: Jonathan G Rennison Date: Mon, 3 Jan 2022 03:09:32 +0000 Subject: [PATCH] Store encrypted company password hashes in server saves Restore when loading back into the server if server has required secret --- src/network/network.cpp | 29 +++-- src/network/network_func.h | 2 + src/network/network_internal.h | 2 +- src/saveload/company_sl.cpp | 104 ++++++++++++++++++ src/saveload/extended_ver_sl.cpp | 1 + src/saveload/extended_ver_sl.h | 1 + src/settings.cpp | 33 ++++++ src/settings_type.h | 2 + .../settings/network_secrets_settings.ini | 24 ++++ 9 files changed, 188 insertions(+), 10 deletions(-) diff --git a/src/network/network.cpp b/src/network/network.cpp index d139ca01c1..ddf3a3a13e 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -38,6 +38,7 @@ #include "../core/checksum_func.hpp" #include "../string_func_extra.h" #include "../3rdparty/randombytes/randombytes.h" +#include "../settings_internal.h" #include #include @@ -64,6 +65,8 @@ bool _is_network_server; ///< Does this client wants to be a network-server? bool _network_settings_access; ///< Can this client change server settings? NetworkCompanyState *_network_company_states = nullptr; ///< Statistics about some companies. std::string _network_company_server_id; ///< Server ID string used for company passwords +uint8 _network_company_password_storage_token[16]; ///< Non-secret token for storage of company passwords in savegames +uint8 _network_company_password_storage_key[32]; ///< Key for storage of company passwords in savegames ClientID _network_own_client_id; ///< Our client identifier. ClientID _redirect_console_to_client; ///< If not invalid, redirect the console output to a client. uint8 _network_reconnect; ///< Reconnect timeout @@ -920,7 +923,7 @@ bool NetworkServerStart() NetworkUDPServerListen(); _network_company_states = new NetworkCompanyState[MAX_COMPANIES]; - _network_company_server_id = NetworkGenerateRandomKeyString(); + _network_company_server_id = NetworkGenerateRandomKeyString(16); _network_server = true; _networking = true; _frame_counter = 0; @@ -1261,24 +1264,27 @@ static void NetworkGenerateServerId() _settings_client.network.network_id = hex_output; } -std::string NetworkGenerateRandomKeyString() +std::string NetworkGenerateRandomKeyString(uint bytes) { - uint8 key[16]; - char hex_output[16 * 2 + 1]; + uint8 *key = AllocaM(uint8, bytes); + char *hex_output = AllocaM(char, bytes * 2); - if (randombytes(key, 16) < 0) { + if (randombytes(key, bytes) < 0) { /* Fallback poor-quality random */ DEBUG(misc, 0, "High quality random source unavailable"); - for (int i = 0; i < 16; i++) { + for (uint i = 0; i < bytes; i++) { key[i] = (uint8)InteractiveRandom(); } } - for (int i = 0; i < 16; ++i) { - seprintf(hex_output + i * 2, lastof(hex_output), "%02x", key[i]); + char txt[3]; + for (uint i = 0; i < bytes; ++i) { + seprintf(txt, lastof(txt), "%02x", key[i]); + hex_output[i * 2] = txt[0]; + hex_output[i * 2 + 1] = txt[1]; } - return std::string(hex_output); + return std::string(hex_output, hex_output + bytes * 2); } class TCPNetworkDebugConnecter : TCPConnecter { @@ -1319,6 +1325,11 @@ void NetworkStartUp() /* Generate an server id when there is none yet */ if (_settings_client.network.network_id.empty()) NetworkGenerateServerId(); + if (_settings_client.network.company_password_storage_token.empty() || _settings_client.network.company_password_storage_secret.empty()) { + SetSettingValue(GetSettingFromName("network.company_password_storage_token")->AsStringSetting(), NetworkGenerateRandomKeyString(16)); + SetSettingValue(GetSettingFromName("network.company_password_storage_secret")->AsStringSetting(), NetworkGenerateRandomKeyString(32)); + } + _network_game_info = {}; NetworkInitialize(); diff --git a/src/network/network_func.h b/src/network/network_func.h index c643cb24bc..88a5456a87 100644 --- a/src/network/network_func.h +++ b/src/network/network_func.h @@ -26,6 +26,8 @@ extern NetworkCompanyState *_network_company_states; extern std::string _network_company_server_id; +extern uint8 _network_company_password_storage_token[16]; +extern uint8 _network_company_password_storage_key[32]; extern ClientID _network_own_client_id; extern ClientID _redirect_console_to_client; diff --git a/src/network/network_internal.h b/src/network/network_internal.h index 436fe610fe..bad3e8b753 100644 --- a/src/network/network_internal.h +++ b/src/network/network_internal.h @@ -131,7 +131,7 @@ uint NetworkCalculateLag(const NetworkClientSocket *cs); StringID GetNetworkErrorMsg(NetworkErrorCode err); bool NetworkMakeClientNameUnique(std::string &new_name); std::string GenerateCompanyPasswordHash(const std::string &password, const std::string &password_server_id, uint32 password_game_seed); -std::string NetworkGenerateRandomKeyString(); +std::string NetworkGenerateRandomKeyString(uint bytes); std::string_view ParseCompanyFromConnectionString(const std::string &connection_string, CompanyID *company_id); NetworkAddress ParseConnectionString(const std::string &connection_string, uint16 default_port); diff --git a/src/saveload/company_sl.cpp b/src/saveload/company_sl.cpp index 9bbca2c7fc..f35e968237 100644 --- a/src/saveload/company_sl.cpp +++ b/src/saveload/company_sl.cpp @@ -16,8 +16,14 @@ #include "../station_base.h" #include "../settings_func.h" #include "../strings_func.h" +#include "../network/network.h" +#include "../network/network_func.h" +#include "../network/network_server.h" +#include "../3rdparty/randombytes/randombytes.h" +#include "../3rdparty/monocypher/monocypher.h" #include "saveload.h" +#include "saveload_buffer.h" #include "table/strings.h" @@ -557,9 +563,107 @@ static void Save_PLYX() SaveSettingsPlyx(); } +static void Load_PLYP() +{ + size_t size = SlGetFieldLength(); + if (size <= 16 + 24 + 16 || !_network_server) { + SlSkipBytes(size); + return; + } + + uint8 token[16]; + ReadBuffer::GetCurrent()->CopyBytes(token, 16); + if (memcmp(token, _network_company_password_storage_token, 16) != 0) { + DEBUG(sl, 2, "Skipping encrypted company passwords"); + SlSkipBytes(size - 16); + return; + } + + uint8 nonce[24]; + uint8 mac[16]; + ReadBuffer::GetCurrent()->CopyBytes(nonce, 24); + ReadBuffer::GetCurrent()->CopyBytes(mac, 16); + + std::vector buffer(size - 16 - 24 - 16); + ReadBuffer::GetCurrent()->CopyBytes(buffer.data(), buffer.size()); + + if (crypto_unlock(buffer.data(), _network_company_password_storage_key, nonce, mac, buffer.data(), buffer.size()) == 0) { + SlLoadFromBuffer(buffer.data(), buffer.size(), [](void *) { + _network_company_server_id.resize(SlReadUint32()); + ReadBuffer::GetCurrent()->CopyBytes((uint8 *)_network_company_server_id.data(), _network_company_server_id.size()); + + while (true) { + uint16 cid = SlReadUint16(); + if (cid >= MAX_COMPANIES) break; + std::string password; + password.resize(SlReadUint32()); + ReadBuffer::GetCurrent()->CopyBytes((uint8 *)password.data(), password.size()); + NetworkServerSetCompanyPassword((CompanyID)cid, password, true); + } + + ReadBuffer::GetCurrent()->SkipBytes(SlReadByte()); // Skip padding + }, nullptr); + DEBUG(sl, 2, "Decrypted company passwords"); + } else { + DEBUG(sl, 2, "Failed to decrypt company passwords"); + } +} + +static void Save_PLYP() +{ + if (!_network_server || IsNetworkServerSave()) { + SlSetLength(0); + return; + } + + uint8 nonce[24]; /* Use only once per key: random */ + if (randombytes(nonce, 24) < 0) { + /* Can't get a random nonce, just give up */ + SlSetLength(0); + return; + } + + std::vector buffer = SlSaveToVector([](void *) { + SlWriteUint32(_network_company_server_id.size()); + MemoryDumper::GetCurrent()->CopyBytes((const uint8 *)_network_company_server_id.data(), _network_company_server_id.size()); + + for (const Company *c : Company::Iterate()) { + SlWriteUint16(c->index); + + const std::string &password = _network_company_states[c->index].password; + SlWriteUint32(password.size()); + MemoryDumper::GetCurrent()->CopyBytes((const uint8 *)password.data(), password.size()); + } + + SlWriteUint16(0xFFFF); + + /* Add some random length padding to not make it too obvious from the length whether passwords are set or not */ + uint8 padding[256]; + if (randombytes(padding, 256) >= 0) { + SlWriteByte(padding[0]); + MemoryDumper::GetCurrent()->CopyBytes(padding + 1, padding[0]); + } else { + SlWriteByte(0); + } + }, nullptr); + + + uint8 mac[16]; /* Message authentication code */ + + /* Encrypt in place */ + crypto_lock(mac, buffer.data(), _network_company_password_storage_key, nonce, buffer.data(), buffer.size()); + + SlSetLength(16 + 24 + 16 + buffer.size()); + MemoryDumper::GetCurrent()->CopyBytes(_network_company_password_storage_token, 16); + MemoryDumper::GetCurrent()->CopyBytes(nonce, 24); + MemoryDumper::GetCurrent()->CopyBytes(mac, 16); + MemoryDumper::GetCurrent()->CopyBytes(buffer.data(), buffer.size()); +} + static const ChunkHandler company_chunk_handlers[] = { { 'PLYR', Save_PLYR, Load_PLYR, Ptrs_PLYR, Check_PLYR, CH_ARRAY }, { 'PLYX', Save_PLYX, Load_PLYX, nullptr, Check_PLYX, CH_RIFF }, + { 'PLYP', Save_PLYP, Load_PLYP, nullptr, nullptr, CH_RIFF }, }; extern const ChunkHandlerTable _company_chunk_handlers(company_chunk_handlers); diff --git a/src/saveload/extended_ver_sl.cpp b/src/saveload/extended_ver_sl.cpp index 9c32c4b7a5..2d1255d204 100644 --- a/src/saveload/extended_ver_sl.cpp +++ b/src/saveload/extended_ver_sl.cpp @@ -161,6 +161,7 @@ const SlxiSubChunkInfo _sl_xv_sub_chunk_infos[] = { { XSLFI_BANKRUPTCY_EXTRA, XSCF_NULL, 1, 1, "bankruptcy_extra", nullptr, nullptr, nullptr }, { XSLFI_OBJECT_GROUND_TYPES, XSCF_NULL, 1, 1, "object_ground_types", nullptr, nullptr, nullptr }, { XSLFI_LINKGRAPH_AIRCRAFT, XSCF_NULL, 1, 1, "linkgraph_aircraft", nullptr, nullptr, nullptr }, + { XSLFI_COMPANY_PW, XSCF_IGNORABLE_ALL, 1, 1, "company_password", nullptr, nullptr, "PLYP" }, { XSLFI_SCRIPT_INT64, XSCF_NULL, 1, 1, "script_int64", nullptr, nullptr, nullptr }, { XSLFI_NULL, XSCF_NULL, 0, 0, nullptr, nullptr, nullptr, nullptr },// This is the end marker }; diff --git a/src/saveload/extended_ver_sl.h b/src/saveload/extended_ver_sl.h index b36cf9052f..84ad337320 100644 --- a/src/saveload/extended_ver_sl.h +++ b/src/saveload/extended_ver_sl.h @@ -115,6 +115,7 @@ enum SlXvFeatureIndex { XSLFI_BANKRUPTCY_EXTRA, ///< Extra company bankruptcy fields XSLFI_OBJECT_GROUND_TYPES, ///< Object ground types XSLFI_LINKGRAPH_AIRCRAFT, ///< Link graph last aircraft update field and aircraft link scaling setting + XSLFI_COMPANY_PW, ///< Company passwords XSLFI_SCRIPT_INT64, ///< See: SLV_SCRIPT_INT64 diff --git a/src/settings.cpp b/src/settings.cpp index cc3c311146..014574f502 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -143,6 +143,7 @@ typedef void SettingDescProc(IniFile &ini, const SettingTable &desc, const char typedef void SettingDescProcList(IniFile &ini, const char *grpname, StringList &list); static bool IsSignedVarMemType(VarType vt); +static bool DecodeHexText(const char *pos, uint8 *dest, size_t dest_size); /** @@ -1684,6 +1685,38 @@ static bool ReplaceAsteriskWithEmptyPassword(std::string &newval) return true; } +static bool IsValidHexKeyString(const std::string &newval) +{ + for (const char c : newval) { + if (!IsValidChar(c, CS_HEXADECIMAL)) return false; + } + return true; +} + +static bool IsValidHex128BitKeyString(std::string &newval) +{ + return newval.size() == 32 && IsValidHexKeyString(newval); +} + +static bool IsValidHex256BitKeyString(std::string &newval) +{ + return newval.size() == 64 && IsValidHexKeyString(newval); +} + +static void ParseCompanyPasswordStorageToken(const std::string &value) +{ + extern uint8 _network_company_password_storage_token[16]; + if (value.size() != 32) return; + DecodeHexText(value.c_str(), _network_company_password_storage_token, 16); +} + +static void ParseCompanyPasswordStorageSecret(const std::string &value) +{ + extern uint8 _network_company_password_storage_key[32]; + if (value.size() != 64) return; + DecodeHexText(value.c_str(), _network_company_password_storage_key, 32); +} + /** Update the game info, and send it to the clients when we are running as a server. */ static void UpdateClientConfigValues() { diff --git a/src/settings_type.h b/src/settings_type.h index 043da3a257..c45098ba17 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -361,6 +361,8 @@ struct NetworkSettings { 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 + std::string company_password_storage_token; ///< company password storage token + std::string company_password_storage_secret; ///< company password storage secret bool autoclean_companies; ///< automatically remove companies that are not in use uint8 autoclean_unprotected; ///< remove passwordless companies after this many months uint8 autoclean_protected; ///< remove the password from passworded companies after this many months diff --git a/src/table/settings/network_secrets_settings.ini b/src/table/settings/network_secrets_settings.ini index 8c1f4fa758..aba8c14a4b 100644 --- a/src/table/settings/network_secrets_settings.ini +++ b/src/table/settings/network_secrets_settings.ini @@ -8,6 +8,10 @@ [pre-amble] static bool ReplaceAsteriskWithEmptyPassword(std::string &newval); +static bool IsValidHex128BitKeyString(std::string &newval); +static bool IsValidHex256BitKeyString(std::string &newval); +static void ParseCompanyPasswordStorageToken(const std::string &value); +static void ParseCompanyPasswordStorageSecret(const std::string &value); static const SettingTable _network_secrets_settings = { [post-amble] @@ -99,3 +103,23 @@ type = SLE_STR length = NETWORK_INVITE_CODE_SECRET_LENGTH flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC | SF_NETWORK_ONLY def = nullptr + +[SDTC_SSTR] +var = network.company_password_storage_token +type = SLE_STR +length = 33 +flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC | SF_NETWORK_ONLY | SF_RUN_CALLBACKS_ON_PARSE +def = nullptr +pre_cb = IsValidHex128BitKeyString +post_cb = ParseCompanyPasswordStorageToken +startup = true + +[SDTC_SSTR] +var = network.company_password_storage_secret +type = SLE_STR +length = 65 +flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC | SF_NETWORK_ONLY | SF_RUN_CALLBACKS_ON_PARSE +def = nullptr +pre_cb = IsValidHex256BitKeyString +post_cb = ParseCompanyPasswordStorageSecret +startup = true