From 47a1702a57215de14a536803b96e4eb4cb3963d1 Mon Sep 17 00:00:00 2001 From: Pavel Solodovnikov Date: Sat, 12 Apr 2025 12:07:30 +0300 Subject: [PATCH 1/8] AES-GCM runtime availability check Some backends may not support AES-GCM on all systems (example: libsodium). Add a new static AES_GCM_CipherContext::IsAvailable() function. Co-Authored-By: past-due <30942300+past-due@users.noreply.github.com> Signed-off-by: Pavel Solodovnikov --- src/common/crypto.h | 3 +++ src/common/crypto_bcrypt.cpp | 9 +++++++++ src/common/crypto_libsodium.cpp | 10 ++++++++++ src/common/crypto_symmetric_opensslevp.cpp | 5 +++++ 4 files changed, 27 insertions(+) diff --git a/src/common/crypto.h b/src/common/crypto.h index 054b0b3fe..4eb5aecef 100644 --- a/src/common/crypto.h +++ b/src/common/crypto.h @@ -68,6 +68,9 @@ class AES_GCM_CipherContext : public SymmetricCryptContextBase // Initialize context with the specified private key, IV size, and tag size bool InitCipher( const void *pKey, size_t cbKey, size_t cbIV, size_t cbTag, bool bEncrypt ); + + // Determine whether AES_GCM is supported on this system + crypto backend + static bool IsAvailable(); }; /// Encryption context for AES-GCM diff --git a/src/common/crypto_bcrypt.cpp b/src/common/crypto_bcrypt.cpp index 8d1edebd5..b55599982 100644 --- a/src/common/crypto_bcrypt.cpp +++ b/src/common/crypto_bcrypt.cpp @@ -150,6 +150,15 @@ bool AES_GCM_CipherContext::InitCipher( const void *pKey, size_t cbKey, size_t c return true; } +bool AES_GCM_CipherContext::IsAvailable() +{ + BCryptContext ctx; + if (BCryptOpenAlgorithmProvider(&ctx.hAlgAES, BCRYPT_AES_ALGORITHM, nullptr, 0) != 0) + return false; + AssertFatal(ctx.hAlgAES != INVALID_HANDLE_VALUE); + return true; +} + bool AES_GCM_EncryptContext::Encrypt( const void *pPlaintextData, size_t cbPlaintextData, const void *pIV, diff --git a/src/common/crypto_libsodium.cpp b/src/common/crypto_libsodium.cpp index 7f7702c66..edd8077b4 100644 --- a/src/common/crypto_libsodium.cpp +++ b/src/common/crypto_libsodium.cpp @@ -70,6 +70,16 @@ bool AES_GCM_CipherContext::InitCipher( const void *pKey, size_t cbKey, size_t c return true; } +bool AES_GCM_CipherContext::IsAvailable() +{ + // Libsodium requires AES and CLMUL instructions for AES-GCM, available in + // Intel "Westmere" and up. 90.41% of Steam users have this as of the + // November 2019 survey. + // Libsodium recommends ChaCha20-Poly1305 in software if you've not got AES support + // in hardware. + return crypto_aead_aes256gcm_is_available() == 1; +} + bool AES_GCM_EncryptContext::Encrypt( const void *pPlaintextData, size_t cbPlaintextData, const void *pIV, diff --git a/src/common/crypto_symmetric_opensslevp.cpp b/src/common/crypto_symmetric_opensslevp.cpp index a204735aa..cc0d2eae3 100644 --- a/src/common/crypto_symmetric_opensslevp.cpp +++ b/src/common/crypto_symmetric_opensslevp.cpp @@ -96,6 +96,11 @@ bool AES_GCM_CipherContext::InitCipher( const void *pKey, size_t cbKey, size_t c return true; } +bool AES_GCM_CipherContext::IsAvailable() +{ + return true; +} + bool AES_GCM_EncryptContext::Encrypt( const void *pPlaintextData, size_t cbPlaintextData, const void *pIV, From 732f155038cb849894a26216d462613cb7c72200 Mon Sep 17 00:00:00 2001 From: past-due <30942300+past-due@users.noreply.github.com> Date: Sun, 17 Oct 2021 11:51:24 -0400 Subject: [PATCH 2/8] crypto_bcrypt.cpp: Initialize hAlgAES --- src/common/crypto_bcrypt.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/crypto_bcrypt.cpp b/src/common/crypto_bcrypt.cpp index b55599982..394f24130 100644 --- a/src/common/crypto_bcrypt.cpp +++ b/src/common/crypto_bcrypt.cpp @@ -37,6 +37,7 @@ typedef struct _BCryptContext { ULONG cbKeyObject; _BCryptContext() { + hAlgAES = INVALID_HANDLE_VALUE; hKey = INVALID_HANDLE_VALUE; pbKeyObject = NULL; cbKeyObject = 0; From 63dd18dc18ac8bc5a3caa9656f8aa497b1d7076a Mon Sep 17 00:00:00 2001 From: Pavel Solodovnikov Date: Sat, 12 Apr 2025 12:29:22 +0300 Subject: [PATCH 3/8] Advertise / initialize AES_GCM cipher only if supported Some backends may not support AES-GCM on all systems. Be more precise in what algorithms are advertised / initialized. Co-Authored-By: past-due <30942300+past-due@users.noreply.github.com> Signed-off-by: Pavel Solodovnikov --- .../steamnetworkingsockets_connections.cpp | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp index 061cb7d2f..c1814a2b5 100644 --- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp +++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp @@ -1227,19 +1227,32 @@ void CSteamNetworkConnectionBase::SetCryptoCipherList() // V case 0: // Not allowed - m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); + if (AES_GCM_CipherContext::IsAvailable()) + { + m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); + } + if (m_msgCryptLocal.ciphers_size() == 0) + { + ConnectionState_ProblemDetectedLocally( k_ESteamNetConnectionEnd_Remote_BadCrypt, "No crypto ciphers available, although the connection requires encryption" ); + } break; case 1: // Allowed, but prefer encrypted - m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); + if (AES_GCM_CipherContext::IsAvailable()) + { + m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); + } m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_NULL ); break; case 2: // Allowed, preferred m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_NULL ); - m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); + if (AES_GCM_CipherContext::IsAvailable()) + { + m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); + } break; case 3: @@ -1536,7 +1549,12 @@ ESteamNetConnectionEnd CSteamNetworkConnectionBase::RecvCryptoHandshake( // Check for legacy client that didn't send a list of ciphers if ( m_msgCryptRemote.ciphers_size() == 0 ) - m_msgCryptRemote.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); + { + if ( AES_GCM_CipherContext::IsAvailable() ) + { + m_msgCryptRemote.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); + } + } // We need our own cert. If we don't have one by now, then we might try generating one if ( !m_msgSignedCertLocal.has_cert() ) From ffb527d4f1743521b434dee6c49f5cd520d09a9e Mon Sep 17 00:00:00 2001 From: Pavel Solodovnikov Date: Sat, 12 Apr 2025 11:49:34 +0300 Subject: [PATCH 4/8] crypto: Add support for ChaCha20-Poly1305 algorithm This change contains an implementation of the ChaCha20-Poly1305 crypto cipher for all supported crypto libraries (both OpenSSL and Libsodium). Windows's BCrypt library doesn't support it (hence, `ChaCha20_Poly1305_CipherContext::IsAvailable() == false`). In Libsodium, ChaCha20-Poly1305 is always available, while in OpenSSL it depends on whether the library has been compiled with the corresponding features or not (controlled via `OPENSSL_NO_CHACHA` and `OPENSSL_NO_POLY1305` feature macros). Signed-off-by: Pavel Solodovnikov --- src/common/crypto.h | 54 ++- src/common/crypto_bcrypt.cpp | 33 ++ src/common/crypto_libsodium.cpp | 106 +++++- src/common/crypto_symmetric_opensslevp.cpp | 335 ++++++++++++++++-- .../steamnetworkingsockets_messages.proto | 2 +- 5 files changed, 493 insertions(+), 37 deletions(-) diff --git a/src/common/crypto.h b/src/common/crypto.h index 4eb5aecef..708a95bcc 100644 --- a/src/common/crypto.h +++ b/src/common/crypto.h @@ -27,7 +27,7 @@ class SymmetricCryptContextBase void Wipe(); protected: - void *m_ctx; + void *m_ctx = nullptr; uint32 m_cbIV, m_cbTag; }; @@ -113,6 +113,58 @@ class AES_GCM_DecryptContext final : public AES_GCM_CipherContext, public ISymme ) override; }; +/// Base class for ChaCha20-Poly1305 encryption and decryption +class ChaCha20_Poly1305_CipherContext : public SymmetricCryptContextBase +{ +public: + + // Initialize context with the specified private key, IV size, and tag size + bool InitCipher(const void* pKey, size_t cbKey, size_t cbIV, size_t cbTag, bool bEncrypt); + + // Determine whether ChaCha20-Poly1305 is supported on this system + crypto backend + static bool IsAvailable(); +}; + +/// Encryption context for ChaCha20-Poly1305 +class ChaCha20_Poly1305_EncryptContext final : public ChaCha20_Poly1305_CipherContext, public ISymmetricEncryptContext +{ +public: + + // Initialize context with the specified private key, IV size, and tag size + inline bool Init(const void* pKey, size_t cbKey, size_t cbIV, size_t cbTag) + { + return InitCipher(pKey, cbKey, cbIV, cbTag, true); + } + + // Implements ISymmetricEncryptContext + virtual bool Encrypt( + const void* pPlaintextData, size_t cbPlaintextData, + const void* pIV, + void* pEncryptedDataAndTag, uint32* pcbEncryptedDataAndTag, + const void* pAdditionalAuthenticationData, size_t cbAuthenticationData // Optional additional authentication data. Not encrypted, but will be included in the tag, so it can be authenticated. + ) override; +}; + +/// Decryption context for ChaCha20-Poly1305 +class ChaCha20_Poly1305_DecryptContext final : public ChaCha20_Poly1305_CipherContext, public ISymmetricDecryptContext +{ +public: + + // Initialize context with the specified private key, IV size, and tag size + inline bool Init(const void* pKey, size_t cbKey, size_t cbIV, size_t cbTag) + { + return InitCipher(pKey, cbKey, cbIV, cbTag, false); + } + + // Implements ISymmetricDecryptContext + virtual bool Decrypt( + const void* pEncryptedDataAndTag, size_t cbEncryptedDataAndTag, + const void* pIV, + void* pPlaintextData, uint32* pcbPlaintextData, + const void* pAdditionalAuthenticationData, size_t cbAuthenticationData // Optional additional authentication data. Not encrypted, but will be included in the tag, so it can be authenticated. + ) override; +}; + namespace CCrypto { void Init(); diff --git a/src/common/crypto_bcrypt.cpp b/src/common/crypto_bcrypt.cpp index 394f24130..51332ee2c 100644 --- a/src/common/crypto_bcrypt.cpp +++ b/src/common/crypto_bcrypt.cpp @@ -228,6 +228,39 @@ bool AES_GCM_DecryptContext::Decrypt( return NT_SUCCESS(status); } +bool ChaCha20_Poly1305_CipherContext::InitCipher(const void* /*pKey*/, size_t /*cbKey*/, size_t /*cbIV*/, size_t /*cbTag*/, bool /*bEncrypt*/) +{ + AssertMsg(false, "The ChaCha20-Poly1305 algorithm is not implemented in the BCrypt library"); + return false; +} + +bool ChaCha20_Poly1305_CipherContext::IsAvailable() +{ + return false; +} + +bool ChaCha20_Poly1305_EncryptContext::Encrypt( + const void* /*pPlaintextData*/, size_t /*cbPlaintextData*/, + const void* /*pIV*/, + void* /*pEncryptedDataAndTag*/, uint32* /*pcbEncryptedDataAndTag*/, + const void* /*pAdditionalAuthenticationData*/, size_t /*cbAuthenticationData*/ + ) +{ + AssertMsg(false, "The ChaCha20-Poly1305 algorithm is not implemented in the BCrypt library"); + return false; +} + +bool AES_GCM_DecryptContext::Decrypt( + const void* /*pEncryptedDataAndTag*/, size_t /*cbEncryptedDataAndTag*/, + const void* /*pIV*/, + void* /*pPlaintextData*/, uint32* /*pcbPlaintextData*/, + const void* /*pAdditionalAuthenticationData*/, size_t /*cbAuthenticationData*/ + ) +{ + AssertMsg(false, "The ChaCha20-Poly1305 algorithm is not implemented in the BCrypt library"); + return false; +} + //----------------------------------------------------------------------------- // Purpose: Generate a SHA256 hash // Input: pchInput - Plaintext string of item to hash (null terminated) diff --git a/src/common/crypto_libsodium.cpp b/src/common/crypto_libsodium.cpp index edd8077b4..6d536af46 100644 --- a/src/common/crypto_libsodium.cpp +++ b/src/common/crypto_libsodium.cpp @@ -1,19 +1,20 @@ #include "crypto.h" +#ifdef VALVE_CRYPTO_LIBSODIUM + #include #include #include "tier0/memdbgoff.h" #include #include +#include #include #include #include #include #include "tier0/memdbgon.h" -#ifdef VALVE_CRYPTO_LIBSODIUM - SymmetricCryptContextBase::SymmetricCryptContextBase() : m_ctx(nullptr), m_cbIV(0), m_cbTag(0) { @@ -143,6 +144,107 @@ bool AES_GCM_DecryptContext::Decrypt( return nDecryptResult == 0; } +/// This implementation uses the IETF variant of the ChaCha20-Poly1305 algorithm from libsodium. +/// For more information, please see https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction +bool ChaCha20_Poly1305_CipherContext::InitCipher(const void* pKey, size_t cbKey, size_t cbIV, size_t cbTag, bool bEncrypt) +{ + if (cbKey != crypto_aead_chacha20poly1305_ietf_KEYBYTES) + { + AssertMsg(false, "ChaCha20-Poly1305-IETF key sizes other than %d are unsupported.", crypto_aead_chacha20poly1305_ietf_KEYBYTES); + return false; + } + if (cbIV != crypto_aead_chacha20poly1305_ietf_NPUBBYTES) + { + AssertMsg(false, "Nonce size is unsupported"); + return false; + } + + Wipe(); + + if (pKey == nullptr) + { + AssertMsg(false, "Invalid secret key"); + return false; + } + + m_ctx = sodium_malloc(cbKey); + memcpy(m_ctx, pKey, cbKey); + + m_cbIV = cbIV; + m_cbTag = crypto_aead_chacha20poly1305_ietf_ABYTES; + COMPILE_TIME_ASSERT(crypto_aead_chacha20poly1305_ietf_ABYTES == 16); + + return true; +} + +bool ChaCha20_Poly1305_CipherContext::IsAvailable() +{ + return true; +} + +bool ChaCha20_Poly1305_EncryptContext::Encrypt( + const void* pPlaintextData, size_t cbPlaintextData, + const void* pIV, + void* pEncryptedDataAndTag, uint32* pcbEncryptedDataAndTag, + const void* pAdditionalAuthenticationData, size_t cbAuthenticationData + ) +{ + + // Make sure caller's buffer is big enough to hold the result. + if (cbPlaintextData + crypto_aead_chacha20poly1305_ietf_ABYTES > *pcbEncryptedDataAndTag) + { + *pcbEncryptedDataAndTag = 0; + return false; + } + + unsigned long long cbEncryptedDataAndTag_longlong; + if (crypto_aead_chacha20poly1305_ietf_encrypt( + static_cast(pEncryptedDataAndTag), &cbEncryptedDataAndTag_longlong, + static_cast(pPlaintextData), cbPlaintextData, + static_cast(pAdditionalAuthenticationData), cbAuthenticationData, + nullptr, + static_cast(pIV), + static_cast(m_ctx) + ) != 0 + ) { + AssertMsg(false, "crypto_aead_chacha20poly1305_ietf_encrypt failed"); // docs say this "should never happen" + *pcbEncryptedDataAndTag = 0; + return false; + } + + *pcbEncryptedDataAndTag = cbEncryptedDataAndTag_longlong; + + return true; +} + +bool ChaCha20_Poly1305_DecryptContext::Decrypt( + const void* pEncryptedDataAndTag, size_t cbEncryptedDataAndTag, + const void* pIV, + void* pPlaintextData, uint32* pcbPlaintextData, + const void* pAdditionalAuthenticationData, size_t cbAuthenticationData + ) +{ + // Make sure caller's buffer is big enough to hold the result + if (cbEncryptedDataAndTag > *pcbPlaintextData + crypto_aead_chacha20poly1305_ietf_ABYTES) + { + *pcbPlaintextData = 0; + return false; + } + + unsigned long long cbPlaintextData_longlong = 0; + const int nDecryptResult = crypto_aead_chacha20poly1305_ietf_decrypt( + static_cast(pPlaintextData), &cbPlaintextData_longlong, + nullptr, + static_cast(pEncryptedDataAndTag), cbEncryptedDataAndTag, + static_cast(pAdditionalAuthenticationData), cbAuthenticationData, + static_cast(pIV), static_cast(m_ctx) + ); + + *pcbPlaintextData = cbPlaintextData_longlong; + + return nDecryptResult == 0; +} + void CCrypto::Init() { // sodium_init is safe to call multiple times from multiple threads diff --git a/src/common/crypto_symmetric_opensslevp.cpp b/src/common/crypto_symmetric_opensslevp.cpp index cc0d2eae3..59d8ce91a 100644 --- a/src/common/crypto_symmetric_opensslevp.cpp +++ b/src/common/crypto_symmetric_opensslevp.cpp @@ -9,6 +9,87 @@ extern void OneTimeCryptoInitOpenSSL(); +namespace +{ + +enum class SupportedCipherTypes +{ + AES_GCM, + CHACHA20_POLY1305 +}; + +template +const EVP_CIPHER* GetCipherForKeySize(size_t cbKey) +{ + static_assert(CipherType == SupportedCipherTypes::AES_GCM || CipherType == SupportedCipherTypes::CHACHA20_POLY1305, + "Unsupported cipher type"); + + // Select the cipher based on the size of the key + if constexpr ( CipherType == SupportedCipherTypes::AES_GCM ) + { + const EVP_CIPHER* cipher = nullptr; + switch (cbKey) + { + case 128 / 8: cipher = EVP_aes_128_gcm(); break; + case 192 / 8: cipher = EVP_aes_192_gcm(); break; + case 256 / 8: cipher = EVP_aes_256_gcm(); break; + } + return cipher; + } + else if constexpr ( CipherType == SupportedCipherTypes::CHACHA20_POLY1305 ) + { +#if !defined( OPENSSL_NO_CHACHA ) && !defined( OPENSSL_NO_POLY1305 ) + return cbKey == 32 ? EVP_chacha20_poly1305() : nullptr; +#else + AssertMsg(false, "OpenSSL configured without ChaCha20-Poly1305 support!"); + return nullptr; +#endif + } + return nullptr; +} + +void ResetCipherContext(EVP_CIPHER_CTX* ctx) +{ +#if OPENSSL_VERSION_NUMBER < 0x10100000 + EVP_CIPHER_CTX_cleanup( ctx ); + EVP_CIPHER_CTX_init( ctx ); +#else + EVP_CIPHER_CTX_reset( ctx ); +#endif +} + +EVP_CIPHER_CTX* NewCipherContext() +{ +#if OPENSSL_VERSION_NUMBER < 0x10100000 + auto ctx = new EVP_CIPHER_CTX; + if ( !ctx ) + return nullptr; + EVP_CIPHER_CTX_init( ctx ); + return ctx; +#else + return EVP_CIPHER_CTX_new(); +#endif +} + +bool SetCommonCipherParams(EVP_CIPHER_CTX* ctx, const EVP_CIPHER* cipher, const void* pKey, bool bEncrypt, size_t cbIV) +{ + // Setup for encryption setting the key + if ( EVP_CipherInit_ex( ctx, cipher, nullptr, (const uint8*)pKey, nullptr, bEncrypt ? 1 : 0 ) != 1 ) + { + return false; + } + + // Set IV length + if ( EVP_CIPHER_CTX_ctrl( ctx, EVP_CTRL_GCM_SET_IVLEN, (int)cbIV, NULL ) != 1 ) + { + AssertMsg(false, "Bad IV size"); + return false; + } + return true; +} + +} // anonymous namespace + SymmetricCryptContextBase::SymmetricCryptContextBase() { m_ctx = nullptr; @@ -38,36 +119,20 @@ bool AES_GCM_CipherContext::InitCipher( const void *pKey, size_t cbKey, size_t c EVP_CIPHER_CTX *ctx = (EVP_CIPHER_CTX*)m_ctx; if ( ctx ) { - #if OPENSSL_VERSION_NUMBER < 0x10100000 - EVP_CIPHER_CTX_cleanup( ctx ); - EVP_CIPHER_CTX_init( ctx ); - #else - EVP_CIPHER_CTX_reset( ctx ); - #endif + ResetCipherContext( ctx ); } else { - #if OPENSSL_VERSION_NUMBER < 0x10100000 - ctx = new EVP_CIPHER_CTX; - if ( !ctx ) - return false; - EVP_CIPHER_CTX_init( ctx ); - #else - ctx = EVP_CIPHER_CTX_new(); - if ( !ctx ) - return false; - #endif + ctx = NewCipherContext(); + if ( !ctx ) + { + return false; + } m_ctx = ctx; } // Select the cipher based on the size of the key - const EVP_CIPHER *cipher = nullptr; - switch ( cbKey ) - { - case 128/8: cipher = EVP_aes_128_gcm(); break; - case 192/8: cipher = EVP_aes_192_gcm(); break; - case 256/8: cipher = EVP_aes_256_gcm(); break; - } + const EVP_CIPHER *cipher = GetCipherForKeySize(cbKey); if ( cipher == nullptr ) { AssertMsg( false, "Invalid AES-GCM key size" ); @@ -75,21 +140,12 @@ bool AES_GCM_CipherContext::InitCipher( const void *pKey, size_t cbKey, size_t c return false; } - // Setup for encryption setting the key - if ( EVP_CipherInit_ex( ctx, cipher, nullptr, (const uint8*)pKey, nullptr, bEncrypt ? 1 : 0 ) != 1 ) + if (!SetCommonCipherParams(ctx, cipher, pKey, bEncrypt, cbIV)) { Wipe(); return false; } - // Set IV length - if ( EVP_CIPHER_CTX_ctrl( ctx, EVP_CTRL_GCM_SET_IVLEN, (int)cbIV, NULL) != 1 ) - { - AssertMsg( false, "Bad IV size" ); - Wipe(); - return false; - } - // Remember parameters m_cbIV = (uint32)cbIV; m_cbTag = (uint32)cbTag; @@ -260,6 +316,219 @@ bool AES_GCM_DecryptContext::Decrypt( return true; } +bool ChaCha20_Poly1305_CipherContext::InitCipher(const void* pKey, size_t cbKey, size_t cbIV, size_t cbTag, bool bEncrypt) +{ + EVP_CIPHER_CTX* ctx = (EVP_CIPHER_CTX*)m_ctx; + if ( ctx ) + { + ResetCipherContext( ctx ); + } + else + { + ctx = NewCipherContext(); + if ( !ctx ) + { + return false; + } + m_ctx = ctx; + } + + const EVP_CIPHER* cipher = GetCipherForKeySize(cbKey); + if ( cipher == nullptr ) + { + AssertMsg(false, "Bad private key size. Only 256 bit-wide keys are supported for ChaCha20-Poly1305"); + Wipe(); + return false; + } + + // Setup for encryption setting the key + if (EVP_CipherInit_ex(ctx, cipher, nullptr, (const uint8*)pKey, nullptr, bEncrypt ? 1 : 0) != 1) + { + Wipe(); + return false; + } + + // Set IV length + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, (int)cbIV, NULL) != 1) + { + AssertMsg(false, "Bad IV size"); + Wipe(); + return false; + } + + // Remember parameters + m_cbIV = (uint32)cbIV; + m_cbTag = (uint32)cbTag; + return true; +} + +bool ChaCha20_Poly1305_CipherContext::IsAvailable() +{ +#if !defined(OPENSSL_NO_CHACHA) && !defined(OPENSSL_NO_POLY1305) + return true; +#else + return false; +#endif +} + +bool ChaCha20_Poly1305_EncryptContext::Encrypt( + const void* pPlaintextData, size_t cbPlaintextData, + const void* pIV, + void* pEncryptedDataAndTag, uint32* pcbEncryptedDataAndTag, + const void* pAdditionalAuthenticationData, size_t cbAuthenticationData // Optional additional authentication data. Not encrypted, but will be included in the tag, so it can be authenticated. +) { + EVP_CIPHER_CTX* ctx = (EVP_CIPHER_CTX*)m_ctx; + if (!ctx) + { + AssertMsg(false, "Not initialized!"); + *pcbEncryptedDataAndTag = 0; + return false; + } + + // Calculate size of encrypted data. Note that GCM does not use padding. + uint32 cbEncryptedWithoutTag = (uint32)cbPlaintextData; + uint32 cbEncryptedTotal = cbEncryptedWithoutTag + m_cbTag; + + // Make sure their buffer is big enough + if (cbEncryptedTotal > *pcbEncryptedDataAndTag) + { + AssertMsg(false, "Buffer isn't big enough to hold padded+encrypted data and tag"); + return false; + } + + // This function really shouldn't fail unless we have a bug, + // so people might not check the return value. So make sure + // if we do fail, they don't think anything was encrypted. + *pcbEncryptedDataAndTag = 0; + + // Set IV + VerifyFatal(EVP_EncryptInit_ex(ctx, nullptr, nullptr, nullptr, (const uint8*)pIV) == 1); + + int nBytesWritten; + + // AAD, if any + if (cbAuthenticationData > 0 && pAdditionalAuthenticationData) + { + VerifyFatal(EVP_EncryptUpdate(ctx, nullptr, &nBytesWritten, (const uint8*)pAdditionalAuthenticationData, (int)cbAuthenticationData) == 1); + } + else + { + Assert(cbAuthenticationData == 0); + } + + // Now the actual plaintext to be encrypted + uint8* pOut = (uint8*)pEncryptedDataAndTag; + VerifyFatal(EVP_EncryptUpdate(ctx, pOut, &nBytesWritten, (const uint8*)pPlaintextData, (int)cbPlaintextData) == 1); + pOut += nBytesWritten; + + // Finish up + VerifyFatal(EVP_EncryptFinal_ex(ctx, pOut, &nBytesWritten) == 1); + pOut += nBytesWritten; + + // Make sure that we have the expected number of encrypted bytes at this point + VerifyFatal((uint8*)pEncryptedDataAndTag + cbEncryptedWithoutTag == pOut); + + // Append the tag + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, (int)m_cbTag, pOut) != 1) + { + AssertMsg(false, "Bad tag size"); + return false; + } + + // Give the caller back the size of everything + *pcbEncryptedDataAndTag = cbEncryptedTotal; + + // Success. + return true; +} + +bool ChaCha20_Poly1305_DecryptContext::Decrypt( + const void* pEncryptedDataAndTag, size_t cbEncryptedDataAndTag, + const void* pIV, + void* pPlaintextData, uint32* pcbPlaintextData, + const void* pAdditionalAuthenticationData, size_t cbAuthenticationData +) { + + EVP_CIPHER_CTX* ctx = (EVP_CIPHER_CTX*)m_ctx; + if (!ctx) + { + AssertMsg(false, "Not initialized!"); + *pcbPlaintextData = 0; + return false; + } + + // Make sure buffer and tag sizes aren't totally bogus + if (m_cbTag > cbEncryptedDataAndTag) + { + AssertMsg(false, "Encrypted size doesn't make sense for tag size"); + *pcbPlaintextData = 0; + return false; + } + uint32 cbEncryptedDataWithoutTag = uint32(cbEncryptedDataAndTag - m_cbTag); + + // Make sure their buffer is big enough. Remember that in GCM mode, + // there is no padding, so if this fails, we indeed would have overflowed + if (cbEncryptedDataWithoutTag > *pcbPlaintextData) + { + AssertMsg(false, "Buffer might not be big enough to hold decrypted data"); + *pcbPlaintextData = 0; + return false; + } + + // People really have to check the return value, but in case they + // don't, make sure they don't think we decrypted any data + *pcbPlaintextData = 0; + + // Set IV + VerifyFatal(EVP_DecryptInit_ex(ctx, nullptr, nullptr, nullptr, (const uint8*)pIV) == 1); + + int nBytesWritten; + + // AAD, if any + if (cbAuthenticationData > 0 && pAdditionalAuthenticationData) + { + // I don't think it's actually possible to failed here, but + // since the caller really must be checking the return value, + // let's not make this fatal + if (EVP_DecryptUpdate(ctx, nullptr, &nBytesWritten, (const uint8*)pAdditionalAuthenticationData, (int)cbAuthenticationData) != 1) + { + AssertMsg(false, "EVP_DecryptUpdate failed?"); + return false; + } + } + else + { + Assert(cbAuthenticationData == 0); + } + + uint8* pOut = (uint8*)pPlaintextData; + const uint8* pIn = (const uint8*)pEncryptedDataAndTag; + + // Now the actual ciphertext to be decrypted + if (EVP_DecryptUpdate(ctx, pOut, &nBytesWritten, pIn, (int)cbEncryptedDataWithoutTag) != 1) + return false; + pOut += nBytesWritten; + pIn += cbEncryptedDataWithoutTag; + + // Set expected tag value + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, (int)m_cbTag, const_cast(pIn)) != 1) + { + AssertMsg(false, "Bad tag size"); + return false; + } + + // Finish up, and check tag + if (EVP_DecryptFinal_ex(ctx, pOut, &nBytesWritten) <= 0) + return false; // data has been tamped with + pOut += nBytesWritten; + + // Make sure we got back the size we expected, and return the size + VerifyFatal(pOut == (uint8*)pPlaintextData + cbEncryptedDataWithoutTag); + *pcbPlaintextData = cbEncryptedDataWithoutTag; + + // Success. + return true; +} // // !KLUDGE! This is not specific to OpenSSL, and I'd like to put it in crypto.cpp. diff --git a/src/common/steamnetworkingsockets_messages.proto b/src/common/steamnetworkingsockets_messages.proto index 30c0ea6fc..700748a8f 100644 --- a/src/common/steamnetworkingsockets_messages.proto +++ b/src/common/steamnetworkingsockets_messages.proto @@ -18,7 +18,7 @@ enum ESteamNetworkingSocketsCipher k_ESteamNetworkingSocketsCipher_INVALID = 0; // Dummy value k_ESteamNetworkingSocketsCipher_NULL = 1; // No encryption or authentication k_ESteamNetworkingSocketsCipher_AES_256_GCM = 2; // AES256 in GCM mode with 12-byte security tag. Basically equivalent to TLS_AES_256_GCM_xxx - //k_ESteamNetworkingSocketsCipher_CHACHA20_POLY1305 = 3; + k_ESteamNetworkingSocketsCipher_CHACHA20_POLY1305 = 3; // ChaCha20-Poly1305 }; // Used in crypto handshake. Clients describe what they are willing to use, From 0430b548e62041e61ddd7acf1b6029351a374e5d Mon Sep 17 00:00:00 2001 From: Pavel Solodovnikov Date: Sat, 12 Apr 2025 12:44:57 +0300 Subject: [PATCH 5/8] crypto: Advertise support for ChaCha20-Poly1305 cipher when establishing connections Signed-off-by: Pavel Solodovnikov --- .../steamnetworkingsockets_connections.cpp | 46 +++++++++++++++++-- .../steamnetworkingsockets_internal.h | 3 ++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp index c1814a2b5..125d26c2f 100644 --- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp +++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp @@ -1227,11 +1227,15 @@ void CSteamNetworkConnectionBase::SetCryptoCipherList() // V case 0: // Not allowed - if (AES_GCM_CipherContext::IsAvailable()) + if ( AES_GCM_CipherContext::IsAvailable() ) { m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); } - if (m_msgCryptLocal.ciphers_size() == 0) + if ( ChaCha20_Poly1305_CipherContext::IsAvailable() ) + { + m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_CHACHA20_POLY1305 ); + } + if ( m_msgCryptLocal.ciphers_size() == 0 ) { ConnectionState_ProblemDetectedLocally( k_ESteamNetConnectionEnd_Remote_BadCrypt, "No crypto ciphers available, although the connection requires encryption" ); } @@ -1239,20 +1243,28 @@ void CSteamNetworkConnectionBase::SetCryptoCipherList() case 1: // Allowed, but prefer encrypted - if (AES_GCM_CipherContext::IsAvailable()) + if ( AES_GCM_CipherContext::IsAvailable() ) { m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); } + if ( ChaCha20_Poly1305_CipherContext::IsAvailable() ) + { + m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_CHACHA20_POLY1305 ); + } m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_NULL ); break; case 2: // Allowed, preferred m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_NULL ); - if (AES_GCM_CipherContext::IsAvailable()) + if ( AES_GCM_CipherContext::IsAvailable() ) { m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); } + if ( ChaCha20_Poly1305_CipherContext::IsAvailable() ) + { + m_msgCryptLocal.add_ciphers( k_ESteamNetworkingSocketsCipher_CHACHA20_POLY1305 ); + } break; case 3: @@ -1554,6 +1566,10 @@ ESteamNetConnectionEnd CSteamNetworkConnectionBase::RecvCryptoHandshake( { m_msgCryptRemote.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); } + if ( ChaCha20_Poly1305_CipherContext::IsAvailable() ) + { + m_msgCryptRemote.add_ciphers( k_ESteamNetworkingSocketsCipher_CHACHA20_POLY1305 ); + } } // We need our own cert. If we don't have one by now, then we might try generating one @@ -1616,7 +1632,7 @@ ESteamNetConnectionEnd CSteamNetworkConnectionBase::FinishCryptoHandshake( bool break; } } - switch (m_eNegotiatedCipher ) + switch ( m_eNegotiatedCipher ) { default: case k_ESteamNetworkingSocketsCipher_INVALID: @@ -1630,6 +1646,10 @@ ESteamNetConnectionEnd CSteamNetworkConnectionBase::FinishCryptoHandshake( bool case k_ESteamNetworkingSocketsCipher_AES_256_GCM: m_cbEncryptionOverhead = k_cbAESGCMTagSize; break; + + case k_ESteamNetworkingSocketsCipher_CHACHA20_POLY1305: + m_cbEncryptionOverhead = k_cbChaCha20Poly1305TagSize; + break; } // Recalculate MTU @@ -1777,6 +1797,22 @@ ESteamNetConnectionEnd CSteamNetworkConnectionBase::FinishCryptoHandshake( bool } } break; + case k_ESteamNetworkingSocketsCipher_CHACHA20_POLY1305: + { + auto* pSend = new ChaCha20_Poly1305_EncryptContext; + auto* pRecv = new ChaCha20_Poly1305_DecryptContext; + m_pCryptContextSend.reset( pSend ); + m_pCryptContextRecv.reset( pRecv ); + + if ( + !pSend->Init( cryptKeySend.m_buf, cryptKeySend.k_nSize, m_cryptIVSend.k_nSize, k_cbChaCha20Poly1305TagSize ) + || !pRecv->Init( cryptKeyRecv.m_buf, cryptKeyRecv.k_nSize, m_cryptIVRecv.k_nSize, k_cbChaCha20Poly1305TagSize ) ) + { + V_strcpy_safe( errMsg, "Error initializing crypto" ); + return k_ESteamNetConnectionEnd_Remote_BadCrypt; + } + } break; + } // diff --git a/src/steamnetworkingsockets/steamnetworkingsockets_internal.h b/src/steamnetworkingsockets/steamnetworkingsockets_internal.h index 50dba2360..a0954ad34 100644 --- a/src/steamnetworkingsockets/steamnetworkingsockets_internal.h +++ b/src/steamnetworkingsockets/steamnetworkingsockets_internal.h @@ -265,6 +265,9 @@ const int k_cbSteamNetworkingSocketsNoFragmentHeaderReserve = 100; /// which is what OpenSSL uses by default for TLS. const int k_cbAESGCMTagSize = 16; +/// Size of the authentication tag for ChaCha20-Poly1305 in bytes. +const int k_cbChaCha20Poly1305TagSize = 16; + /// Max length of plaintext and encrypted payload we will send. AES-GCM does /// not use padding (but it does have the security tag). So this can be /// arbitrary, it does not need to account for the block size. From 8ef0b58ba25834c382c984d56ee8223b3e8819c1 Mon Sep 17 00:00:00 2001 From: Pavel Solodovnikov Date: Mon, 14 Apr 2025 11:40:14 +0300 Subject: [PATCH 6/8] crypto: Use `IsAvailable()` check for AES-GCM availability in libsodium impl Signed-off-by: Pavel Solodovnikov --- src/common/crypto_libsodium.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/crypto_libsodium.cpp b/src/common/crypto_libsodium.cpp index 6d536af46..178686447 100644 --- a/src/common/crypto_libsodium.cpp +++ b/src/common/crypto_libsodium.cpp @@ -38,7 +38,7 @@ bool AES_GCM_CipherContext::InitCipher( const void *pKey, size_t cbKey, size_t c // November 2019 survey. // Libsodium recommends ChaCha20-Poly1305 in software if you've not got AES support // in hardware. - if ( crypto_aead_aes256gcm_is_available() != 1 ) + if ( !IsAvailable() ) { AssertMsg( false, "No hardware AES support on this CPU." ); return false; From f8dd1d0bb7d23373dfe13a8cb68b218fa987e0e2 Mon Sep 17 00:00:00 2001 From: Pavel Solodovnikov Date: Mon, 21 Apr 2025 11:47:34 +0300 Subject: [PATCH 7/8] crypto: Do not restrict libsodium use for only x86_64 arch Now that there's ChaCha20-Poly1305 implementation alongside AES-GCM (which is always available in libsodium), this restriction is not needed anymore. Signed-off-by: Pavel Solodovnikov --- CMakeLists.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9796b830f..25bc6a67b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,12 +138,6 @@ if(USE_CRYPTO STREQUAL "libsodium" OR USE_CRYPTO25519 STREQUAL "libsodium") find_package(sodium REQUIRED) endif() -if(USE_CRYPTO STREQUAL "libsodium") - if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "amd64.*|x86_64.*|AMD64.*|i686.*|i386.*|x86.*") - message(FATAL_ERROR "-DUSE_CRYPTO=libsodium invalid, libsodium AES implementation only works on x86/x86_64 CPUs") - endif() -endif() - # We always need at least sse2 on x86 if(CMAKE_SYSTEM_PROCESSOR MATCHES "amd64.*|x86_64.*|AMD64.*|i686.*|i386.*|x86.*") set(TARGET_ARCH_FLAGS "-msse2") From adf6ffa69714891e4f016d1eda512d29e0363035 Mon Sep 17 00:00:00 2001 From: past-due <30942300+past-due@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:40:00 -0400 Subject: [PATCH 8/8] crypto: Do not add ChaCha20-Poly1305 cipher for legacy client that didn't send a list of ciphers --- .../clientlib/steamnetworkingsockets_connections.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp index 125d26c2f..1a9c16dba 100644 --- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp +++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp @@ -1566,10 +1566,6 @@ ESteamNetConnectionEnd CSteamNetworkConnectionBase::RecvCryptoHandshake( { m_msgCryptRemote.add_ciphers( k_ESteamNetworkingSocketsCipher_AES_256_GCM ); } - if ( ChaCha20_Poly1305_CipherContext::IsAvailable() ) - { - m_msgCryptRemote.add_ciphers( k_ESteamNetworkingSocketsCipher_CHACHA20_POLY1305 ); - } } // We need our own cert. If we don't have one by now, then we might try generating one