From a9407a5781bdc722139cf3b660092d3a3ccf618c Mon Sep 17 00:00:00 2001 From: rtw1x1 Date: Tue, 3 Feb 2026 11:58:21 +0000 Subject: [PATCH] fix: Key validation failure --- src/EterBase/SecureCipher.cpp | 105 ++++++++++-------- src/EterBase/SecureCipher.h | 11 +- src/EterLib/NetStream.cpp | 9 ++ src/EterLib/NetStream.h | 4 + src/ScriptLib/PythonLauncher.cpp | 43 +++++++ src/UserInterface/AccountConnector.cpp | 1 + src/UserInterface/GuildMarkDownloader.cpp | 1 + src/UserInterface/GuildMarkUploader.cpp | 1 + .../PythonNetworkStreamPhaseHandShake.cpp | 1 + 9 files changed, 125 insertions(+), 51 deletions(-) diff --git a/src/EterBase/SecureCipher.cpp b/src/EterBase/SecureCipher.cpp index a7c845b..4db474f 100644 --- a/src/EterBase/SecureCipher.cpp +++ b/src/EterBase/SecureCipher.cpp @@ -23,6 +23,8 @@ SecureCipher::SecureCipher() sodium_memzero(m_sk, sizeof(m_sk)); sodium_memzero(m_tx_key, sizeof(m_tx_key)); sodium_memzero(m_rx_key, sizeof(m_rx_key)); + sodium_memzero(m_tx_stream_nonce, sizeof(m_tx_stream_nonce)); + sodium_memzero(m_rx_stream_nonce, sizeof(m_rx_stream_nonce)); sodium_memzero(m_session_token, sizeof(m_session_token)); } @@ -58,6 +60,8 @@ void SecureCipher::CleanUp() sodium_memzero(m_sk, sizeof(m_sk)); sodium_memzero(m_tx_key, sizeof(m_tx_key)); sodium_memzero(m_rx_key, sizeof(m_rx_key)); + sodium_memzero(m_tx_stream_nonce, sizeof(m_tx_stream_nonce)); + sodium_memzero(m_rx_stream_nonce, sizeof(m_rx_stream_nonce)); sodium_memzero(m_session_token, sizeof(m_session_token)); m_initialized = false; @@ -84,6 +88,13 @@ bool SecureCipher::ComputeClientKeys(const uint8_t* server_pk) return false; } + // Set up fixed stream nonces per direction + // client→server = 0x02, server→client = 0x01 + sodium_memzero(m_tx_stream_nonce, NONCE_SIZE); + m_tx_stream_nonce[0] = 0x02; + sodium_memzero(m_rx_stream_nonce, NONCE_SIZE); + m_rx_stream_nonce[0] = 0x01; + return true; } @@ -100,6 +111,13 @@ bool SecureCipher::ComputeServerKeys(const uint8_t* client_pk) return false; } + // Set up fixed stream nonces per direction + // server→client = 0x01, client→server = 0x02 + sodium_memzero(m_tx_stream_nonce, NONCE_SIZE); + m_tx_stream_nonce[0] = 0x01; + sodium_memzero(m_rx_stream_nonce, NONCE_SIZE); + m_rx_stream_nonce[0] = 0x02; + return true; } @@ -110,9 +128,9 @@ void SecureCipher::GenerateChallenge(uint8_t* out_challenge) void SecureCipher::ComputeChallengeResponse(const uint8_t* challenge, uint8_t* out_response) { - // HMAC the challenge using our rx_key as the authentication key - // This proves we derived the correct shared secret - crypto_auth(out_response, challenge, CHALLENGE_SIZE, m_rx_key); + // HMAC the challenge using our tx_key as the authentication key + // Client tx_key == Server rx_key, so the server can verify with its rx_key + crypto_auth(out_response, challenge, CHALLENGE_SIZE, m_tx_key); } bool SecureCipher::VerifyChallengeResponse(const uint8_t* challenge, const uint8_t* response) @@ -121,21 +139,36 @@ bool SecureCipher::VerifyChallengeResponse(const uint8_t* challenge, const uint8 return crypto_auth_verify(response, challenge, CHALLENGE_SIZE, m_rx_key) == 0; } -void SecureCipher::BuildNonce(uint8_t* nonce, uint64_t counter, bool is_tx) +void SecureCipher::ApplyStreamCipher(void* buffer, size_t len, + const uint8_t* key, uint64_t& byte_counter, + const uint8_t* stream_nonce) { - // 24-byte nonce structure: - // [0]: direction flag (0x01 for tx, 0x02 for rx) - // [1-7]: reserved/zero - // [8-15]: 64-bit counter (little-endian) - // [16-23]: reserved/zero + uint8_t* p = (uint8_t*)buffer; - sodium_memzero(nonce, NONCE_SIZE); - nonce[0] = is_tx ? 0x01 : 0x02; - - // Store counter in little-endian at offset 8 - for (int i = 0; i < 8; ++i) + // Handle partial leading block (if byte_counter isn't block-aligned) + uint32_t offset = (uint32_t)(byte_counter % 64); + if (offset != 0 && len > 0) { - nonce[8 + i] = (uint8_t)(counter >> (i * 8)); + // Generate full keystream block, use only the portion we need + uint8_t ks[64]; + sodium_memzero(ks, 64); + crypto_stream_xchacha20_xor_ic(ks, ks, 64, stream_nonce, byte_counter / 64, key); + + size_t use = len < (64 - offset) ? len : (64 - offset); + for (size_t i = 0; i < use; ++i) + p[i] ^= ks[offset + i]; + + p += use; + len -= use; + byte_counter += use; + } + + // Handle remaining data (starts at a block boundary) + if (len > 0) + { + crypto_stream_xchacha20_xor_ic(p, p, (unsigned long long)len, + stream_nonce, byte_counter / 64, key); + byte_counter += len; } } @@ -146,23 +179,23 @@ size_t SecureCipher::Encrypt(const void* plaintext, size_t plaintext_len, void* return 0; } + // AEAD encryption uses a random nonce (not the stream nonce) uint8_t nonce[NONCE_SIZE]; - BuildNonce(nonce, m_tx_nonce, true); + randombytes_buf(nonce, NONCE_SIZE); unsigned long long ciphertext_len = 0; if (crypto_aead_xchacha20poly1305_ietf_encrypt( (uint8_t*)ciphertext, &ciphertext_len, (const uint8_t*)plaintext, plaintext_len, - nullptr, 0, // No additional data - nullptr, // No secret nonce + nullptr, 0, + nullptr, nonce, m_tx_key) != 0) { return 0; } - ++m_tx_nonce; return (size_t)ciphertext_len; } @@ -179,23 +212,21 @@ size_t SecureCipher::Decrypt(const void* ciphertext, size_t ciphertext_len, void } uint8_t nonce[NONCE_SIZE]; - BuildNonce(nonce, m_rx_nonce, false); + randombytes_buf(nonce, NONCE_SIZE); unsigned long long plaintext_len = 0; if (crypto_aead_xchacha20poly1305_ietf_decrypt( (uint8_t*)plaintext, &plaintext_len, - nullptr, // No secret nonce output + nullptr, (const uint8_t*)ciphertext, ciphertext_len, - nullptr, 0, // No additional data + nullptr, 0, nonce, m_rx_key) != 0) { - // Decryption failed - either wrong key, tampered data, or replay attack return 0; } - ++m_rx_nonce; return (size_t)plaintext_len; } @@ -204,18 +235,7 @@ void SecureCipher::EncryptInPlace(void* buffer, size_t len) if (!m_activated || len == 0) return; - uint8_t nonce[NONCE_SIZE]; - BuildNonce(nonce, m_tx_nonce, true); - - crypto_stream_xchacha20_xor_ic( - (uint8_t*)buffer, - (const uint8_t*)buffer, - (unsigned long long)len, - nonce, - 0, - m_tx_key); - - ++m_tx_nonce; + ApplyStreamCipher(buffer, len, m_tx_key, m_tx_nonce, m_tx_stream_nonce); } void SecureCipher::DecryptInPlace(void* buffer, size_t len) @@ -223,18 +243,7 @@ void SecureCipher::DecryptInPlace(void* buffer, size_t len) if (!m_activated || len == 0) return; - uint8_t nonce[NONCE_SIZE]; - BuildNonce(nonce, m_rx_nonce, false); - - crypto_stream_xchacha20_xor_ic( - (uint8_t*)buffer, - (const uint8_t*)buffer, - (unsigned long long)len, - nonce, - 0, - m_rx_key); - - ++m_rx_nonce; + ApplyStreamCipher(buffer, len, m_rx_key, m_rx_nonce, m_rx_stream_nonce); } bool SecureCipher::EncryptToken(const uint8_t* plaintext, size_t len, diff --git a/src/EterBase/SecureCipher.h b/src/EterBase/SecureCipher.h index 23af01a..8ec6f72 100644 --- a/src/EterBase/SecureCipher.h +++ b/src/EterBase/SecureCipher.h @@ -90,13 +90,18 @@ private: uint8_t m_tx_key[KEY_SIZE]; // Key for encrypting outgoing packets uint8_t m_rx_key[KEY_SIZE]; // Key for decrypting incoming packets - // Nonce counters - prevent replay attacks + // Byte counters for continuous stream cipher uint64_t m_tx_nonce = 0; uint64_t m_rx_nonce = 0; + // Fixed nonces per direction (set during key exchange) + uint8_t m_tx_stream_nonce[NONCE_SIZE]; + uint8_t m_rx_stream_nonce[NONCE_SIZE]; + // Server-generated session token uint8_t m_session_token[SESSION_TOKEN_SIZE]; - // Build 24-byte nonce from counter - void BuildNonce(uint8_t* nonce, uint64_t counter, bool is_tx); + // Continuous stream cipher operation (handles arbitrary chunk sizes) + void ApplyStreamCipher(void* buffer, size_t len, const uint8_t* key, + uint64_t& byte_counter, const uint8_t* stream_nonce); }; diff --git a/src/EterLib/NetStream.cpp b/src/EterLib/NetStream.cpp index 09be7f2..5303e47 100644 --- a/src/EterLib/NetStream.cpp +++ b/src/EterLib/NetStream.cpp @@ -12,6 +12,15 @@ bool CNetworkStream::IsSecurityMode() return m_secureCipher.IsActivated(); } +void CNetworkStream::DecryptPendingRecvData() +{ + int remaining = m_recvBufInputPos - m_recvBufOutputPos; + if (remaining > 0 && m_secureCipher.IsActivated()) + { + m_secureCipher.DecryptInPlace(m_recvBuf + m_recvBufOutputPos, remaining); + } +} + void CNetworkStream::SetRecvBufferSize(int recvBufSize) { if (m_recvBuf) diff --git a/src/EterLib/NetStream.h b/src/EterLib/NetStream.h index 41d3fa3..e7d02bd 100644 --- a/src/EterLib/NetStream.h +++ b/src/EterLib/NetStream.h @@ -67,6 +67,10 @@ class CNetworkStream bool IsSecureCipherActivated() const { return m_secureCipher.IsActivated(); } void ActivateSecureCipher() { m_secureCipher.SetActivated(true); } + // Decrypt any unprocessed data already in the recv buffer + // Must be called after activating the cipher mid-stream + void DecryptPendingRecvData(); + private: time_t m_connectLimitTime; diff --git a/src/ScriptLib/PythonLauncher.cpp b/src/ScriptLib/PythonLauncher.cpp index 350378b..cb06900 100644 --- a/src/ScriptLib/PythonLauncher.cpp +++ b/src/ScriptLib/PythonLauncher.cpp @@ -37,6 +37,13 @@ void Traceback() str.append("\n"); } + if (!PyErr_Occurred()) + { + str.append("(No Python error set - failure occurred at C++ level)"); + LogBoxf("Traceback:\n\n%s\n", str.c_str()); + return; + } + PyObject * exc; PyObject * v; PyObject * tb; @@ -44,6 +51,42 @@ void Traceback() PyErr_Fetch(&exc, &v, &tb); PyErr_NormalizeException(&exc, &v, &tb); + // Try using traceback.format_exception for full details + PyObject* tbMod = PyImport_ImportModule("traceback"); + if (tbMod) + { + PyObject* fmtFunc = PyObject_GetAttrString(tbMod, "format_exception"); + if (fmtFunc) + { + PyObject* result = PyObject_CallFunction(fmtFunc, (char*)"OOO", + exc ? exc : Py_None, + v ? v : Py_None, + tb ? tb : Py_None); + if (result && PyList_Check(result)) + { + Py_ssize_t n = PyList_Size(result); + for (Py_ssize_t i = 0; i < n; ++i) + { + PyObject* line = PyList_GetItem(result, i); + if (line && PyString_Check(line)) + str.append(PyString_AS_STRING(line)); + } + Py_DECREF(result); + Py_DECREF(fmtFunc); + Py_DECREF(tbMod); + Py_XDECREF(exc); + Py_XDECREF(v); + Py_XDECREF(tb); + LogBoxf("Traceback:\n\n%s\n", str.c_str()); + return; + } + Py_XDECREF(result); + Py_DECREF(fmtFunc); + } + Py_DECREF(tbMod); + } + + // Fallback: manual extraction if (exc) { PyObject* excName = PyObject_GetAttrString(exc, "__name__"); diff --git a/src/UserInterface/AccountConnector.cpp b/src/UserInterface/AccountConnector.cpp index 7ec02be..e2e395f 100644 --- a/src/UserInterface/AccountConnector.cpp +++ b/src/UserInterface/AccountConnector.cpp @@ -248,6 +248,7 @@ bool CAccountConnector::__AuthState_RecvKeyComplete() cipher.SetSessionToken(session_token); cipher.SetActivated(true); + DecryptPendingRecvData(); Tracen("Secure channel established - encryption activated"); return true; diff --git a/src/UserInterface/GuildMarkDownloader.cpp b/src/UserInterface/GuildMarkDownloader.cpp index bdcfb1a..05e3614 100644 --- a/src/UserInterface/GuildMarkDownloader.cpp +++ b/src/UserInterface/GuildMarkDownloader.cpp @@ -470,6 +470,7 @@ bool CGuildMarkDownloader::__LoginState_RecvKeyComplete() cipher.SetSessionToken(session_token); cipher.SetActivated(true); + DecryptPendingRecvData(); Tracen("SECURE CIPHER ACTIVATED"); return true; diff --git a/src/UserInterface/GuildMarkUploader.cpp b/src/UserInterface/GuildMarkUploader.cpp index bee8577..d96e51e 100644 --- a/src/UserInterface/GuildMarkUploader.cpp +++ b/src/UserInterface/GuildMarkUploader.cpp @@ -430,6 +430,7 @@ bool CGuildMarkUploader::__LoginState_RecvKeyComplete() cipher.SetSessionToken(session_token); cipher.SetActivated(true); + DecryptPendingRecvData(); Tracen("SECURE CIPHER ACTIVATED"); return true; diff --git a/src/UserInterface/PythonNetworkStreamPhaseHandShake.cpp b/src/UserInterface/PythonNetworkStreamPhaseHandShake.cpp index fcd9249..36061ed 100644 --- a/src/UserInterface/PythonNetworkStreamPhaseHandShake.cpp +++ b/src/UserInterface/PythonNetworkStreamPhaseHandShake.cpp @@ -205,6 +205,7 @@ bool CPythonNetworkStream::RecvKeyComplete() cipher.SetSessionToken(decrypted_token); cipher.SetActivated(true); + DecryptPendingRecvData(); Tracen("SECURE CIPHER ACTIVATED"); return true;