diff --git a/CMakeLists.txt b/CMakeLists.txt index cf34cfe9fc..1d6a095311 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -243,6 +243,8 @@ set(toxcore_SOURCES toxcore/ccompat.h toxcore/crypto_core.c toxcore/crypto_core.h + toxcore/noise.c + toxcore/noise.h toxcore/crypto_core_pack.c toxcore/crypto_core_pack.h toxcore/DHT.c @@ -639,6 +641,7 @@ if(UNITTEST AND TARGET GTest::gtest AND TARGET GTest::gmock) unit_test(toxcore mem) unit_test(toxcore mono_time) unit_test(toxcore net_crypto) + unit_test(toxcore noise) unit_test(toxcore network) unit_test(toxcore onion_client) unit_test(toxcore ping_array) diff --git a/auto_tests/forwarding_test.c b/auto_tests/forwarding_test.c index 722f16d0dc..273ce55a8d 100644 --- a/auto_tests/forwarding_test.c +++ b/auto_tests/forwarding_test.c @@ -132,7 +132,7 @@ static Forwarding_Subtox *new_forwarding_subtox(const Memory *mem, bool no_udp, ck_assert(subtox->tcp_np != nullptr); const TCP_Proxy_Info inf = {{{{0}}}}; - subtox->c = new_net_crypto(subtox->log, mem, rng, ns, subtox->mono_time, subtox->net, subtox->dht, &auto_test_dht_funcs, &inf, subtox->tcp_np); + subtox->c = new_net_crypto(subtox->log, mem, rng, ns, subtox->mono_time, subtox->net, subtox->dht, &auto_test_dht_funcs, &inf, subtox->tcp_np, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); subtox->forwarding = new_forwarding(subtox->log, mem, rng, subtox->mono_time, subtox->dht, subtox->net); ck_assert(subtox->forwarding != nullptr); diff --git a/auto_tests/onion_test.c b/auto_tests/onion_test.c index 875bbe8663..2f0726c8fb 100644 --- a/auto_tests/onion_test.c +++ b/auto_tests/onion_test.c @@ -492,7 +492,7 @@ static Onions *new_onions(const Memory *mem, const Random *rng, uint16_t port, u } TCP_Proxy_Info inf = {{{{0}}}}; - on->nc = new_net_crypto(on->log, mem, rng, ns, on->mono_time, net, dht, &auto_test_dht_funcs, &inf, on->tcp_np); + on->nc = new_net_crypto(on->log, mem, rng, ns, on->mono_time, net, dht, &auto_test_dht_funcs, &inf, on->tcp_np, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); on->onion_c = new_onion_client(on->log, mem, rng, on->mono_time, on->nc, dht, net); if (!on->onion_c) { diff --git a/auto_tests/scenarios/scenario_toxav_peer_offline_test.c b/auto_tests/scenarios/scenario_toxav_peer_offline_test.c index 62ac4e75be..54bcbdbedc 100644 --- a/auto_tests/scenarios/scenario_toxav_peer_offline_test.c +++ b/auto_tests/scenarios/scenario_toxav_peer_offline_test.c @@ -133,7 +133,7 @@ int main(int argc, char *argv[]) ToxScenarioStatus res = tox_scenario_run(s); if (res != TOX_SCENARIO_DONE) { - fprintf(stderr, "Scenario failed with status %d\n", res); + fprintf(stderr, "Scenario failed with status %u\n", res); return 1; } diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index c1daebbcf8..a896ed7e1d 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -15,6 +15,7 @@ sh_test( args = ["$(locations %s)" % f for f in CIMPLE_FILES] + [ "-Wno-boolean-return", "-Wno-callback-names", + "-Wno-callgraph", "-Wno-enum-from-int", "-Wno-nullability", "-Wno-ownership-decls", diff --git a/toxcore/BUILD.bazel b/toxcore/BUILD.bazel index e8b2ac755d..ad34a988be 100644 --- a/toxcore/BUILD.bazel +++ b/toxcore/BUILD.bazel @@ -340,6 +340,33 @@ cc_library( ], ) +cc_library( + name = "noise", + srcs = ["noise.c"], + hdrs = ["noise.h"], + visibility = ["//c-toxcore:__subpackages__"], + deps = [ + ":attributes", + ":ccompat", + ":crypto_core", + "@libsodium", + ], +) + +cc_test( + name = "noise_test", + size = "small", + srcs = ["noise_test.cc"], + deps = [ + ":crypto_core", + ":crypto_core_test_util", + ":noise", + "//c-toxcore/testing/support", + "@com_google_googletest//:gtest", + "@com_google_googletest//:gtest_main", + ], +) + cc_library( name = "crypto_core_pack", srcs = ["crypto_core_pack.c"], @@ -375,6 +402,7 @@ cc_test( deps = [ ":crypto_core", ":crypto_core_test_util", + ":noise", ":util", "//c-toxcore/testing/support", "@com_google_googletest//:gtest", @@ -1030,6 +1058,7 @@ cc_library( ":net", ":net_profile", ":network", + ":noise", ":rng", ":util", "@pthread", @@ -1038,7 +1067,7 @@ cc_library( cc_test( name = "net_crypto_test", - size = "small", + size = "medium", srcs = ["net_crypto_test.cc"], deps = [ ":DHT_test_util", diff --git a/toxcore/Makefile.inc b/toxcore/Makefile.inc index e1ee928038..52a21deceb 100644 --- a/toxcore/Makefile.inc +++ b/toxcore/Makefile.inc @@ -64,6 +64,8 @@ libtoxcore_la_SOURCES = ../third_party/cmp/cmp.c \ ../toxcore/crypto_core_pack.h \ ../toxcore/crypto_core.c \ ../toxcore/crypto_core.h \ + ../toxcore/noise.c \ + ../toxcore/noise.h \ ../toxcore/DHT.c \ ../toxcore/DHT.h \ ../toxcore/ev.c \ diff --git a/toxcore/Messenger.c b/toxcore/Messenger.c index 53802c4f0e..be2caa9488 100644 --- a/toxcore/Messenger.c +++ b/toxcore/Messenger.c @@ -3470,7 +3470,7 @@ Messenger *new_messenger(Mono_Time *mono_time, const Memory *mem, const Random * } m->tcp_np = tcp_np; - Net_Crypto *net_crypto = new_net_crypto(m->log, m->mem, m->rng, m->ns, m->mono_time, m->net, m->dht, &m_dht_funcs, &options->proxy_info, m->tcp_np); + Net_Crypto *net_crypto = new_net_crypto(m->log, m->mem, m->rng, m->ns, m->mono_time, m->net, m->dht, &m_dht_funcs, &options->proxy_info, m->tcp_np, options->handshake_mode); if (net_crypto == nullptr) { LOGGER_WARNING(m->log, "net_crypto initialisation failed"); diff --git a/toxcore/Messenger.h b/toxcore/Messenger.h index 201ff3c62e..8ca9f5c493 100644 --- a/toxcore/Messenger.h +++ b/toxcore/Messenger.h @@ -77,6 +77,7 @@ typedef struct Messenger_Options { Logger *_Nonnull log; bool ipv6enabled; + Crypto_Handshake_Mode handshake_mode; bool udp_disabled; TCP_Proxy_Info proxy_info; uint16_t port_range[2]; diff --git a/toxcore/crypto_core.c b/toxcore/crypto_core.c index 26da11d9c0..82a9faed44 100644 --- a/toxcore/crypto_core.c +++ b/toxcore/crypto_core.c @@ -28,6 +28,8 @@ static_assert(CRYPTO_MAC_SIZE == crypto_box_MACBYTES, "CRYPTO_MAC_SIZE should be equal to crypto_box_MACBYTES"); static_assert(CRYPTO_NONCE_SIZE == crypto_box_NONCEBYTES, "CRYPTO_NONCE_SIZE should be equal to crypto_box_NONCEBYTES"); +static_assert(CRYPTO_NOISE_NONCE_SIZE == crypto_stream_chacha20_ietf_NONCEBYTES, + "CRYPTO_NOISE_NONCE_SIZE should be equal to crypto_stream_chacha20_ietf_NONCEBYTES"); static_assert(CRYPTO_HMAC_SIZE == crypto_auth_BYTES, "CRYPTO_HMAC_SIZE should be equal to crypto_auth_BYTES"); static_assert(CRYPTO_HMAC_KEY_SIZE == crypto_auth_KEYBYTES, @@ -46,6 +48,12 @@ static_assert(CRYPTO_SIGN_PUBLIC_KEY_SIZE == crypto_sign_PUBLICKEYBYTES, static_assert(CRYPTO_SIGN_SECRET_KEY_SIZE == crypto_sign_SECRETKEYBYTES, "CRYPTO_SIGN_SECRET_KEY_SIZE should be equal to crypto_sign_SECRETKEYBYTES"); + +static_assert(CRYPTO_MAC_SIZE == crypto_aead_chacha20poly1305_IETF_ABYTES, + "CRYPTO_MAC_SIZE should be equal to crypto_aead_chacha20poly1305_IETF_ABYTES"); +static_assert(CRYPTO_SHARED_KEY_SIZE == CRYPTO_SYMMETRIC_KEY_SIZE, + "CRYPTO_SHARED_KEY_SIZE should be equal to CRYPTO_SYMMETRIC_KEY_SIZE"); + bool create_extended_keypair(Extended_Public_Key *pk, Extended_Secret_Key *sk, const Random *rng) { /* create signature key pair */ @@ -235,7 +243,7 @@ int32_t encrypt_data_symmetric(const Memory *mem, const uint8_t nonce[CRYPTO_NONCE_SIZE], const uint8_t *plain, size_t length, uint8_t *encrypted) { - if (length == 0 || shared_key == nullptr || nonce == nullptr || plain == nullptr || encrypted == nullptr) { + if (length == 0 || length >= INT32_MAX - crypto_box_MACBYTES || shared_key == nullptr || nonce == nullptr || plain == nullptr || encrypted == nullptr) { return -1; } @@ -280,7 +288,6 @@ int32_t encrypt_data_symmetric(const Memory *mem, crypto_free(mem, temp_plain, size_temp_plain); crypto_free(mem, temp_encrypted, size_temp_encrypted); #endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */ - assert(length < INT32_MAX - crypto_box_MACBYTES); return (int32_t)(length + crypto_box_MACBYTES); } @@ -289,13 +296,12 @@ int32_t decrypt_data_symmetric(const Memory *mem, const uint8_t nonce[CRYPTO_NONCE_SIZE], const uint8_t *encrypted, size_t length, uint8_t *plain) { - if (length <= crypto_box_BOXZEROBYTES || shared_key == nullptr || nonce == nullptr || encrypted == nullptr + if (length <= crypto_box_BOXZEROBYTES || length >= INT32_MAX || shared_key == nullptr || nonce == nullptr || encrypted == nullptr || plain == nullptr) { return -1; } #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION - assert(length >= crypto_box_MACBYTES); memcpy(plain, encrypted, length - crypto_box_MACBYTES); // Don't encrypt anything #else @@ -332,8 +338,6 @@ int32_t decrypt_data_symmetric(const Memory *mem, crypto_free(mem, temp_plain, size_temp_plain); crypto_free(mem, temp_encrypted, size_temp_encrypted); #endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */ - assert(length > crypto_box_MACBYTES); - assert(length < INT32_MAX); return (int32_t)(length - crypto_box_MACBYTES); } @@ -373,10 +377,7 @@ int32_t decrypt_data(const Memory *mem, void increment_nonce(uint8_t nonce[CRYPTO_NONCE_SIZE]) { - /* TODO(irungentoo): use `increment_nonce_number(nonce, 1)` or - * sodium_increment (change to little endian). - * - * NOTE don't use breaks inside this loop. + /* NOTE don't use breaks inside this loop. * In particular, make sure, as far as possible, * that loop bounds and their potential underflow or overflow * are independent of user-controlled input (you may have heard of the Heartbleed bug). @@ -488,3 +489,92 @@ void random_bytes(const Random *rng, uint8_t *bytes, size_t length) { rng_bytes(rng, bytes, length); } + +// Necessary functions for Noise, cf. https://noiseprotocol.org/noise.html (Revision 34) + +int32_t encrypt_data_symmetric_aead(const uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE], const uint8_t nonce[CRYPTO_NOISE_NONCE_SIZE], + const uint8_t *plain, size_t plain_length, uint8_t *encrypted, + const uint8_t *ad, size_t ad_length) +{ + if (plain_length == 0 || plain_length >= INT32_MAX - crypto_aead_chacha20poly1305_IETF_ABYTES + || shared_key == nullptr || nonce == nullptr || plain == nullptr || encrypted == nullptr) { + return -1; + } + +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + memcpy(encrypted, plain, plain_length); + memzero(encrypted + plain_length, crypto_aead_chacha20poly1305_IETF_ABYTES); +#else + if (crypto_aead_chacha20poly1305_ietf_encrypt(encrypted, nullptr, plain, plain_length, + ad, ad_length, nullptr, nonce, shared_key) != 0) { + return -1; + } +#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */ + + return (int32_t)(plain_length + crypto_aead_chacha20poly1305_IETF_ABYTES); +} + +int32_t decrypt_data_symmetric_aead(const uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE], const uint8_t nonce[CRYPTO_NOISE_NONCE_SIZE], + const uint8_t *encrypted, size_t encrypted_length, uint8_t *plain, + const uint8_t *ad, size_t ad_length) +{ + if (encrypted_length <= crypto_aead_chacha20poly1305_IETF_ABYTES || encrypted_length >= INT32_MAX + || shared_key == nullptr || nonce == nullptr || encrypted == nullptr || plain == nullptr) { + return -1; + } + +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + memcpy(plain, encrypted, encrypted_length - crypto_aead_chacha20poly1305_IETF_ABYTES); +#else + if (crypto_aead_chacha20poly1305_ietf_decrypt(plain, nullptr, nullptr, encrypted, + encrypted_length, ad, ad_length, nonce, shared_key) != 0) { + return -1; + } +#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */ + + return (int32_t)(encrypted_length - crypto_aead_chacha20poly1305_IETF_ABYTES); +} + +int32_t encrypt_data_symmetric_xaead(const uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE], const uint8_t nonce[CRYPTO_NONCE_SIZE], + const uint8_t *plain, size_t plain_length, uint8_t *encrypted, + const uint8_t *ad, size_t ad_length) +{ + if (plain_length == 0 || plain_length >= INT32_MAX - crypto_aead_xchacha20poly1305_ietf_ABYTES + || shared_key == nullptr || nonce == nullptr || plain == nullptr || encrypted == nullptr) { + return -1; + } + +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + memcpy(encrypted, plain, plain_length); + memzero(encrypted + plain_length, crypto_aead_xchacha20poly1305_ietf_ABYTES); +#else + if (crypto_aead_xchacha20poly1305_ietf_encrypt(encrypted, nullptr, plain, plain_length, + ad, ad_length, nullptr, nonce, shared_key) != 0) { + return -1; + } +#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */ + + return (int32_t)(plain_length + crypto_aead_xchacha20poly1305_ietf_ABYTES); +} + +int32_t decrypt_data_symmetric_xaead(const uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE], const uint8_t nonce[CRYPTO_NONCE_SIZE], + const uint8_t *encrypted, size_t encrypted_length, uint8_t *plain, + const uint8_t *ad, size_t ad_length) +{ + if (encrypted_length <= crypto_aead_xchacha20poly1305_ietf_ABYTES || encrypted_length >= INT32_MAX + || shared_key == nullptr || nonce == nullptr || encrypted == nullptr || plain == nullptr) { + return -1; + } + +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + memcpy(plain, encrypted, encrypted_length - crypto_aead_xchacha20poly1305_ietf_ABYTES); +#else + if (crypto_aead_xchacha20poly1305_ietf_decrypt(plain, nullptr, nullptr, encrypted, + encrypted_length, ad, ad_length, nonce, shared_key) != 0) { + return -1; + } +#endif /* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION */ + + return (int32_t)(encrypted_length - crypto_aead_xchacha20poly1305_ietf_ABYTES); +} + diff --git a/toxcore/crypto_core.h b/toxcore/crypto_core.h index 74543dcc79..c33d4a66a3 100644 --- a/toxcore/crypto_core.h +++ b/toxcore/crypto_core.h @@ -69,6 +69,11 @@ extern "C" { */ #define CRYPTO_NONCE_SIZE 24 +/** + * @brief NoiseIK: The number of bytes in a nonce used for encryption/decryption (ChaChaPoly1305-IETF). + */ +#define CRYPTO_NOISE_NONCE_SIZE 12 + /** * @brief The number of bytes in a SHA256 hash. */ @@ -79,6 +84,7 @@ extern "C" { */ #define CRYPTO_SHA512_SIZE 64 + /** * @brief The number of bytes in an encryption public key used by DHT group chats. */ @@ -278,9 +284,6 @@ typedef struct Extended_Secret_Key { * @brief Creates an extended keypair: curve25519 and ed25519 for encryption and signing * respectively. The Encryption keys are derived from the signature keys. * - * NOTE: This does *not* use Random, so any code using this will not be fuzzable. - * TODO: Make it use Random. - * * @param[out] pk The buffer where the public key will be stored. Must have room for EXT_PUBLIC_KEY_SIZE bytes. * @param[out] sk The buffer where the secret key will be stored. Must have room for EXT_SECRET_KEY_SIZE bytes. * @param rng The random number generator to use for the key generator seed. @@ -417,6 +420,64 @@ bool crypto_memunlock(void *_Nonnull data, size_t length); */ void new_hmac_key(const Random *_Nonnull rng, uint8_t key[_Nonnull CRYPTO_HMAC_KEY_SIZE]); +/** + * @brief Encrypt message with precomputed shared key using ChaCha20-Poly1305-IETF (RFC7539). + * + * Encrypts plain of plain_length to encrypted of plain_length + @ref CRYPTO_MAC_SIZE + * using a shared key @ref CRYPTO_SHARED_KEY_SIZE big and a @ref CRYPTO_NOISE_NONCE_SIZE + * byte nonce. The encrypted message, as well as a tag authenticating both the confidential + * message m and adlen bytes of non-confidential data ad, are put into encrypted. + * + * @retval -1 if there was a problem. + * @return length of encrypted data if everything was fine. + */ +int32_t encrypt_data_symmetric_aead(const uint8_t shared_key[_Nonnull CRYPTO_SHARED_KEY_SIZE], const uint8_t nonce[_Nonnull CRYPTO_NOISE_NONCE_SIZE], const uint8_t *_Nonnull plain, + size_t plain_length, + uint8_t encrypted[_Nonnull /*! plain_length + CRYPTO_MAC_SIZE */], const uint8_t *_Nullable ad, size_t ad_length); + +/** + * @brief Decrypt message with precomputed shared key using ChaCha20-Poly1305-IETF (RFC7539). + * + * Decrypts encrypted of encrypted_length to plain of length + * `encrypted_length - CRYPTO_MAC_SIZE` using a shared key @ref CRYPTO_SHARED_KEY_SIZE + * big and a @ref CRYPTO_NOISE_NONCE_SIZE byte nonce. + * + * @retval -1 if there was a problem (decryption failed). + * @return length of plain data if everything was fine. + */ +int32_t decrypt_data_symmetric_aead(const uint8_t shared_key[_Nonnull CRYPTO_SHARED_KEY_SIZE], const uint8_t nonce[_Nonnull CRYPTO_NOISE_NONCE_SIZE], const uint8_t *_Nonnull encrypted, + size_t encrypted_length, + uint8_t *_Nonnull plain, const uint8_t *_Nullable ad, size_t ad_length); + +/** + * @brief Encrypt message with precomputed shared key using XChaCha20-Poly1305. + * + * Encrypts plain of plain_length to encrypted of plain_length + @ref CRYPTO_MAC_SIZE + * using a shared key @ref CRYPTO_SYMMETRIC_KEY_SIZE big and a @ref CRYPTO_NONCE_SIZE + * byte nonce. The encrypted message, as well as a tag authenticating both the confidential + * message m and adlen bytes of non-confidential data ad, are put into encrypted. + * + * @retval -1 if there was a problem. + * @return length of encrypted data if everything was fine. + */ +int32_t encrypt_data_symmetric_xaead(const uint8_t shared_key[_Nonnull CRYPTO_SHARED_KEY_SIZE], const uint8_t nonce[_Nonnull CRYPTO_NONCE_SIZE], const uint8_t *_Nonnull plain, size_t plain_length, + uint8_t *_Nonnull encrypted, const uint8_t *_Nullable ad, size_t ad_length); + +/** + * @brief Decrypt message with precomputed shared key using XChaCha20-Poly1305. + * + * Decrypts encrypted of encrypted_length to plain of length + * `encrypted_length - CRYPTO_MAC_SIZE` using a shared key @ref CRYPTO_SHARED_KEY_SIZE + * big and a @ref CRYPTO_NONCE_SIZE byte nonce. + * + * @retval -1 if there was a problem (decryption failed). + * @return length of plain data if everything was fine. + */ +int32_t decrypt_data_symmetric_xaead(const uint8_t shared_key[_Nonnull CRYPTO_SHARED_KEY_SIZE], const uint8_t nonce[_Nonnull CRYPTO_NONCE_SIZE], const uint8_t *_Nonnull encrypted, + size_t encrypted_length, + uint8_t *_Nonnull plain, const uint8_t *_Nullable ad, size_t ad_length); + + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/toxcore/crypto_core_test.cc b/toxcore/crypto_core_test.cc index 256f5b1875..2398f89a98 100644 --- a/toxcore/crypto_core_test.cc +++ b/toxcore/crypto_core_test.cc @@ -149,4 +149,285 @@ TEST(CryptoCore, Hmac) } } +TEST(CryptoCore, ExtendedKeyAccessors) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + Extended_Public_Key pk; + Extended_Secret_Key sk; + ASSERT_TRUE(create_extended_keypair(&pk, &sk, &c_rng)); + + EXPECT_EQ(memcmp(get_enc_key(&pk), pk.enc, CRYPTO_PUBLIC_KEY_SIZE), 0); + EXPECT_EQ(memcmp(get_sig_pk(&pk), pk.sig, CRYPTO_SIGN_PUBLIC_KEY_SIZE), 0); + EXPECT_EQ(memcmp(get_sig_sk(&sk), sk.sig, CRYPTO_SIGN_SECRET_KEY_SIZE), 0); + EXPECT_EQ(memcmp(get_chat_id(&pk), pk.sig, CRYPTO_SIGN_PUBLIC_KEY_SIZE), 0); + + uint8_t new_sig[CRYPTO_SIGN_PUBLIC_KEY_SIZE]; + random_bytes(&c_rng, new_sig, sizeof(new_sig)); + set_sig_pk(&pk, new_sig); + EXPECT_EQ(memcmp(pk.sig, new_sig, CRYPTO_SIGN_PUBLIC_KEY_SIZE), 0); +} + +TEST(CryptoCore, PublicKeyValid) +{ + uint8_t pk[CRYPTO_PUBLIC_KEY_SIZE]; + memset(pk, 0, sizeof(pk)); + pk[31] = 127; + EXPECT_TRUE(public_key_valid(pk)); + + pk[31] = 128; + EXPECT_FALSE(public_key_valid(pk)); + pk[31] = 255; + EXPECT_FALSE(public_key_valid(pk)); +} + +TEST(CryptoCore, CryptoMemzero) +{ + uint8_t data[100]; + memset(data, 0xAA, sizeof(data)); + crypto_memzero(data, sizeof(data)); + for (size_t i = 0; i < sizeof(data); ++i) { + EXPECT_EQ(data[i], 0); + } +} + +TEST(CryptoCore, ChecksumEq) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + uint8_t h1[CRYPTO_SHA512_SIZE], h2[CRYPTO_SHA512_SIZE]; + random_bytes(&c_rng, h1, sizeof(h1)); + memcpy(h2, h1, sizeof(h1)); + EXPECT_TRUE(crypto_sha512_eq(h1, h2)); + h2[0] ^= 1; + EXPECT_FALSE(crypto_sha512_eq(h1, h2)); + + uint8_t s1[CRYPTO_SHA256_SIZE], s2[CRYPTO_SHA256_SIZE]; + random_bytes(&c_rng, s1, sizeof(s1)); + memcpy(s2, s1, sizeof(s1)); + EXPECT_TRUE(crypto_sha256_eq(s1, s2)); + s2[0] ^= 1; + EXPECT_FALSE(crypto_sha256_eq(s1, s2)); +} + +TEST(CryptoCore, RandomFunctions) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + + // Just verify they run and return values in range (where applicable) + // Detailed statistical testing is out of scope. + EXPECT_LE(random_u08(&c_rng), 255); + + uint16_t r16 = random_u16(&c_rng); + (void)r16; + + uint32_t r32 = random_u32(&c_rng); + (void)r32; + + uint64_t r64 = random_u64(&c_rng); + (void)r64; + + for (int i = 0; i < 100; ++i) { + uint32_t bound = 10; + EXPECT_LT(random_range_u32(&c_rng, bound), bound); + } +} + +TEST(CryptoCore, RandomNonce) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + uint8_t n1[CRYPTO_NONCE_SIZE]; + uint8_t n2[CRYPTO_NONCE_SIZE]; + random_nonce(&c_rng, n1); + random_nonce(&c_rng, n2); + EXPECT_NE(memcmp(n1, n2, sizeof(n1)), 0); +} + +TEST(CryptoCore, RandomBytes) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + uint8_t b1[32]; + uint8_t b2[32]; + random_bytes(&c_rng, b1, sizeof(b1)); + random_bytes(&c_rng, b2, sizeof(b2)); + EXPECT_NE(memcmp(b1, b2, sizeof(b1)), 0); +} + +TEST(CryptoCore, SymmetricEncryption) +{ + SimulatedEnvironment env{12345}; + auto c_mem = env.fake_memory().c_memory(); + auto c_rng = env.fake_random().c_random(); + + uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; + new_symmetric_key(&c_rng, shared_key); + + uint8_t nonce[CRYPTO_NONCE_SIZE]; + random_nonce(&c_rng, nonce); + + const std::string plaintext = "Hello, Tox!"; + std::vector encrypted(plaintext.size() + CRYPTO_MAC_SIZE); + std::vector decrypted(plaintext.size()); + + int len = encrypt_data_symmetric(&c_mem, shared_key, nonce, + reinterpret_cast(plaintext.data()), plaintext.size(), encrypted.data()); + EXPECT_EQ(len, plaintext.size() + CRYPTO_MAC_SIZE); + + len = decrypt_data_symmetric( + &c_mem, shared_key, nonce, encrypted.data(), encrypted.size(), decrypted.data()); + EXPECT_EQ(len, plaintext.size()); + EXPECT_EQ(memcmp(plaintext.data(), decrypted.data(), plaintext.size()), 0); + + // Test decryption failure with wrong key + uint8_t wrong_key[CRYPTO_SHARED_KEY_SIZE]; + new_symmetric_key(&c_rng, wrong_key); + len = decrypt_data_symmetric( + &c_mem, wrong_key, nonce, encrypted.data(), encrypted.size(), decrypted.data()); + EXPECT_EQ(len, -1); +} + +TEST(CryptoCore, EncryptPrecompute) +{ + SimulatedEnvironment env{12345}; + auto c_mem = env.fake_memory().c_memory(); + auto c_rng = env.fake_random().c_random(); + + PublicKey pk; + SecretKey sk; + crypto_new_keypair(&c_rng, pk.data(), sk.data()); + + uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; + encrypt_precompute(pk.data(), sk.data(), shared_key); + + uint8_t nonce[CRYPTO_NONCE_SIZE]; + random_nonce(&c_rng, nonce); + + const std::string plaintext = "Precompute Test"; + std::vector encrypted(plaintext.size() + CRYPTO_MAC_SIZE); + std::vector decrypted(plaintext.size()); + + encrypt_data_symmetric(&c_mem, shared_key, nonce, + reinterpret_cast(plaintext.data()), plaintext.size(), encrypted.data()); + + decrypt_data_symmetric( + &c_mem, shared_key, nonce, encrypted.data(), encrypted.size(), decrypted.data()); + + EXPECT_EQ(memcmp(plaintext.data(), decrypted.data(), plaintext.size()), 0); +} + +TEST(CryptoCore, Hashing) +{ + const std::string data = "Hash me!"; + uint8_t hash256[CRYPTO_SHA256_SIZE]; + uint8_t hash512[CRYPTO_SHA512_SIZE]; + + crypto_sha256(hash256, reinterpret_cast(data.data()), data.size()); + crypto_sha512(hash512, reinterpret_cast(data.data()), data.size()); + + // Basic check: hashes should not be all zeros + uint8_t zeros[CRYPTO_SHA512_SIZE] = {0}; + EXPECT_NE(memcmp(hash256, zeros, CRYPTO_SHA256_SIZE), 0); + EXPECT_NE(memcmp(hash512, zeros, CRYPTO_SHA512_SIZE), 0); + + // Deterministic check + uint8_t hash256_2[CRYPTO_SHA256_SIZE]; + crypto_sha256(hash256_2, reinterpret_cast(data.data()), data.size()); + EXPECT_EQ(memcmp(hash256, hash256_2, CRYPTO_SHA256_SIZE), 0); +} + +TEST(CryptoCore, AsymmetricEncryption) +{ + SimulatedEnvironment env{12345}; + auto c_mem = env.fake_memory().c_memory(); + auto c_rng = env.fake_random().c_random(); + + PublicKey pk; + SecretKey sk; + crypto_new_keypair(&c_rng, pk.data(), sk.data()); + + // Verify key derivation + PublicKey derived_pk; + crypto_derive_public_key(derived_pk.data(), sk.data()); + EXPECT_EQ(pk, derived_pk); + + uint8_t nonce[CRYPTO_NONCE_SIZE]; + random_nonce(&c_rng, nonce); + + const std::string plaintext = "Asymmetric Test"; + std::vector encrypted(plaintext.size() + CRYPTO_MAC_SIZE); + std::vector decrypted(plaintext.size()); + + int len = encrypt_data(&c_mem, pk.data(), sk.data(), nonce, + reinterpret_cast(plaintext.data()), plaintext.size(), encrypted.data()); + EXPECT_EQ(len, plaintext.size() + CRYPTO_MAC_SIZE); + + len = decrypt_data( + &c_mem, pk.data(), sk.data(), nonce, encrypted.data(), encrypted.size(), decrypted.data()); + EXPECT_EQ(len, plaintext.size()); + EXPECT_EQ(memcmp(plaintext.data(), decrypted.data(), plaintext.size()), 0); +} + +TEST(CryptoCore, AEAD_ChaCha20Poly1305) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + + uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; + new_symmetric_key(&c_rng, shared_key); + + uint8_t nonce[CRYPTO_NOISE_NONCE_SIZE]; // 12 bytes + random_bytes(&c_rng, nonce, sizeof(nonce)); + + const std::string plaintext = "AEAD Test Data"; + const std::string ad = "Associated Data"; + std::vector encrypted(plaintext.size() + CRYPTO_MAC_SIZE); + std::vector decrypted(plaintext.size()); + + int len = encrypt_data_symmetric_aead(shared_key, nonce, + reinterpret_cast(plaintext.data()), plaintext.size(), encrypted.data(), + reinterpret_cast(ad.data()), ad.size()); + EXPECT_EQ(len, plaintext.size() + CRYPTO_MAC_SIZE); + + len = decrypt_data_symmetric_aead(shared_key, nonce, encrypted.data(), encrypted.size(), + decrypted.data(), reinterpret_cast(ad.data()), ad.size()); + EXPECT_EQ(len, plaintext.size()); + EXPECT_EQ(memcmp(plaintext.data(), decrypted.data(), plaintext.size()), 0); + + // Fail with wrong AD + const std::string wrong_ad = "Wrong Data"; + len = decrypt_data_symmetric_aead(shared_key, nonce, encrypted.data(), encrypted.size(), + decrypted.data(), reinterpret_cast(wrong_ad.data()), wrong_ad.size()); + EXPECT_EQ(len, -1); +} + +TEST(CryptoCore, AEAD_XChaCha20Poly1305) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + + uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; + new_symmetric_key(&c_rng, shared_key); + + uint8_t nonce[CRYPTO_NONCE_SIZE]; // 24 bytes + random_nonce(&c_rng, nonce); + + const std::string plaintext = "XAEAD Test Data"; + const std::string ad = "X Associated Data"; + std::vector encrypted(plaintext.size() + CRYPTO_MAC_SIZE); + std::vector decrypted(plaintext.size()); + + int len = encrypt_data_symmetric_xaead(shared_key, nonce, + reinterpret_cast(plaintext.data()), plaintext.size(), encrypted.data(), + reinterpret_cast(ad.data()), ad.size()); + EXPECT_EQ(len, plaintext.size() + CRYPTO_MAC_SIZE); + + len = decrypt_data_symmetric_xaead(shared_key, nonce, encrypted.data(), encrypted.size(), + decrypted.data(), reinterpret_cast(ad.data()), ad.size()); + EXPECT_EQ(len, plaintext.size()); + EXPECT_EQ(memcmp(plaintext.data(), decrypted.data(), plaintext.size()), 0); +} + } // namespace diff --git a/toxcore/friend_connection.c b/toxcore/friend_connection.c index c7ce9d8147..ab9806f7db 100644 --- a/toxcore/friend_connection.c +++ b/toxcore/friend_connection.c @@ -548,7 +548,7 @@ static int handle_lossy_packet(void *_Nonnull object, int id, const uint8_t *_No static int handle_new_connections(void *_Nonnull object, const New_Connection *_Nonnull n_c) { Friend_Connections *const fr_c = (Friend_Connections *)object; - const int friendcon_id = getfriend_conn_id_pk(fr_c, n_c->public_key); + const int friendcon_id = getfriend_conn_id_pk(fr_c, n_c->peer_id_public_key); Friend_Conn *const friend_con = get_conn(fr_c, friendcon_id); if (friend_con == nullptr) { @@ -577,8 +577,8 @@ static int handle_new_connections(void *_Nonnull object, const New_Connection *_ friend_con->dht_ip_port_lastrecv = mono_time_get(fr_c->mono_time); } - if (!pk_equal(friend_con->dht_temp_pk, n_c->dht_public_key)) { - change_dht_pk(fr_c, friendcon_id, n_c->dht_public_key); + if (!pk_equal(friend_con->dht_temp_pk, n_c->peer_dht_public_key)) { + change_dht_pk(fr_c, friendcon_id, n_c->peer_dht_public_key); } nc_dht_pk_callback(fr_c->net_crypto, id, &dht_pk_callback, fr_c, friendcon_id); diff --git a/toxcore/friend_connection_test.cc b/toxcore/friend_connection_test.cc index 1b0a370242..3ca701520d 100644 --- a/toxcore/friend_connection_test.cc +++ b/toxcore/friend_connection_test.cc @@ -54,7 +54,7 @@ class FriendConnTestNode { net_crypto_.reset(new_net_crypto(dht_wrapper_.logger(), &dht_wrapper_.node().c_memory, &dht_wrapper_.node().c_random, &dht_wrapper_.node().c_network, dht_wrapper_.mono_time(), dht_wrapper_.networking(), dht_wrapper_.get_dht(), &DHTWrapper::funcs, &proxy_info, - net_profile_.get())); + net_profile_.get(), CRYPTO_HANDSHAKE_MODE_NOISE_ONLY)); new_keys(net_crypto_.get()); diff --git a/toxcore/friend_requests.c b/toxcore/friend_requests.c index 2a13dd2225..79e44a616f 100644 --- a/toxcore/friend_requests.c +++ b/toxcore/friend_requests.c @@ -113,7 +113,7 @@ int remove_request_received(Friend_Requests *fr, const uint8_t *real_pk) { for (uint32_t i = 0; i < MAX_RECEIVED_STORED; ++i) { if (pk_equal(fr->received.requests[i], real_pk)) { - crypto_memzero(fr->received.requests[i], CRYPTO_PUBLIC_KEY_SIZE); + memset(fr->received.requests[i], 0, CRYPTO_PUBLIC_KEY_SIZE); return 0; } } diff --git a/toxcore/group.c b/toxcore/group.c index 8c782ddb5e..b7178c730e 100644 --- a/toxcore/group.c +++ b/toxcore/group.c @@ -3044,7 +3044,7 @@ static int lossy_packet_not_received(const Group_c *_Nonnull g, int peer_index, const uint16_t top_distance = message_number - g->group[peer_index].top_lossy_number; if (top_distance >= MAX_LOSSY_COUNT) { - crypto_memzero(g->group[peer_index].recv_lossy, sizeof(g->group[peer_index].recv_lossy)); + memset(g->group[peer_index].recv_lossy, 0, sizeof(g->group[peer_index].recv_lossy)); } else { // top_distance < MAX_LOSSY_COUNT for (unsigned int i = g->group[peer_index].bottom_lossy_number; i != g->group[peer_index].bottom_lossy_number + top_distance; diff --git a/toxcore/net_crypto.c b/toxcore/net_crypto.c index 9496fba203..d699d1741c 100644 --- a/toxcore/net_crypto.c +++ b/toxcore/net_crypto.c @@ -5,6 +5,12 @@ /** * Functions for the core network crypto. + * This implements NoiseIK: + * + * <- s + * ... + * -> e, es, s, ss + * <- e, ee, se * * NOTE: This code has to be perfect. We don't mess around with encryption. */ @@ -27,7 +33,6 @@ #include "net_profile.h" #include "network.h" #include "util.h" - typedef struct Packet_Data { uint64_t sent_time; uint16_t length; @@ -55,16 +60,32 @@ typedef enum Crypto_Conn_State { } Crypto_Conn_State; typedef struct Crypto_Connection { - uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The real public key of the peer. */ - uint8_t recv_nonce[CRYPTO_NONCE_SIZE]; /* Nonce of received packets. */ - uint8_t sent_nonce[CRYPTO_NONCE_SIZE]; /* Nonce of sent packets. */ - uint8_t sessionpublic_key[CRYPTO_PUBLIC_KEY_SIZE]; /* Our public key for this session. */ - uint8_t sessionsecret_key[CRYPTO_SECRET_KEY_SIZE]; /* Our private key for this session. */ - uint8_t peersessionpublic_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The public key of the peer. */ - uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; /* The precomputed shared key from encrypt_precompute. */ + // TODO(goldroom): can be removed when legacy handshake is dropped (Noise derives this from the handshake) + uint8_t peer_id_public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The real/static identity public X25519 key of the peer. */ + + uint8_t recv_nonce[CRYPTO_NONCE_SIZE]; /* Nonce used to decrypt incoming packets after non-Noise and NoiseIK handshake. */ + uint8_t send_nonce[CRYPTO_NONCE_SIZE]; /* Nonce used to encrypt outgoing packets after non-Noise and NoiseIK handshake. */ + + // TODO(goldroom): only used by legacy handshake; can be removed when legacy is dropped + uint8_t ephemeral_public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* Our public ephemeral X25519 key for this session. */ + uint8_t ephemeral_secret_key[CRYPTO_SECRET_KEY_SIZE]; /* Our private ephemeral X25519 key for this session. */ + + // TODO(goldroom): only used by legacy handshake; can be removed when legacy is dropped + uint8_t peer_ephemeral_public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The public ephemeral X25519 key of the peer. */ + + /* The precomputed shared key from encrypt_precompute. + * Used for cookie requests/responses in non-Noise and NoiseIK handshake: and for transport payload encryption (non-Noise only). */ + uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; + Crypto_Conn_State status; /* See Crypto_Conn_State documentation */ uint64_t cookie_request_number; /* number used in the cookie request packets for this connection */ - uint8_t dht_public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The dht public key of the peer */ + uint8_t peer_dht_public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The DHT public X25519 key of the peer. */ + + bool noise_handshake_enabled; /* Necessary for Noise handshake backwards compatibility */ + bool legacy_ephemeral_keys_set; /* True when legacy ephemeral keys have been generated */ + Noise_Handshake *noise_handshake; /* NoiseIK handshake state, freed after Split() */ + uint8_t send_key[CRYPTO_SHARED_KEY_SIZE]; /* Symmetric key used to encrypt outgoing packets after NoiseIK handshake. */ + uint8_t recv_key[CRYPTO_SHARED_KEY_SIZE]; /* Symmetric key used to decrypt incoming packets after NoiseIK handshake. */ uint8_t *_Nullable temp_packet; /* Where the cookie request/handshake packet is stored while it is being sent. */ uint16_t temp_packet_length; @@ -149,11 +170,11 @@ struct Net_Crypto { uint32_t crypto_connections_length; /* Length of connections array. */ /* Our public and secret keys. */ - uint8_t self_public_key[CRYPTO_PUBLIC_KEY_SIZE]; - uint8_t self_secret_key[CRYPTO_SECRET_KEY_SIZE]; + uint8_t self_id_public_key[CRYPTO_PUBLIC_KEY_SIZE]; + uint8_t self_id_secret_key[CRYPTO_SECRET_KEY_SIZE]; /* The secret key used for cookies */ - uint8_t secret_symmetric_key[CRYPTO_SYMMETRIC_KEY_SIZE]; + uint8_t cookie_symmetric_key[CRYPTO_SYMMETRIC_KEY_SIZE]; new_connection_cb *_Nullable new_connection_callback; void *_Nullable new_connection_callback_object; @@ -166,16 +187,65 @@ struct Net_Crypto { /* Rate limiter for cookie requests */ uint64_t cookie_request_last_time; uint32_t cookie_request_tokens; + + /* Handshake mode selection: NOISE_ONLY, NOISE_BOTH, or LEGACY_ONLY */ + Crypto_Handshake_Mode handshake_mode; }; +/** @brief Free a Noise_Handshake, zeroing its contents first. + * + * Safe to call when `*nh` is null (no-op). Sets `*nh` to null afterwards. + */ +static void noise_handshake_free(const Memory *_Nonnull mem, Noise_Handshake *_Nullable *_Nonnull nh) +{ + if (*nh == nullptr) { + return; + } + + crypto_memzero(*nh, sizeof(Noise_Handshake)); + mem_delete(mem, *nh); + *nh = nullptr; +} + +/** @brief Perform Noise Split() and free handshake state. + * + * Derives transport send_key and recv_key from the Noise chaining_key via HKDF. + * The initiator gets (send=output1, recv=output2); the responder gets the reverse. + * Zeroes nonces, frees the handshake struct, and sets the pointer to null. + * + * NOTE: Standard Noise Split() produces keys for use with the handshake cipher + * (ChaChaPoly1305 with 12-byte nonce). Tox diverges by using the derived keys with + * XChaCha20-Poly1305 (24-byte nonce) for transport, to remain compatible with the + * existing packet format where only 2 bytes of nonce are sent on the wire. + */ +static void noise_split_and_cleanup(const Memory *_Nonnull mem, Crypto_Connection *_Nonnull conn) +{ + if (conn->noise_handshake == nullptr) { + return; + } + + if (conn->noise_handshake->initiator) { + noise_hkdf(conn->send_key, CRYPTO_SYMMETRIC_KEY_SIZE, conn->recv_key, CRYPTO_SYMMETRIC_KEY_SIZE, + nullptr, 0, conn->noise_handshake->chaining_key); + } else { + noise_hkdf(conn->recv_key, CRYPTO_SYMMETRIC_KEY_SIZE, conn->send_key, CRYPTO_SYMMETRIC_KEY_SIZE, + nullptr, 0, conn->noise_handshake->chaining_key); + } + + memset(conn->send_nonce, 0, CRYPTO_NONCE_SIZE); + memset(conn->recv_nonce, 0, CRYPTO_NONCE_SIZE); + + noise_handshake_free(mem, &conn->noise_handshake); +} + const uint8_t *nc_get_self_public_key(const Net_Crypto *c) { - return c->self_public_key; + return c->self_id_public_key; } const uint8_t *nc_get_self_secret_key(const Net_Crypto *c) { - return c->self_secret_key; + return c->self_id_secret_key; } TCP_Connections *nc_get_tcp_c(const Net_Crypto *c) @@ -224,9 +294,11 @@ static bool crypt_connection_id_is_valid(const Net_Crypto *_Nonnull c, int crypt */ static int create_cookie_request(const Net_Crypto *_Nonnull c, uint8_t *_Nonnull packet, const uint8_t *_Nonnull dht_public_key, uint64_t number, uint8_t *_Nonnull shared_key) { + // TODO(goldroom): consider Noise-specific cookie mechanism (e.g. WireGuard-style MAC cookies for DoS protection) uint8_t plain[COOKIE_REQUEST_PLAIN_LENGTH]; - memcpy(plain, c->self_public_key, CRYPTO_PUBLIC_KEY_SIZE); + memcpy(plain, c->self_id_public_key, CRYPTO_PUBLIC_KEY_SIZE); + // TODO(goldroom): padding for backwards-compatibility with older protocol versions; can this be removed? memzero(plain + CRYPTO_PUBLIC_KEY_SIZE, CRYPTO_PUBLIC_KEY_SIZE); memcpy(plain + (CRYPTO_PUBLIC_KEY_SIZE * 2), &number, sizeof(uint64_t)); const uint8_t *tmp_shared_key = c->dht_funcs->get_shared_key_sent(c->dht, dht_public_key); @@ -314,7 +386,7 @@ static int create_cookie_response(const Net_Crypto *_Nonnull c, uint8_t *_Nonnul memcpy(cookie_plain + CRYPTO_PUBLIC_KEY_SIZE, dht_public_key, CRYPTO_PUBLIC_KEY_SIZE); uint8_t plain[COOKIE_LENGTH + sizeof(uint64_t)]; - if (create_cookie(c->mem, c->rng, c->mono_time, plain, cookie_plain, c->secret_symmetric_key) != 0) { + if (create_cookie(c->mem, c->rng, c->mono_time, plain, cookie_plain, c->cookie_symmetric_key) != 0) { return -1; } @@ -489,107 +561,421 @@ static int handle_cookie_response(const Memory *_Nonnull mem, uint8_t *_Nonnull return COOKIE_LENGTH; } + +/* Legacy handshake packet length */ #define HANDSHAKE_PACKET_LENGTH (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE + COOKIE_LENGTH + CRYPTO_MAC_SIZE) -/** @brief Create a handshake packet and put it in packet. - * @param cookie must be COOKIE_LENGTH bytes. - * @param packet must be of size HANDSHAKE_PACKET_LENGTH or bigger. +/* + * Noise IK handshake packet layouts (Noise_IK_25519_ChaChaPoly_BLAKE2b). + * + * Initiator -> Responder (-> e, es, s, ss): 322 bytes + * [uint8_t 0x1c] 1 + * [uint8_t version] 1 (plaintext) + * [ephemeral public key] 32 (plaintext) + * [encrypted static public key + MAC] 48 (32 + 16) + * [encrypted payload + MAC] 240 (224 + 16) + * payload = [responder cookie 112] [initiator cookie 112] + * + * Responder -> Initiator (<- e, ee, se): 162 bytes + * [uint8_t 0x1c] 1 + * [uint8_t version] 1 (plaintext) + * [ephemeral public key] 32 (plaintext) + * [encrypted payload + MAC] 128 (112 + 16) + * payload = [initiator cookie 112] + */ +#define NOISE_HANDSHAKE_PACKET_LENGTH_INITIATOR (1 + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_MAC_SIZE + COOKIE_LENGTH + COOKIE_LENGTH + CRYPTO_MAC_SIZE) +#define NOISE_HANDSHAKE_PACKET_LENGTH_RESPONDER (1 + 1 + CRYPTO_PUBLIC_KEY_SIZE + COOKIE_LENGTH + CRYPTO_MAC_SIZE) +#define NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_INITIATOR (COOKIE_LENGTH + COOKIE_LENGTH) +#define NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_RESPONDER (COOKIE_LENGTH) + +/** @brief Current Noise protocol version. + * + * Bump this when making incompatible changes to the handshake (e.g. different + * cipher, different payload format). The version byte is sent in plaintext at + * position `[1]` of every Noise handshake packet, allowing the receiver to + * detect mismatches before initializing the Noise state. It is also used as the + * Noise prologue and mixed into the hash state, so any tampering causes AEAD + * failure. + */ +#define NOISE_PROTOCOL_VERSION 0x01 + +/** @brief Create a handshake packet (Noise or legacy). + * + * cf. Noise section 5.3 -> WriteMessage(payload, message_buffer) + * + * If noise_handshake is non-null, creates a Noise IK handshake packet. + * Otherwise creates a legacy handshake packet. + * + * @param peer_cookie must be COOKIE_LENGTH bytes. + * @param packet must be large enough for the resulting handshake type. + * @param send_nonce base nonce for legacy handshake (unused for Noise). + * @param ephemeral_private_key ephemeral private key (unused for Noise). + * @param ephemeral_public_key ephemeral public key (unused for Noise). + * @param peer_id_public_key static public key of the peer. + * @param peer_dht_public_key DHT public key of the peer. + * @param noise_handshake Noise state, or null for legacy. * * @retval -1 on failure. - * @retval HANDSHAKE_PACKET_LENGTH on success. + * @retval HANDSHAKE_PACKET_LENGTH on success (legacy). + * @retval NOISE_HANDSHAKE_PACKET_LENGTH_INITIATOR on success (Noise initiator). + * @retval NOISE_HANDSHAKE_PACKET_LENGTH_RESPONDER on success (Noise responder). */ -static int create_crypto_handshake(const Net_Crypto *_Nonnull c, uint8_t *_Nonnull packet, const uint8_t *_Nonnull cookie, const uint8_t *_Nonnull nonce, const uint8_t *_Nonnull session_pk, - const uint8_t *_Nonnull peer_real_pk, const uint8_t *_Nonnull peer_dht_pubkey) +static int create_crypto_handshake(const Net_Crypto *_Nonnull c, uint8_t *_Nonnull packet, const uint8_t peer_cookie[COOKIE_LENGTH], const uint8_t send_nonce[_Nullable CRYPTO_NONCE_SIZE], + const uint8_t ephemeral_private_key[_Nullable CRYPTO_SECRET_KEY_SIZE], + const uint8_t ephemeral_public_key[_Nullable CRYPTO_PUBLIC_KEY_SIZE], const uint8_t peer_id_public_key[_Nullable CRYPTO_PUBLIC_KEY_SIZE], const uint8_t peer_dht_public_key[CRYPTO_PUBLIC_KEY_SIZE], + Noise_Handshake *_Nullable noise_handshake) { - uint8_t plain[CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE + COOKIE_LENGTH]; - memcpy(plain, nonce, CRYPTO_NONCE_SIZE); - memcpy(plain + CRYPTO_NONCE_SIZE, session_pk, CRYPTO_PUBLIC_KEY_SIZE); - crypto_sha512(plain + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE, cookie, COOKIE_LENGTH); - uint8_t cookie_plain[COOKIE_DATA_LENGTH]; - memcpy(cookie_plain, peer_real_pk, CRYPTO_PUBLIC_KEY_SIZE); - memcpy(cookie_plain + CRYPTO_PUBLIC_KEY_SIZE, peer_dht_pubkey, CRYPTO_PUBLIC_KEY_SIZE); + /* Noise-based handshake */ + if (noise_handshake != nullptr) { + /* Noise INITIATOR: -> e, es, s, ss (see packet layout above defines) */ + if (noise_handshake->initiator) { + LOGGER_DEBUG(c->log, "Noise: INITIATOR"); - if (create_cookie(c->mem, c->rng, c->mono_time, plain + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE, - cookie_plain, c->secret_symmetric_key) != 0) { - return -1; - } + uint8_t *const hs = packet + 2; - random_nonce(c->rng, packet + 1 + COOKIE_LENGTH); - const int len = encrypt_data(c->mem, peer_real_pk, c->self_secret_key, packet + 1 + COOKIE_LENGTH, plain, sizeof(plain), - packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE); + /* e */ + memcpy(hs, noise_handshake->ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE); + noise_mix_hash(noise_handshake->hash, noise_handshake->ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE); - if (len != HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE)) { - return -1; - } + /* es */ + uint8_t noise_handshake_temp_key[CRYPTO_SHARED_KEY_SIZE]; + + if (noise_mix_key(noise_handshake->chaining_key, noise_handshake_temp_key, noise_handshake->ephemeral_private, noise_handshake->remote_static) != 0) { + return -1; + } + + /* s */ + if (noise_encrypt_and_hash(hs + CRYPTO_PUBLIC_KEY_SIZE, c->self_id_public_key, CRYPTO_PUBLIC_KEY_SIZE, noise_handshake_temp_key, + noise_handshake->hash) != 0) { + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + return -1; + } + + /* ss */ + if (noise_mix_key(noise_handshake->chaining_key, noise_handshake_temp_key, c->self_id_secret_key, noise_handshake->remote_static) != 0) { + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + return -1; + } + + /* Noise Handshake Payload */ + uint8_t handshake_payload_plain[NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_INITIATOR]; + + /* Include the responder's cookie (received earlier via cookie response). */ + memcpy(handshake_payload_plain, peer_cookie, COOKIE_LENGTH); + + uint8_t cookie_plain[COOKIE_DATA_LENGTH]; + memcpy(cookie_plain, noise_handshake->remote_static, CRYPTO_PUBLIC_KEY_SIZE); + memcpy(cookie_plain + CRYPTO_PUBLIC_KEY_SIZE, peer_dht_public_key, CRYPTO_PUBLIC_KEY_SIZE); + + /* Add the initiator's own cookie to the payload for the responder to echo back. */ + if (create_cookie(c->mem, c->rng, c->mono_time, handshake_payload_plain + COOKIE_LENGTH, + cookie_plain, c->cookie_symmetric_key) != 0) { + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + return -1; + } + + /* Nonce is always 0: each MixKey produces a fresh key, so reuse is safe. */ + if (noise_encrypt_and_hash(hs + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_MAC_SIZE, + handshake_payload_plain, sizeof(handshake_payload_plain), noise_handshake_temp_key, + noise_handshake->hash) != 0) { + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + crypto_memzero(handshake_payload_plain, NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_INITIATOR); + return -1; + } + + packet[0] = NET_PACKET_CRYPTO_NOISE_HS; + packet[1] = NOISE_PROTOCOL_VERSION; + + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + crypto_memzero(handshake_payload_plain, NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_INITIATOR); + + return NOISE_HANDSHAKE_PACKET_LENGTH_INITIATOR; + } else { + /* Noise RESPONDER: <- e, ee, se (see packet layout above defines) */ + LOGGER_DEBUG(c->log, "Noise: RESPONDER"); + + uint8_t *const hs = packet + 2; + + /* e */ + memcpy(hs, noise_handshake->ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE); + noise_mix_hash(noise_handshake->hash, noise_handshake->ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE); + + /* ee */ + uint8_t noise_handshake_temp_key[CRYPTO_SHARED_KEY_SIZE]; + + if (noise_mix_key(noise_handshake->chaining_key, noise_handshake_temp_key, noise_handshake->ephemeral_private, noise_handshake->remote_ephemeral) != 0) { + return -1; + } + + /* se: DH(e, rs) if responder: responder's ephemeral initiator's static */ + if (noise_mix_key(noise_handshake->chaining_key, noise_handshake_temp_key, noise_handshake->ephemeral_private, noise_handshake->remote_static) != 0) { + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + return -1; + } + + /* Create Noise Handshake Payload */ + uint8_t handshake_payload_plain[NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_RESPONDER]; + + /* Include the initiator's cookie to echo back. */ + memcpy(handshake_payload_plain, peer_cookie, COOKIE_LENGTH); + + /* Nonce is always 0: each MixKey produces a fresh key, so reuse is safe. */ + if (noise_encrypt_and_hash(hs + CRYPTO_PUBLIC_KEY_SIZE, + handshake_payload_plain, sizeof(handshake_payload_plain), noise_handshake_temp_key, + noise_handshake->hash) != 0) { + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + crypto_memzero(handshake_payload_plain, NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_RESPONDER); + return -1; + } + + packet[0] = NET_PACKET_CRYPTO_NOISE_HS; + packet[1] = NOISE_PROTOCOL_VERSION; + + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + crypto_memzero(handshake_payload_plain, NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_RESPONDER); + + return NOISE_HANDSHAKE_PACKET_LENGTH_RESPONDER; + } + } else { /* legacy handshake */ + if (send_nonce == nullptr || ephemeral_public_key == nullptr || peer_id_public_key == nullptr) { + return -1; + } - packet[0] = NET_PACKET_CRYPTO_HS; - memcpy(packet + 1, cookie, COOKIE_LENGTH); + uint8_t plain[CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE + COOKIE_LENGTH]; + memcpy(plain, send_nonce, CRYPTO_NONCE_SIZE); + memcpy(plain + CRYPTO_NONCE_SIZE, ephemeral_public_key, CRYPTO_PUBLIC_KEY_SIZE); + crypto_sha512(plain + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE, peer_cookie, COOKIE_LENGTH); + uint8_t cookie_plain[COOKIE_DATA_LENGTH]; + memcpy(cookie_plain, peer_id_public_key, CRYPTO_PUBLIC_KEY_SIZE); + memcpy(cookie_plain + CRYPTO_PUBLIC_KEY_SIZE, peer_dht_public_key, CRYPTO_PUBLIC_KEY_SIZE); - return HANDSHAKE_PACKET_LENGTH; + if (create_cookie(c->mem, c->rng, c->mono_time, plain + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE, + cookie_plain, c->cookie_symmetric_key) != 0) { + return -1; + } + + random_nonce(c->rng, packet + 1 + COOKIE_LENGTH); + const int len = encrypt_data(c->mem, peer_id_public_key, c->self_id_secret_key, packet + 1 + COOKIE_LENGTH, plain, sizeof(plain), + packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE); + + if (len != HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE)) { + return -1; + } + + packet[0] = NET_PACKET_CRYPTO_HS; + memcpy(packet + 1, peer_cookie, COOKIE_LENGTH); + + return HANDSHAKE_PACKET_LENGTH; + } } -/** @brief Handle a crypto handshake packet of length. - * put the nonce contained in the packet in nonce, - * the session public key in session_pk - * the real public key of the peer in peer_real_pk - * the dht public key of the peer in dht_public_key and - * the cookie inside the encrypted part of the packet in cookie. +/** @brief Parse and validate a received handshake packet (Noise or legacy). * - * if expected_real_pk isn't NULL it denotes the real public key - * the packet should be from. + * cf. Noise section 5.3 -> ReadMessage(payload, message_buffer) * - * nonce must be at least CRYPTO_NONCE_SIZE - * session_pk must be at least CRYPTO_PUBLIC_KEY_SIZE - * peer_real_pk must be at least CRYPTO_PUBLIC_KEY_SIZE - * cookie must be at least COOKIE_LENGTH + * If noise_handshake is non-null, parses as Noise IK. Otherwise as legacy. + * Output parameters that are null are skipped (Noise does not use all of them). + * + * @param packet received handshake packet. + * @param[out] recv_nonce peer's base nonce (legacy only). + * @param[out] peer_ephemeral_public_key peer's ephemeral key (legacy only). + * @param[out] peer_id_public_key peer's static identity key. + * @param[out] peer_dht_public_key peer's DHT key. + * @param[out] peer_cookie cookie from the peer. + * @param expected_peer_id_pk expected peer identity key, or null to skip check. + * @param noise_handshake Noise state, or null for legacy. * * @retval false on failure. * @retval true on success. */ -static bool handle_crypto_handshake(const Net_Crypto *_Nonnull c, uint8_t *_Nonnull nonce, uint8_t *_Nonnull session_pk, uint8_t *_Nonnull peer_real_pk, - uint8_t *_Nonnull dht_public_key, uint8_t *_Nonnull cookie, const uint8_t *_Nonnull packet, uint16_t length, const uint8_t *_Nullable expected_real_pk) +static bool handle_crypto_handshake(const Net_Crypto *_Nonnull c, uint8_t recv_nonce[CRYPTO_NONCE_SIZE], uint8_t peer_ephemeral_public_key[CRYPTO_PUBLIC_KEY_SIZE], + uint8_t peer_id_public_key[CRYPTO_PUBLIC_KEY_SIZE], uint8_t peer_dht_public_key[CRYPTO_PUBLIC_KEY_SIZE], + uint8_t peer_cookie[COOKIE_LENGTH], const uint8_t *_Nonnull packet, uint16_t packet_length, + const uint8_t expected_peer_id_pk[CRYPTO_PUBLIC_KEY_SIZE], Noise_Handshake *_Nullable noise_handshake) { - if (length != HANDSHAKE_PACKET_LENGTH) { - LOGGER_WARNING(c->log, "Handshake length mismatch: %u != %u", length, (unsigned int)HANDSHAKE_PACKET_LENGTH); - return false; - } + /* Noise-based handshake */ + if (noise_handshake != nullptr) { + LOGGER_DEBUG(c->log, "noise_handshake->initiator: %d", noise_handshake->initiator); - uint8_t cookie_plain[COOKIE_DATA_LENGTH]; + uint8_t cookie_plain[COOKIE_DATA_LENGTH]; - if (open_cookie(c->mem, c->mono_time, cookie_plain, packet + 1, c->secret_symmetric_key) != 0) { - LOGGER_WARNING(c->log, "Failed to open cookie"); - return false; - } + /* Parse initiator packet: -> e, es, s, ss (see packet layout above defines) */ + if (!noise_handshake->initiator) { + if (packet_length != NOISE_HANDSHAKE_PACKET_LENGTH_INITIATOR) { + LOGGER_WARNING(c->log, "Noise responder: unexpected packet length %u (expected %u)", + packet_length, (unsigned int)NOISE_HANDSHAKE_PACKET_LENGTH_INITIATOR); + return false; + } - if (expected_real_pk != nullptr && !pk_equal(cookie_plain, expected_real_pk)) { - LOGGER_WARNING(c->log, "Expected real pk mismatch"); - return false; - } + const uint8_t *const hs = packet + 2; - uint8_t cookie_hash[CRYPTO_SHA512_SIZE]; - crypto_sha512(cookie_hash, packet + 1, COOKIE_LENGTH); + /* e */ + memcpy(noise_handshake->remote_ephemeral, hs, CRYPTO_PUBLIC_KEY_SIZE); + noise_mix_hash(noise_handshake->hash, noise_handshake->remote_ephemeral, CRYPTO_PUBLIC_KEY_SIZE); - uint8_t plain[CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE + COOKIE_LENGTH]; - const int len = decrypt_data(c->mem, cookie_plain, c->self_secret_key, packet + 1 + COOKIE_LENGTH, - packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE, - HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE), plain); + /* es */ + uint8_t noise_handshake_temp_key[CRYPTO_SHARED_KEY_SIZE]; - if (len != sizeof(plain)) { - LOGGER_WARNING(c->log, "Failed to decrypt handshake data"); - return false; - } + if (noise_mix_key(noise_handshake->chaining_key, noise_handshake_temp_key, c->self_id_secret_key, noise_handshake->remote_ephemeral) != 0) { + LOGGER_DEBUG(c->log, "RESPONDER: Noise MixKey(es) failed"); + return false; + } - if (!crypto_sha512_eq(cookie_hash, plain + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE)) { - LOGGER_WARNING(c->log, "Cookie hash mismatch"); - return false; - } + /* s */ + /* Nonce is always 0: each MixKey produces a fresh key, so reuse is safe. */ + if (noise_decrypt_and_hash(noise_handshake->remote_static, hs + CRYPTO_PUBLIC_KEY_SIZE, CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_MAC_SIZE, + noise_handshake_temp_key, noise_handshake->hash) != CRYPTO_PUBLIC_KEY_SIZE) { + LOGGER_DEBUG(c->log, "RESPONDER: Noise ReadMessage remote static decryption failed"); + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + return false; + } - memcpy(nonce, plain, CRYPTO_NONCE_SIZE); - memcpy(session_pk, plain + CRYPTO_NONCE_SIZE, CRYPTO_PUBLIC_KEY_SIZE); - memcpy(cookie, plain + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE, COOKIE_LENGTH); - memcpy(peer_real_pk, cookie_plain, CRYPTO_PUBLIC_KEY_SIZE); - memcpy(dht_public_key, cookie_plain + CRYPTO_PUBLIC_KEY_SIZE, CRYPTO_PUBLIC_KEY_SIZE); - return true; + /* ss */ + if (noise_mix_key(noise_handshake->chaining_key, noise_handshake_temp_key, c->self_id_secret_key, noise_handshake->remote_static) != 0) { + LOGGER_DEBUG(c->log, "RESPONDER: Noise MixKey(ss) failed"); + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + return false; + } + /* Payload decryption */ + uint8_t handshake_payload_plain[NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_INITIATOR]; + + /* Nonce for payload decryption is _always_ 0 in case of ChaCha20-Poly1305 */ + if (noise_decrypt_and_hash(handshake_payload_plain, hs + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_MAC_SIZE, + sizeof(handshake_payload_plain) + CRYPTO_MAC_SIZE, noise_handshake_temp_key, + noise_handshake->hash) != sizeof(handshake_payload_plain)) { + LOGGER_DEBUG(c->log, "RESPONDER: Noise HS payload decryption failed"); + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + return false; + } + + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + + /* Cookie can only be verified after Noise payload decryption. */ + if (open_cookie(c->mem, c->mono_time, cookie_plain, handshake_payload_plain, c->cookie_symmetric_key) != 0) { + crypto_memzero(handshake_payload_plain, NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_INITIATOR); + return false; + } + + /* Verify that the peer's static key matches what the cookie claims. */ + if (!pk_equal(cookie_plain, noise_handshake->remote_static)) { + crypto_memzero(handshake_payload_plain, NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_INITIATOR); + return false; + } + + /* Extract the initiator's cookie so the responder can echo it back. */ + memcpy(peer_cookie, handshake_payload_plain + COOKIE_LENGTH, COOKIE_LENGTH); + /* Needed by friend_connection.c:handle_new_connections() to look up the friend. */ + if (peer_id_public_key != nullptr) { + memcpy(peer_id_public_key, noise_handshake->remote_static, CRYPTO_PUBLIC_KEY_SIZE); + } + /* Extract DHT key from the cookie for routing. */ + memcpy(peer_dht_public_key, cookie_plain + CRYPTO_PUBLIC_KEY_SIZE, CRYPTO_PUBLIC_KEY_SIZE); + + crypto_memzero(handshake_payload_plain, NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_INITIATOR); + + return true; + } else { + /* Parse responder packet: <- e, ee, se (see packet layout above defines) */ + if (packet_length != NOISE_HANDSHAKE_PACKET_LENGTH_RESPONDER) { + LOGGER_WARNING(c->log, "Noise initiator: unexpected packet length %u (expected %u)", + packet_length, (unsigned int)NOISE_HANDSHAKE_PACKET_LENGTH_RESPONDER); + return false; + } + + const uint8_t *const hs = packet + 2; + + memcpy(noise_handshake->remote_ephemeral, hs, CRYPTO_PUBLIC_KEY_SIZE); + noise_mix_hash(noise_handshake->hash, noise_handshake->remote_ephemeral, CRYPTO_PUBLIC_KEY_SIZE); + + /* ee */ + uint8_t noise_handshake_temp_key[CRYPTO_SHARED_KEY_SIZE]; + + if (noise_mix_key(noise_handshake->chaining_key, noise_handshake_temp_key, noise_handshake->ephemeral_private, noise_handshake->remote_ephemeral) != 0) { + LOGGER_DEBUG(c->log, "INITIATOR: Noise MixKey(ee) failed"); + return false; + } + + /* se: DH(s, re) if initiator: initiator's static responder's ephemeral */ + if (noise_mix_key(noise_handshake->chaining_key, noise_handshake_temp_key, c->self_id_secret_key, noise_handshake->remote_ephemeral) != 0) { + LOGGER_DEBUG(c->log, "INITIATOR: Noise MixKey(se) failed"); + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + return false; + } + + /* Payload decryption */ + uint8_t handshake_payload_plain[NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_RESPONDER]; + + /* Nonce is always 0: each MixKey produces a fresh key, so reuse is safe. */ + if (noise_decrypt_and_hash(handshake_payload_plain, hs + CRYPTO_PUBLIC_KEY_SIZE, + sizeof(handshake_payload_plain) + CRYPTO_MAC_SIZE, noise_handshake_temp_key, + noise_handshake->hash) != sizeof(handshake_payload_plain)) { + LOGGER_DEBUG(c->log, "INITIATOR: Noise ReadMessage decryption failed"); + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + return false; + } + + crypto_memzero(noise_handshake_temp_key, CRYPTO_SHARED_KEY_SIZE); + + /* Cookie can only be verified after Noise payload decryption. */ + if (open_cookie(c->mem, c->mono_time, cookie_plain, handshake_payload_plain, c->cookie_symmetric_key) != 0) { + crypto_memzero(handshake_payload_plain, NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_RESPONDER); + return false; + } + + /* Verify that the peer's static key matches what the cookie claims. */ + const uint8_t *expected_pk = expected_peer_id_pk != nullptr ? expected_peer_id_pk : noise_handshake->remote_static; + if (!pk_equal(cookie_plain, expected_pk)) { + crypto_memzero(handshake_payload_plain, NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_RESPONDER); + return false; + } + + /* Extract DHT key from the cookie for routing. */ + memcpy(peer_dht_public_key, cookie_plain + CRYPTO_PUBLIC_KEY_SIZE, CRYPTO_PUBLIC_KEY_SIZE); + + crypto_memzero(handshake_payload_plain, NOISE_HANDSHAKE_PAYLOAD_PLAIN_LENGTH_RESPONDER); + + return true; + } + } else { /* legacy handshake */ + if (packet_length != HANDSHAKE_PACKET_LENGTH) { + return false; + } + + uint8_t cookie_plain[COOKIE_DATA_LENGTH]; + + if (open_cookie(c->mem, c->mono_time, cookie_plain, packet + 1, c->cookie_symmetric_key) != 0) { + return false; + } + + /* Compares static identity public keys from the peer */ + if (expected_peer_id_pk != nullptr && !pk_equal(cookie_plain, expected_peer_id_pk)) { + return false; + } + + uint8_t cookie_hash[CRYPTO_SHA512_SIZE]; + crypto_sha512(cookie_hash, packet + 1, COOKIE_LENGTH); + + uint8_t plain[CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE + COOKIE_LENGTH]; + const int len = decrypt_data(c->mem, cookie_plain, c->self_id_secret_key, packet + 1 + COOKIE_LENGTH, + packet + 1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE, + HANDSHAKE_PACKET_LENGTH - (1 + COOKIE_LENGTH + CRYPTO_NONCE_SIZE), plain); + + if (len != sizeof(plain)) { + return false; + } + + if (!crypto_sha512_eq(cookie_hash, plain + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE)) { + return false; + } + + memcpy(recv_nonce, plain, CRYPTO_NONCE_SIZE); + memcpy(peer_ephemeral_public_key, plain + CRYPTO_NONCE_SIZE, CRYPTO_PUBLIC_KEY_SIZE); + memcpy(peer_cookie, plain + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE, COOKIE_LENGTH); + memcpy(peer_id_public_key, cookie_plain, CRYPTO_PUBLIC_KEY_SIZE); + memcpy(peer_dht_public_key, cookie_plain + CRYPTO_PUBLIC_KEY_SIZE, CRYPTO_PUBLIC_KEY_SIZE); + + return true; + } } static Crypto_Connection *_Nullable get_crypto_connection(const Net_Crypto *_Nonnull c, int crypt_connection_id) @@ -734,7 +1120,7 @@ static int send_packet_to(const Net_Crypto *_Nonnull c, int crypt_connection_id, const uint64_t current_time = mono_time_get(c->mono_time); if ((((UDP_DIRECT_TIMEOUT / 2) + conn->direct_send_attempt_time) < current_time && length < 96) - || data[0] == NET_PACKET_COOKIE_REQUEST || data[0] == NET_PACKET_CRYPTO_HS) { + || data[0] == NET_PACKET_COOKIE_REQUEST || data[0] == NET_PACKET_CRYPTO_HS || data[0] == NET_PACKET_CRYPTO_NOISE_HS) { if ((uint32_t)sendpacket(c->net, &ip_port, data, length) == length) { direct_send_attempt = true; conn->direct_send_attempt_time = mono_time_get(c->mono_time); @@ -1099,15 +1485,26 @@ static int send_data_packet(const Net_Crypto *_Nonnull c, int crypt_connection_i const uint16_t packet_size = 1 + sizeof(uint16_t) + length + CRYPTO_MAC_SIZE; VLA(uint8_t, packet, packet_size); packet[0] = NET_PACKET_CRYPTO_DATA; - memcpy(packet + 1, conn->sent_nonce + (CRYPTO_NONCE_SIZE - sizeof(uint16_t)), sizeof(uint16_t)); - const int len = encrypt_data_symmetric(c->mem, conn->shared_key, conn->sent_nonce, data, length, packet + 1 + sizeof(uint16_t)); + memcpy(packet + 1, conn->send_nonce + (CRYPTO_NONCE_SIZE - sizeof(uint16_t)), sizeof(uint16_t)); + + // TODO(goldroom): consider passing packet[0..2] (type + nonce_hi) as AD. + // Currently safe to omit: the full 24-byte nonce (which subsumes the 2-byte + // on-wire prefix) is already an AEAD input, so flipping the prefix only + // causes decryption failure, not forgery. Adding AD later requires a + // NOISE_PROTOCOL_VERSION bump (changes the AEAD tag). + int len = 0; + if (conn->noise_handshake_enabled) { + len = encrypt_data_symmetric_xaead(conn->send_key, conn->send_nonce, data, length, packet + 1 + sizeof(uint16_t), nullptr, 0); + } else { /* legacy: uses precomputed shared key */ + len = encrypt_data_symmetric(c->mem, conn->shared_key, conn->send_nonce, data, length, packet + 1 + sizeof(uint16_t)); + } if (len + 1 + sizeof(uint16_t) != packet_size) { LOGGER_ERROR(c->log, "encryption failed: %d", len); return -1; } - increment_nonce(conn->sent_nonce); + increment_nonce(conn->send_nonce); return send_packet_to(c, crypt_connection_id, packet, packet_size); } @@ -1263,8 +1660,17 @@ static int handle_data_packet(const Net_Crypto *_Nonnull c, int crypt_connection net_unpack_u16(packet + 1, &num); const uint16_t diff = num - num_cur_nonce; increment_nonce_number(nonce, diff); - const int len = decrypt_data_symmetric(c->mem, conn->shared_key, nonce, packet + 1 + sizeof(uint16_t), - length - (1 + sizeof(uint16_t)), data); + + // TODO(goldroom): consider passing packet[0..2] (type + nonce_hi) as AD. + // See matching comment in send_data_packet() for rationale. + int len = 0; + if (conn->noise_handshake_enabled) { + len = decrypt_data_symmetric_xaead(conn->recv_key, nonce, packet + 1 + sizeof(uint16_t), length - (1 + sizeof(uint16_t)), data, + nullptr, 0); + } else { /* legacy: uses precomputed shared key */ + len = decrypt_data_symmetric(c->mem, conn->shared_key, nonce, packet + 1 + sizeof(uint16_t), + length - (1 + sizeof(uint16_t)), data); + } if ((unsigned int)len != length - crypto_packet_overhead) { return -1; @@ -1452,14 +1858,45 @@ static int create_send_handshake(const Net_Crypto *_Nonnull c, int crypt_connect return -1; } - uint8_t handshake_packet[HANDSHAKE_PACKET_LENGTH]; + /* Noise-based handshake */ + if (conn->noise_handshake_enabled && conn->noise_handshake != nullptr) { + LOGGER_DEBUG(c->log, "conn->noise_handshake->initiator: %d", conn->noise_handshake->initiator); + if (conn->noise_handshake->initiator) { + uint8_t handshake_packet[NOISE_HANDSHAKE_PACKET_LENGTH_INITIATOR]; - if (create_crypto_handshake(c, handshake_packet, cookie, conn->sent_nonce, conn->sessionpublic_key, - conn->public_key, dht_public_key) != sizeof(handshake_packet)) { - return -1; - } + if (create_crypto_handshake(c, handshake_packet, cookie, nullptr, conn->ephemeral_secret_key, conn->ephemeral_public_key, + conn->noise_handshake->remote_static, dht_public_key, conn->noise_handshake) != sizeof(handshake_packet)) { + return -1; + } - if (new_temp_packet(c, crypt_connection_id, handshake_packet, sizeof(handshake_packet)) != 0) { + if (new_temp_packet(c, crypt_connection_id, handshake_packet, sizeof(handshake_packet)) != 0) { + return -1; + } + } else { /* Noise RESPONDER */ + uint8_t handshake_packet[NOISE_HANDSHAKE_PACKET_LENGTH_RESPONDER]; + + if (create_crypto_handshake(c, handshake_packet, cookie, nullptr, conn->ephemeral_secret_key, conn->ephemeral_public_key, + conn->noise_handshake->remote_static, dht_public_key, conn->noise_handshake) != sizeof(handshake_packet)) { + return -1; + } + + if (new_temp_packet(c, crypt_connection_id, handshake_packet, sizeof(handshake_packet)) != 0) { + return -1; + } + } + } else if (c->handshake_mode != CRYPTO_HANDSHAKE_MODE_NOISE_ONLY) { /* legacy handshake */ + uint8_t handshake_packet[HANDSHAKE_PACKET_LENGTH]; + + /* Legacy handshake does not use ephemeral_private_key or noise_handshake. */ + if (create_crypto_handshake(c, handshake_packet, cookie, conn->send_nonce, nullptr, conn->ephemeral_public_key, + conn->peer_id_public_key, dht_public_key, nullptr) != sizeof(handshake_packet)) { + return -1; + } + + if (new_temp_packet(c, crypt_connection_id, handshake_packet, sizeof(handshake_packet)) != 0) { + return -1; + } + } else { return -1; } @@ -1481,6 +1918,7 @@ static int send_kill_packet(const Net_Crypto *_Nonnull c, int crypt_connection_i } const uint8_t kill_packet[1] = {PACKET_ID_KILL}; + return send_data_packet_helper(c, crypt_connection_id, conn->recv_array.buffer_start, conn->send_array.buffer_end, kill_packet, sizeof(kill_packet)); } @@ -1488,6 +1926,7 @@ static int send_kill_packet(const Net_Crypto *_Nonnull c, int crypt_connection_i static void connection_kill(Net_Crypto *_Nonnull c, int crypt_connection_id, void *_Nullable userdata) { const Crypto_Connection *conn = get_crypto_connection(c, crypt_connection_id); + if (conn == nullptr) { return; } @@ -1522,6 +1961,9 @@ static int handle_data_packet_core(Net_Crypto *_Nonnull c, int crypt_connection_ const int len = handle_data_packet(c, crypt_connection_id, data, packet, length); if (len <= (int)(sizeof(uint32_t) * 2)) { + char log_id_public[TOX_BYTES_TO_STRING_BUF_SIZE]; + bytes_to_string(conn->peer_id_public_key, CRYPTO_PUBLIC_KEY_SIZE, log_id_public, sizeof(log_id_public)); + LOGGER_WARNING(c->log, "decryption failure: crypt_connection_id: %d, conn->status: %u, peer_id_public_key: %s", crypt_connection_id, conn->status, log_id_public); return -1; } @@ -1559,6 +2001,7 @@ static int handle_data_packet_core(Net_Crypto *_Nonnull c, int crypt_connection_ } if (real_data[0] == PACKET_ID_KILL) { + LOGGER_DEBUG(c->log, "KILL PACKET RECEIVED crypt_connection_id: %d/conn->status: %u", crypt_connection_id, conn->status); connection_kill(c, crypt_connection_id, userdata); return 0; } @@ -1567,6 +2010,9 @@ static int handle_data_packet_core(Net_Crypto *_Nonnull c, int crypt_connection_ clear_temp_packet(c, crypt_connection_id); conn->status = CRYPTO_CONN_ESTABLISHED; + /* Zero out secret key no longer needed after connection is established */ + crypto_memzero(conn->ephemeral_secret_key, CRYPTO_SECRET_KEY_SIZE); + if (conn->connection_status_callback != nullptr) { conn->connection_status_callback(conn->connection_status_callback_object, conn->connection_status_callback_id, true, userdata); @@ -1643,6 +2089,12 @@ static int handle_data_packet_core(Net_Crypto *_Nonnull c, int crypt_connection_ return 0; } +/** + * @brief Handle a cookie response packet and send the appropriate handshake. + * + * @return -1 in case of failure + * @return 0 if cookie response handled successfully + */ static int handle_packet_cookie_response(const Net_Crypto *_Nonnull c, int crypt_connection_id, const uint8_t *_Nonnull packet, uint16_t length) { Crypto_Connection *conn = get_crypto_connection(c, crypt_connection_id); @@ -1666,7 +2118,70 @@ static int handle_packet_cookie_response(const Net_Crypto *_Nonnull c, int crypt return -1; } - if (create_send_handshake(c, crypt_connection_id, cookie, conn->dht_public_key) != 0) { + if (conn->noise_handshake != nullptr) { + if (conn->noise_handshake->initiator) { + LOGGER_DEBUG(c->log, "INITIATOR: Noise handshake"); + if (create_send_handshake(c, crypt_connection_id, cookie, conn->peer_dht_public_key) != 0) { + return -1; + } + } else { + return -1; + } + } else if (c->handshake_mode != CRYPTO_HANDSHAKE_MODE_NOISE_ONLY) { + /* non-Noise handshake */ + if (create_send_handshake(c, crypt_connection_id, cookie, conn->peer_dht_public_key) != 0) { + return -1; + } + } else { + return -1; + } + + conn->status = CRYPTO_CONN_HANDSHAKE_SENT; + return 0; +} + +/** @brief Process an incoming Noise initiator packet as a responder. + * + * Re-initialises the handshake state (to handle retransmissions cleanly), + * generates a fresh ephemeral keypair, validates the initiator's message, + * and sends our responder handshake. + * + * @return -1 on failure. + * @return 0 on success. + */ +static int noise_respond_to_initiator(Net_Crypto *_Nonnull c, int crypt_connection_id, + Crypto_Connection *_Nonnull conn, + const uint8_t *_Nonnull packet, uint16_t length, + uint8_t dht_public_key[CRYPTO_PUBLIC_KEY_SIZE], + uint8_t cookie[COOKIE_LENGTH]) +{ + if (length < 2 || packet[1] != NOISE_PROTOCOL_VERSION) { + LOGGER_WARNING(c->log, "Noise: version mismatch (got 0x%02x, expected 0x%02x)", + (unsigned int)(length >= 2 ? packet[1] : 0), (unsigned int)NOISE_PROTOCOL_VERSION); + return -1; + } + + const uint8_t prologue[] = {packet[1]}; + + /* Validate on a scratch handshake; commit only on success. A garbage + * initiator-length packet must not destroy the initiator's existing state. */ + Noise_Handshake scratch; + if (noise_handshake_init(&scratch, c->self_id_public_key, nullptr, false, prologue, sizeof(prologue)) != 0) { + return -1; + } + + crypto_new_keypair(c->rng, scratch.ephemeral_public, scratch.ephemeral_private); + + if (!handle_crypto_handshake(c, nullptr, nullptr, nullptr, dht_public_key, cookie, + packet, length, nullptr, &scratch)) { + crypto_memzero(&scratch, sizeof(scratch)); + return -1; + } + + *conn->noise_handshake = scratch; + crypto_memzero(&scratch, sizeof(scratch)); + + if (create_send_handshake(c, crypt_connection_id, cookie, dht_public_key) != 0) { return -1; } @@ -1674,7 +2189,105 @@ static int handle_packet_cookie_response(const Net_Crypto *_Nonnull c, int crypt return 0; } -static int handle_packet_crypto_hs(const Net_Crypto *_Nonnull c, int crypt_connection_id, const uint8_t *_Nonnull packet, uint16_t length, +/** @brief Handle a Noise handshake packet for an existing connection. + * + * Handles three sub-cases: + * - NOISE_BOTH fallback: a legacy packet arrives while Noise is active. + * - Initiator receiving a responder reply. + * - Initiator or responder receiving an initiator packet (simultaneous open). + * + * @return -1 on failure. + * @return 0 on success (dht_public_key and cookie are populated). + * @return 1 if the packet was ignored (simultaneous open, staying as initiator). + */ +static int handle_noise_hs(Net_Crypto *_Nonnull c, int crypt_connection_id, + Crypto_Connection *_Nonnull conn, + const uint8_t *_Nonnull packet, uint16_t length, + uint8_t dht_public_key[CRYPTO_PUBLIC_KEY_SIZE], + uint8_t cookie[COOKIE_LENGTH]) +{ + if (conn->noise_handshake == nullptr) { + return -1; + } + + /* NOISE_BOTH: received a legacy packet type while Noise is active. + * Try legacy validation FIRST; only destroy Noise state on success. */ + if (packet[0] == NET_PACKET_CRYPTO_HS + && c->handshake_mode == CRYPTO_HANDSHAKE_MODE_NOISE_BOTH) { + uint8_t peer_id_public_key[CRYPTO_PUBLIC_KEY_SIZE]; + if (!handle_crypto_handshake(c, conn->recv_nonce, conn->peer_ephemeral_public_key, + peer_id_public_key, dht_public_key, cookie, + packet, length, conn->peer_id_public_key, nullptr)) { + return -1; // invalid packet, Noise state preserved + } + /* Validated legacy packet. Safe to switch away from Noise. */ + noise_handshake_free(c->mem, &conn->noise_handshake); + conn->noise_handshake_enabled = false; + crypto_new_keypair(c->rng, conn->ephemeral_public_key, conn->ephemeral_secret_key); + random_nonce(c->rng, conn->send_nonce); + conn->legacy_ephemeral_keys_set = true; + return 0; + } + + if (length < 2 || packet[1] != NOISE_PROTOCOL_VERSION) { + LOGGER_WARNING(c->log, "Noise: version mismatch (got 0x%02x, expected 0x%02x)", + (unsigned int)(length >= 2 ? packet[1] : 0), (unsigned int)NOISE_PROTOCOL_VERSION); + return -1; + } + + if (conn->noise_handshake->initiator) { + if (length == NOISE_HANDSHAKE_PACKET_LENGTH_RESPONDER) { + /* Normal case: initiator receives responder reply. */ + LOGGER_DEBUG(c->log, "INITIATOR: Noise handshake -> normal"); + if (!handle_crypto_handshake(c, nullptr, nullptr, nullptr, dht_public_key, nullptr, + packet, length, nullptr, conn->noise_handshake)) { + return -1; + } + return 0; + } + + if (length != NOISE_HANDSHAKE_PACKET_LENGTH_INITIATOR) { + return -1; + } + + /* Simultaneous open: both sides sent initiator packets. */ + const int cmp = memcmp(c->self_id_public_key, conn->noise_handshake->remote_static, CRYPTO_PUBLIC_KEY_SIZE); + if (cmp == 0) { + LOGGER_WARNING(c->log, "Simultaneous Open: peer has same public key, rejecting"); + return -1; + } + if (cmp > 0) { + LOGGER_DEBUG(c->log, "INITIATOR: Simultaneous Open - Staying INITIATOR, ignoring packet"); + return 1; /* packet handled, no state transition needed */ + } + + /* Lower public key becomes responder during simultaneous open. */ + LOGGER_DEBUG(c->log, "INITIATOR: Noise handshake -> CHANGE TO RESPONDER"); + return noise_respond_to_initiator(c, crypt_connection_id, conn, packet, length, dht_public_key, cookie); + } + + /* Noise RESPONDER (with or without role change from INITIATOR) */ + if (length == NOISE_HANDSHAKE_PACKET_LENGTH_INITIATOR) { + LOGGER_DEBUG(c->log, "RESPONDER: Noise handshake"); + return noise_respond_to_initiator(c, crypt_connection_id, conn, packet, length, dht_public_key, cookie); + } + + if (length == NOISE_HANDSHAKE_PACKET_LENGTH_RESPONDER) { + /* Responder receiving a responder-length packet is invalid; silently drop. + * NOTE(goldroom): connection_kill() was tried here but caused issues in real-world tests. */ + LOGGER_DEBUG(c->log, "RESPONDER: unexpected responder-length packet, dropping"); + } + + return -1; +} + +/** + * @brief Handle a received handshake packet (Noise or legacy) for an existing connection. + * + * @return -1 in case of failure + * @return 0 if successful + */ +static int handle_packet_crypto_hs(Net_Crypto *_Nonnull c, int crypt_connection_id, const uint8_t *_Nonnull packet, uint16_t length, void *_Nullable userdata) { Crypto_Connection *conn = get_crypto_connection(c, crypt_connection_id); @@ -1682,31 +2295,52 @@ static int handle_packet_crypto_hs(const Net_Crypto *_Nonnull c, int crypt_conne return -1; } - if (conn->status != CRYPTO_CONN_COOKIE_REQUESTING - && conn->status != CRYPTO_CONN_HANDSHAKE_SENT - && conn->status != CRYPTO_CONN_NOT_CONFIRMED) { - return -1; + if (conn->noise_handshake != nullptr) { + if (conn->status != CRYPTO_CONN_COOKIE_REQUESTING + && conn->status != CRYPTO_CONN_HANDSHAKE_SENT) { + return -1; + } + } else { + if (conn->status != CRYPTO_CONN_COOKIE_REQUESTING + && conn->status != CRYPTO_CONN_HANDSHAKE_SENT + && conn->status != CRYPTO_CONN_NOT_CONFIRMED) { + return -1; + } } - uint8_t peer_real_pk[CRYPTO_PUBLIC_KEY_SIZE]; uint8_t dht_public_key[CRYPTO_PUBLIC_KEY_SIZE]; uint8_t cookie[COOKIE_LENGTH]; - if (!handle_crypto_handshake(c, conn->recv_nonce, conn->peersessionpublic_key, peer_real_pk, dht_public_key, cookie, - packet, length, conn->public_key)) { - return -1; + if (conn->noise_handshake_enabled) { + const int noise_rc = handle_noise_hs(c, crypt_connection_id, conn, packet, length, dht_public_key, cookie); + if (noise_rc < 0) { + return -1; + } + if (noise_rc > 0) { + /* Simultaneous open: staying as initiator, packet ignored. */ + return 0; + } + } else { /* legacy handshake */ + uint8_t peer_id_public_key[CRYPTO_PUBLIC_KEY_SIZE]; + if (!handle_crypto_handshake(c, conn->recv_nonce, conn->peer_ephemeral_public_key, peer_id_public_key, dht_public_key, cookie, + packet, length, conn->peer_id_public_key, nullptr)) { + return -1; + } } - if (pk_equal(dht_public_key, conn->dht_public_key)) { - encrypt_precompute(conn->peersessionpublic_key, conn->sessionsecret_key, conn->shared_key); - - if (conn->status == CRYPTO_CONN_COOKIE_REQUESTING) { + if (pk_equal(dht_public_key, conn->peer_dht_public_key)) { + if (conn->noise_handshake_enabled && conn->noise_handshake != nullptr) { + conn->status = CRYPTO_CONN_NOT_CONFIRMED; + noise_split_and_cleanup(c->mem, conn); + } else { /* legacy handshake */ + /* Always send a handshake reply regardless of current state, because we may + * have already sent a Noise packet before falling back to legacy. */ if (create_send_handshake(c, crypt_connection_id, cookie, dht_public_key) != 0) { return -1; } + encrypt_precompute(conn->peer_ephemeral_public_key, conn->ephemeral_secret_key, conn->shared_key); + conn->status = CRYPTO_CONN_NOT_CONFIRMED; } - - conn->status = CRYPTO_CONN_NOT_CONFIRMED; } else { if (conn->dht_pk_callback != nullptr) { conn->dht_pk_callback(conn->dht_pk_callback_object, conn->dht_pk_callback_number, dht_public_key, userdata); @@ -1748,6 +2382,7 @@ static int handle_packet_connection(Net_Crypto *_Nonnull c, int crypt_connection return handle_packet_cookie_response(c, crypt_connection_id, packet, length); case NET_PACKET_CRYPTO_HS: + case NET_PACKET_CRYPTO_NOISE_HS: return handle_packet_crypto_hs(c, crypt_connection_id, packet, length, userdata); case NET_PACKET_CRYPTO_DATA: @@ -1779,6 +2414,7 @@ static int realloc_cryptoconnection(Net_Crypto *_Nonnull c, uint32_t num) } c->crypto_connections = newcrypto_connections; + return 0; } @@ -1807,6 +2443,17 @@ static int create_crypto_connection(Net_Crypto *_Nonnull c) } if (id != -1) { + if (c->handshake_mode != CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY) { + Noise_Handshake *noise_handshake = (Noise_Handshake *)mem_alloc(c->mem, sizeof(Noise_Handshake)); + + if (noise_handshake == nullptr) { + LOGGER_ERROR(c->log, "failed to alloc noise_handshake"); + return -1; + } + + c->crypto_connections[id].noise_handshake = noise_handshake; + } + // Memsetting float/double to 0 is non-portable, so we explicitly set them to 0 c->crypto_connections[id].packet_recv_rate = 0.0; c->crypto_connections[id].packet_send_rate = 0.0; @@ -1844,6 +2491,9 @@ static int wipe_crypto_connection(Net_Crypto *_Nonnull c, int crypt_connection_i uint32_t i; + /* May still be allocated if connection was killed before CRYPTO_CONN_ESTABLISHED. */ + noise_handshake_free(c->mem, &c->crypto_connections[crypt_connection_id].noise_handshake); + crypto_memzero(&c->crypto_connections[crypt_connection_id], sizeof(Crypto_Connection)); /* check if we can resize the connections array */ @@ -1873,7 +2523,7 @@ static int getcryptconnection_id(const Net_Crypto *_Nonnull c, const uint8_t *_N continue; } - if (pk_equal(public_key, c->crypto_connections[i].public_key)) { + if (pk_equal(public_key, c->crypto_connections[i].peer_id_public_key)) { return i; } } @@ -1939,7 +2589,7 @@ void new_connection_handler(Net_Crypto *c, new_connection_cb *new_connection_cal * @retval -1 on failure. * @retval 0 on success. */ -static int handle_new_connection_handshake(Net_Crypto *_Nonnull c, const IP_Port *_Nonnull source, const uint8_t *_Nonnull data, uint16_t length, +static int handle_new_connection_handshake(Net_Crypto *_Nonnull c, const IP_Port *_Nonnull source, const uint8_t *_Nonnull packet, uint16_t length, void *_Nullable userdata) { uint8_t *cookie = (uint8_t *)mem_balloc(c->mem, COOKIE_LENGTH); @@ -1948,52 +2598,133 @@ static int handle_new_connection_handshake(Net_Crypto *_Nonnull c, const IP_Port } New_Connection n_c = {{{{0}}}}; - n_c.cookie = cookie; + n_c.peer_cookie = cookie; n_c.source = *source; n_c.cookie_length = COOKIE_LENGTH; - if (!handle_crypto_handshake(c, n_c.recv_nonce, n_c.peersessionpublic_key, n_c.public_key, n_c.dht_public_key, - n_c.cookie, data, length, nullptr)) { - mem_delete(c->mem, n_c.cookie); + + /* Differentiate between legacy and Noise handshake by packet type. */ + if (packet[0] == NET_PACKET_CRYPTO_NOISE_HS) { + /* Noise handshake packet: accept only if not LEGACY_ONLY */ + if (c->handshake_mode == CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY) { + mem_delete(c->mem, n_c.peer_cookie); + return -1; + } + + if (length < 2 || packet[1] != NOISE_PROTOCOL_VERSION) { + LOGGER_WARNING(c->log, "Noise: version mismatch (got 0x%02x, expected 0x%02x)", + (unsigned int)(length >= 2 ? packet[1] : 0), (unsigned int)NOISE_PROTOCOL_VERSION); + mem_delete(c->mem, n_c.peer_cookie); + return -1; + } + + const uint8_t prologue[] = {packet[1]}; + + Noise_Handshake *noise_hs = (Noise_Handshake *)mem_alloc(c->mem, sizeof(Noise_Handshake)); + if (noise_hs == nullptr) { + mem_delete(c->mem, n_c.peer_cookie); + return -1; + } + n_c.noise_handshake = noise_hs; + if (noise_handshake_init(n_c.noise_handshake, c->self_id_public_key, nullptr, false, prologue, sizeof(prologue)) != 0) { + noise_handshake_free(c->mem, &n_c.noise_handshake); + mem_delete(c->mem, n_c.peer_cookie); + return -1; + } + + crypto_new_keypair(c->rng, n_c.noise_handshake->ephemeral_public, n_c.noise_handshake->ephemeral_private); + + LOGGER_DEBUG(c->log, "Noise RESPONDER: after handshake init"); + + if (!handle_crypto_handshake(c, nullptr, nullptr, n_c.peer_id_public_key, n_c.peer_dht_public_key, + n_c.peer_cookie, packet, length, nullptr, n_c.noise_handshake)) { + noise_handshake_free(c->mem, &n_c.noise_handshake); + mem_delete(c->mem, n_c.peer_cookie); + return -1; + } + } else if (c->handshake_mode != CRYPTO_HANDSHAKE_MODE_NOISE_ONLY) { /* legacy handshake */ + n_c.noise_handshake = nullptr; + if (!handle_crypto_handshake(c, n_c.recv_nonce, n_c.peer_ephemeral_public_key, n_c.peer_id_public_key, n_c.peer_dht_public_key, + n_c.peer_cookie, packet, length, nullptr, nullptr)) { + mem_delete(c->mem, n_c.peer_cookie); + return -1; + } + } else { + mem_delete(c->mem, n_c.peer_cookie); return -1; } - const int crypt_connection_id = getcryptconnection_id(c, n_c.public_key); + const int crypt_connection_id = getcryptconnection_id(c, n_c.peer_id_public_key); if (crypt_connection_id != -1) { Crypto_Connection *conn = get_crypto_connection(c, crypt_connection_id); if (conn == nullptr) { + noise_handshake_free(c->mem, &n_c.noise_handshake); + mem_delete(c->mem, n_c.peer_cookie); return -1; } - if (!pk_equal(n_c.dht_public_key, conn->dht_public_key)) { + if (!pk_equal(n_c.peer_dht_public_key, conn->peer_dht_public_key)) { connection_kill(c, crypt_connection_id, userdata); - } else { - if (conn->status != CRYPTO_CONN_COOKIE_REQUESTING && conn->status != CRYPTO_CONN_HANDSHAKE_SENT) { - mem_delete(c->mem, n_c.cookie); + } else if (packet[0] == NET_PACKET_CRYPTO_NOISE_HS) { /* case NoiseIK handshake */ + if (conn->noise_handshake == nullptr || (conn->status != CRYPTO_CONN_COOKIE_REQUESTING && conn->status != CRYPTO_CONN_HANDSHAKE_SENT)) { + noise_handshake_free(c->mem, &n_c.noise_handshake); + mem_delete(c->mem, n_c.peer_cookie); + return -1; + } + /* Copy the freshly parsed handshake into the existing connection slot. */ + *conn->noise_handshake = *n_c.noise_handshake; + + crypto_connection_add_source(c, crypt_connection_id, source); + + if (create_send_handshake(c, crypt_connection_id, n_c.peer_cookie, n_c.peer_dht_public_key) != 0) { + noise_handshake_free(c->mem, &n_c.noise_handshake); + mem_delete(c->mem, n_c.peer_cookie); return -1; } + conn->status = CRYPTO_CONN_NOT_CONFIRMED; + noise_split_and_cleanup(c->mem, conn); + + noise_handshake_free(c->mem, &n_c.noise_handshake); + mem_delete(c->mem, n_c.peer_cookie); + return 0; + } else { /* legacy handshake */ + conn->noise_handshake_enabled = false; + + /* Switching to legacy; discard Noise state. */ + noise_handshake_free(c->mem, &conn->noise_handshake); + + /* Only generate keys once; this function can be called multiple times + * for the same connection if handshake packets arrive in quick succession. */ + if (!conn->legacy_ephemeral_keys_set) { + crypto_new_keypair(c->rng, conn->ephemeral_public_key, conn->ephemeral_secret_key); + random_nonce(c->rng, conn->send_nonce); + conn->legacy_ephemeral_keys_set = true; + LOGGER_DEBUG(c->log, "switch to legacy handshake"); + } + memcpy(conn->recv_nonce, n_c.recv_nonce, CRYPTO_NONCE_SIZE); - memcpy(conn->peersessionpublic_key, n_c.peersessionpublic_key, CRYPTO_PUBLIC_KEY_SIZE); - encrypt_precompute(conn->peersessionpublic_key, conn->sessionsecret_key, conn->shared_key); + memcpy(conn->peer_ephemeral_public_key, n_c.peer_ephemeral_public_key, CRYPTO_PUBLIC_KEY_SIZE); + encrypt_precompute(conn->peer_ephemeral_public_key, conn->ephemeral_secret_key, conn->shared_key); crypto_connection_add_source(c, crypt_connection_id, source); - if (create_send_handshake(c, crypt_connection_id, n_c.cookie, n_c.dht_public_key) != 0) { - mem_delete(c->mem, n_c.cookie); + if (create_send_handshake(c, crypt_connection_id, n_c.peer_cookie, n_c.peer_dht_public_key) != 0) { + mem_delete(c->mem, n_c.peer_cookie); return -1; } conn->status = CRYPTO_CONN_NOT_CONFIRMED; - mem_delete(c->mem, n_c.cookie); + mem_delete(c->mem, n_c.peer_cookie); return 0; } } const int ret = c->new_connection_callback(c->new_connection_callback_object, &n_c); - mem_delete(c->mem, n_c.cookie); + noise_handshake_free(c->mem, &n_c.noise_handshake); + mem_delete(c->mem, n_c.peer_cookie); return ret; } @@ -2004,7 +2735,7 @@ static int handle_new_connection_handshake(Net_Crypto *_Nonnull c, const IP_Port */ int accept_crypto_connection(Net_Crypto *c, const New_Connection *n_c) { - if (getcryptconnection_id(c, n_c->public_key) != -1) { + if (getcryptconnection_id(c, n_c->peer_id_public_key) != -1) { return -1; } @@ -2022,7 +2753,7 @@ int accept_crypto_connection(Net_Crypto *c, const New_Connection *n_c) return -1; } - const int connection_number_tcp = new_tcp_connection_to(c->tcp_c, n_c->dht_public_key, crypt_connection_id); + const int connection_number_tcp = new_tcp_connection_to(c->tcp_c, n_c->peer_dht_public_key, crypt_connection_id); if (connection_number_tcp == -1) { wipe_crypto_connection(c, crypt_connection_id); @@ -2030,26 +2761,58 @@ int accept_crypto_connection(Net_Crypto *c, const New_Connection *n_c) } conn->connection_number_tcp = connection_number_tcp; - memcpy(conn->public_key, n_c->public_key, CRYPTO_PUBLIC_KEY_SIZE); - memcpy(conn->recv_nonce, n_c->recv_nonce, CRYPTO_NONCE_SIZE); - memcpy(conn->peersessionpublic_key, n_c->peersessionpublic_key, CRYPTO_PUBLIC_KEY_SIZE); - random_nonce(c->rng, conn->sent_nonce); - crypto_new_keypair(c->rng, conn->sessionpublic_key, conn->sessionsecret_key); - encrypt_precompute(conn->peersessionpublic_key, conn->sessionsecret_key, conn->shared_key); - conn->status = CRYPTO_CONN_NOT_CONFIRMED; - - if (create_send_handshake(c, crypt_connection_id, n_c->cookie, n_c->dht_public_key) != 0) { + + /* Noise: this path is only taken by the responder. */ + if (n_c->noise_handshake != nullptr) { + if (!n_c->noise_handshake->initiator) { + conn->noise_handshake_enabled = true; + *conn->noise_handshake = *n_c->noise_handshake; + memcpy(conn->peer_id_public_key, n_c->peer_id_public_key, CRYPTO_PUBLIC_KEY_SIZE); + + /* Must set status before create_send_handshake, which calls + * get_crypto_connection and rejects NO_CONNECTION slots. */ + conn->status = CRYPTO_CONN_NOT_CONFIRMED; + + if (create_send_handshake(c, crypt_connection_id, n_c->peer_cookie, n_c->peer_dht_public_key) != 0) { + kill_tcp_connection_to(c->tcp_c, conn->connection_number_tcp); + wipe_crypto_connection(c, crypt_connection_id); + return -1; + } + + noise_split_and_cleanup(c->mem, conn); + } else { + kill_tcp_connection_to(c->tcp_c, conn->connection_number_tcp); + wipe_crypto_connection(c, crypt_connection_id); + return -1; + } + + } else if (c->handshake_mode != CRYPTO_HANDSHAKE_MODE_NOISE_ONLY) { /* legacy handshake */ + memcpy(conn->peer_id_public_key, n_c->peer_id_public_key, CRYPTO_PUBLIC_KEY_SIZE); + memcpy(conn->recv_nonce, n_c->recv_nonce, CRYPTO_NONCE_SIZE); + memcpy(conn->peer_ephemeral_public_key, n_c->peer_ephemeral_public_key, CRYPTO_PUBLIC_KEY_SIZE); + random_nonce(c->rng, conn->send_nonce); + crypto_new_keypair(c->rng, conn->ephemeral_public_key, conn->ephemeral_secret_key); + encrypt_precompute(conn->peer_ephemeral_public_key, conn->ephemeral_secret_key, conn->shared_key); + conn->status = CRYPTO_CONN_NOT_CONFIRMED; + + if (create_send_handshake(c, crypt_connection_id, n_c->peer_cookie, n_c->peer_dht_public_key) != 0) { + kill_tcp_connection_to(c->tcp_c, conn->connection_number_tcp); + wipe_crypto_connection(c, crypt_connection_id); + return -1; + } + } else { kill_tcp_connection_to(c->tcp_c, conn->connection_number_tcp); wipe_crypto_connection(c, crypt_connection_id); return -1; } - memcpy(conn->dht_public_key, n_c->dht_public_key, CRYPTO_PUBLIC_KEY_SIZE); + memcpy(conn->peer_dht_public_key, n_c->peer_dht_public_key, CRYPTO_PUBLIC_KEY_SIZE); conn->packet_send_rate = CRYPTO_PACKET_MIN_RATE; conn->packet_send_rate_requested = CRYPTO_PACKET_MIN_RATE; conn->packets_left = CRYPTO_MIN_QUEUE_LENGTH; conn->rtt_time = DEFAULT_PING_CONNECTION; crypto_connection_add_source(c, crypt_connection_id, &n_c->source); + return crypt_connection_id; } @@ -2083,20 +2846,42 @@ int new_crypto_connection(Net_Crypto *c, const uint8_t *real_public_key, const u } conn->connection_number_tcp = connection_number_tcp; - memcpy(conn->public_key, real_public_key, CRYPTO_PUBLIC_KEY_SIZE); - random_nonce(c->rng, conn->sent_nonce); - crypto_new_keypair(c->rng, conn->sessionpublic_key, conn->sessionsecret_key); + memcpy(conn->peer_id_public_key, real_public_key, CRYPTO_PUBLIC_KEY_SIZE); + /* Noise transport uses nonces as counters starting from 0. Legacy may + * overwrite these with random nonces below if LEGACY_ONLY is selected. */ + memset(conn->send_nonce, 0, CRYPTO_NONCE_SIZE); + memset(conn->recv_nonce, 0, CRYPTO_NONCE_SIZE); + conn->status = CRYPTO_CONN_COOKIE_REQUESTING; conn->packet_send_rate = CRYPTO_PACKET_MIN_RATE; conn->packet_send_rate_requested = CRYPTO_PACKET_MIN_RATE; conn->packets_left = CRYPTO_MIN_QUEUE_LENGTH; conn->rtt_time = DEFAULT_PING_CONNECTION; - memcpy(conn->dht_public_key, dht_public_key, CRYPTO_PUBLIC_KEY_SIZE); + memcpy(conn->peer_dht_public_key, dht_public_key, CRYPTO_PUBLIC_KEY_SIZE); + + if (c->handshake_mode == CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY) { + conn->noise_handshake_enabled = false; + crypto_new_keypair(c->rng, conn->ephemeral_public_key, conn->ephemeral_secret_key); + random_nonce(c->rng, conn->send_nonce); + conn->legacy_ephemeral_keys_set = true; + } else { + conn->noise_handshake_enabled = true; + + const uint8_t prologue[] = {NOISE_PROTOCOL_VERSION}; + + if (noise_handshake_init(conn->noise_handshake, c->self_id_public_key, real_public_key, true, prologue, sizeof(prologue)) != 0) { + kill_tcp_connection_to(c->tcp_c, conn->connection_number_tcp); + wipe_crypto_connection(c, crypt_connection_id); + return -1; + } + + crypto_new_keypair(c->rng, conn->noise_handshake->ephemeral_public, conn->noise_handshake->ephemeral_private); + } conn->cookie_request_number = random_u64(c->rng); uint8_t cookie_request[COOKIE_REQUEST_LENGTH]; - if (create_cookie_request(c, cookie_request, conn->dht_public_key, conn->cookie_request_number, + if (create_cookie_request(c, cookie_request, conn->peer_dht_public_key, conn->cookie_request_number, conn->shared_key) != sizeof(cookie_request) || new_temp_packet(c, crypt_connection_id, cookie_request, sizeof(cookie_request)) != 0) { kill_tcp_connection_to(c->tcp_c, conn->connection_number_tcp); @@ -2169,6 +2954,7 @@ static int tcp_oob_callback(void *_Nonnull object, const uint8_t *_Nonnull publi const uint8_t *_Nonnull packet, uint16_t length, void *_Nullable userdata) { Net_Crypto *c = (Net_Crypto *)object; + if (length == 0 || length > MAX_CRYPTO_PACKET_SIZE) { return -1; } @@ -2177,7 +2963,7 @@ static int tcp_oob_callback(void *_Nonnull object, const uint8_t *_Nonnull publi return tcp_oob_handle_cookie_request(c, tcp_connections_number, public_key, packet, length); } - if (packet[0] == NET_PACKET_CRYPTO_HS) { + if (packet[0] == NET_PACKET_CRYPTO_HS || packet[0] == NET_PACKET_CRYPTO_NOISE_HS) { const IP_Port source = tcp_connections_number_to_ip_port(tcp_connections_number); if (handle_new_connection_handshake(c, &source, packet, length, userdata) != 0) { @@ -2441,8 +3227,9 @@ static int udp_handle_packet(void *_Nonnull object, const IP_Port *_Nonnull sour const int crypt_connection_id = crypto_id_ip_port(c, source); + /* No existing connection for this source; treat as new incoming handshake. */ if (crypt_connection_id == -1) { - if (packet[0] != NET_PACKET_CRYPTO_HS) { + if (packet[0] != NET_PACKET_CRYPTO_HS && packet[0] != NET_PACKET_CRYPTO_NOISE_HS) { return 1; } @@ -2934,7 +3721,7 @@ bool crypto_connection_status(const Net_Crypto *c, int crypt_connection_id, bool void new_keys(Net_Crypto *c) { - crypto_new_keypair(c->rng, c->self_public_key, c->self_secret_key); + crypto_new_keypair(c->rng, c->self_id_public_key, c->self_id_secret_key); } /** @brief Save the public and private keys to the keys array. @@ -2944,8 +3731,8 @@ void new_keys(Net_Crypto *c) */ void save_keys(const Net_Crypto *c, uint8_t *keys) { - memcpy(keys, c->self_public_key, CRYPTO_PUBLIC_KEY_SIZE); - memcpy(keys + CRYPTO_PUBLIC_KEY_SIZE, c->self_secret_key, CRYPTO_SECRET_KEY_SIZE); + memcpy(keys, c->self_id_public_key, CRYPTO_PUBLIC_KEY_SIZE); + memcpy(keys + CRYPTO_PUBLIC_KEY_SIZE, c->self_id_secret_key, CRYPTO_SECRET_KEY_SIZE); } /** @brief Load the secret key. @@ -2953,15 +3740,15 @@ void save_keys(const Net_Crypto *c, uint8_t *keys) */ void load_secret_key(Net_Crypto *c, const uint8_t *sk) { - memcpy(c->self_secret_key, sk, CRYPTO_SECRET_KEY_SIZE); - crypto_derive_public_key(c->self_public_key, c->self_secret_key); + memcpy(c->self_id_secret_key, sk, CRYPTO_SECRET_KEY_SIZE); + crypto_derive_public_key(c->self_id_public_key, c->self_id_secret_key); } /** @brief Create new instance of Net_Crypto. * Sets all the global connection variables to their default values. */ Net_Crypto *new_net_crypto(const Logger *log, const Memory *mem, const Random *rng, const Network *ns, - Mono_Time *mono_time, Networking_Core *net, void *dht, const Net_Crypto_DHT_Funcs *dht_funcs, const TCP_Proxy_Info *proxy_info, Net_Profile *tcp_np) + Mono_Time *mono_time, Networking_Core *net, void *dht, const Net_Crypto_DHT_Funcs *dht_funcs, const TCP_Proxy_Info *proxy_info, Net_Profile *tcp_np, Crypto_Handshake_Mode handshake_mode) { if (dht == nullptr || dht_funcs == nullptr || dht_funcs->get_shared_key_sent == nullptr || dht_funcs->get_self_public_key == nullptr || dht_funcs->get_self_secret_key == nullptr) { return nullptr; @@ -2995,14 +3782,18 @@ Net_Crypto *new_net_crypto(const Logger *log, const Memory *mem, const Random *r set_packet_tcp_connection_callback(temp->tcp_c, &tcp_data_callback, temp); set_oob_packet_tcp_connection_callback(temp->tcp_c, &tcp_oob_callback, temp); + /* Handshake mode selection: NOISE_ONLY, NOISE_BOTH, or LEGACY_ONLY */ + temp->handshake_mode = handshake_mode; + new_keys(temp); - new_symmetric_key(rng, temp->secret_symmetric_key); + new_symmetric_key(rng, temp->cookie_symmetric_key); temp->current_sleep_time = CRYPTO_SEND_PACKET_INTERVAL; networking_registerhandler(net, NET_PACKET_COOKIE_REQUEST, &udp_handle_cookie_request, temp); networking_registerhandler(net, NET_PACKET_COOKIE_RESPONSE, &udp_handle_packet, temp); networking_registerhandler(net, NET_PACKET_CRYPTO_HS, &udp_handle_packet, temp); + networking_registerhandler(net, NET_PACKET_CRYPTO_NOISE_HS, &udp_handle_packet, temp); networking_registerhandler(net, NET_PACKET_CRYPTO_DATA, &udp_handle_packet, temp); bs_list_init(&temp->ip_port_list, mem, sizeof(IP_Port), 8, ipport_cmp_handler); @@ -3050,6 +3841,7 @@ uint32_t crypto_run_interval(const Net_Crypto *c) /** Main loop. */ void do_net_crypto(Net_Crypto *c, void *userdata) { + // TODO(goldroom): rotate cookie_symmetric_key periodically (~2 minutes, cf. WireGuard) kill_timedout(c, userdata); do_tcp(c, userdata); send_crypto_packets(c); @@ -3072,6 +3864,7 @@ void kill_net_crypto(Net_Crypto *c) networking_registerhandler(c->net, NET_PACKET_COOKIE_REQUEST, nullptr, nullptr); networking_registerhandler(c->net, NET_PACKET_COOKIE_RESPONSE, nullptr, nullptr); networking_registerhandler(c->net, NET_PACKET_CRYPTO_HS, nullptr, nullptr); + networking_registerhandler(c->net, NET_PACKET_CRYPTO_NOISE_HS, nullptr, nullptr); networking_registerhandler(c->net, NET_PACKET_CRYPTO_DATA, nullptr, nullptr); crypto_memzero(c, sizeof(Net_Crypto)); mem_delete(mem, c); @@ -3082,7 +3875,16 @@ void nc_testonly_get_secrets(const Net_Crypto *c, int conn_id, uint8_t *shared_k const Crypto_Connection *conn = get_crypto_connection(c, conn_id); if (conn != nullptr) { memcpy(shared_key, conn->shared_key, CRYPTO_SHARED_KEY_SIZE); - memcpy(sent_nonce, conn->sent_nonce, CRYPTO_NONCE_SIZE); + memcpy(sent_nonce, conn->send_nonce, CRYPTO_NONCE_SIZE); memcpy(recv_nonce, conn->recv_nonce, CRYPTO_NONCE_SIZE); } } + +bool nc_testonly_get_noise_enabled(const Net_Crypto *c, int conn_id) +{ + const Crypto_Connection *conn = get_crypto_connection(c, conn_id); + if (conn == nullptr) { + return false; + } + return conn->noise_handshake_enabled; +} diff --git a/toxcore/net_crypto.h b/toxcore/net_crypto.h index eb751216ba..9dbad7eab4 100644 --- a/toxcore/net_crypto.h +++ b/toxcore/net_crypto.h @@ -23,12 +23,22 @@ #include "net.h" #include "net_profile.h" #include "network.h" +#include "noise.h" #include "rng.h" #ifdef __cplusplus extern "C" { #endif +/** + * @brief Handshake mode selection for crypto connections. + */ +typedef enum Crypto_Handshake_Mode { + CRYPTO_HANDSHAKE_MODE_NOISE_ONLY, /* Only NoiseIK handshake */ + CRYPTO_HANDSHAKE_MODE_NOISE_BOTH, /* NoiseIK preferred, fall back to legacy (default) */ + CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY, /* Only legacy handshake (for old peers / testing) */ +} Crypto_Handshake_Mode; + typedef const uint8_t *_Nullable nc_dht_get_shared_key_sent_cb(void *_Nonnull obj, const uint8_t *_Nonnull public_key); typedef const uint8_t *_Nonnull nc_dht_get_self_public_key_cb(const void *_Nonnull obj); typedef const uint8_t *_Nonnull nc_dht_get_self_secret_key_cb(const void *_Nonnull obj); @@ -144,11 +154,13 @@ TCP_Connections *_Nonnull nc_get_tcp_c(const Net_Crypto *_Nonnull c); typedef struct New_Connection { IP_Port source; - uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The real public key of the peer. */ - uint8_t dht_public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The dht public key of the peer. */ - uint8_t recv_nonce[CRYPTO_NONCE_SIZE]; /* Nonce of received packets. */ - uint8_t peersessionpublic_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The public key of the peer. */ - uint8_t *_Nonnull cookie; + uint8_t peer_id_public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The real public key of the peer. */ + uint8_t peer_dht_public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The dht public key of the peer. */ + uint8_t recv_nonce[CRYPTO_NONCE_SIZE]; /* Nonce to decrypt received packets. */ + uint8_t peer_ephemeral_public_key[CRYPTO_PUBLIC_KEY_SIZE]; /* The public key of the peer. */ + Noise_Handshake *_Nullable noise_handshake; + + uint8_t *_Nullable peer_cookie; uint8_t cookie_length; } New_Connection; @@ -385,7 +397,7 @@ void load_secret_key(Net_Crypto *_Nonnull c, const uint8_t *_Nonnull sk); */ Net_Crypto *_Nullable new_net_crypto(const Logger *_Nonnull log, const Memory *_Nonnull mem, const Random *_Nonnull rng, const Network *_Nonnull ns, Mono_Time *_Nonnull mono_time, Networking_Core *_Nonnull net, void *_Nonnull dht, const Net_Crypto_DHT_Funcs *_Nonnull dht_funcs, - const TCP_Proxy_Info *_Nonnull proxy_info, Net_Profile *_Nonnull tcp_np); + const TCP_Proxy_Info *_Nonnull proxy_info, Net_Profile *_Nonnull tcp_np, Crypto_Handshake_Mode handshake_mode); /** return the optimal interval in ms for running do_net_crypto. */ uint32_t crypto_run_interval(const Net_Crypto *_Nonnull c); @@ -396,6 +408,7 @@ void kill_net_crypto(Net_Crypto *_Nullable c); /** Unit test support functions. Do not use outside tests. */ void nc_testonly_get_secrets(const Net_Crypto *_Nonnull c, int conn_id, uint8_t *_Nonnull shared_key, uint8_t *_Nonnull sent_nonce, uint8_t *_Nonnull recv_nonce); +bool nc_testonly_get_noise_enabled(const Net_Crypto *_Nonnull c, int conn_id); #ifdef __cplusplus } /* extern "C" */ diff --git a/toxcore/net_crypto_fuzz_test.cc b/toxcore/net_crypto_fuzz_test.cc index d8dc8b2f0b..59dbd446e2 100644 --- a/toxcore/net_crypto_fuzz_test.cc +++ b/toxcore/net_crypto_fuzz_test.cc @@ -102,7 +102,8 @@ void TestNetCrypto(Fuzz_Data &input) const Ptr net_crypto( new_net_crypto(logger.get(), &node->c_memory, &node->c_random, &node->c_network, - mono_time.get(), net.get(), dht.get(), &dht_funcs, &proxy_info, tcp_np), + mono_time.get(), net.get(), dht.get(), &dht_funcs, &proxy_info, tcp_np, + CRYPTO_HANDSHAKE_MODE_NOISE_BOTH), kill_net_crypto); if (net_crypto == nullptr) { netprof_kill(&node->c_memory, tcp_np); diff --git a/toxcore/net_crypto_test.cc b/toxcore/net_crypto_test.cc index 5ae1062a21..5d188c2dfb 100644 --- a/toxcore/net_crypto_test.cc +++ b/toxcore/net_crypto_test.cc @@ -31,7 +31,8 @@ using namespace tox::test; template class TestNode { public: - TestNode(SimulatedEnvironment &env, std::uint16_t port, bool enable_trace = false) + TestNode(SimulatedEnvironment &env, std::uint16_t port, + Crypto_Handshake_Mode mode = CRYPTO_HANDSHAKE_MODE_NOISE_BOTH, bool enable_trace = false) : dht_wrapper_(env, port) , net_profile_(netprof_new(dht_wrapper_.logger(), &dht_wrapper_.node().c_memory), [mem = &dht_wrapper_.node().c_memory](Net_Profile *p) { netprof_kill(mem, p); }) @@ -56,7 +57,7 @@ class TestNode { net_crypto_.reset(new_net_crypto(dht_wrapper_.logger(), &dht_wrapper_.node().c_memory, &dht_wrapper_.node().c_random, &dht_wrapper_.node().c_network, dht_wrapper_.mono_time(), dht_wrapper_.networking(), dht_wrapper_.get_dht(), &DHTWrapper::funcs, &proxy_info, - net_profile_.get())); + net_profile_.get(), mode)); // 4. Register Callbacks new_connection_handler(net_crypto_.get(), &TestNode::static_new_connection_cb, this); @@ -134,6 +135,11 @@ class TestNode { // Helper to get the ID assigned to a peer by Public Key (for the acceptor side) int get_connection_id_by_pk(const std::uint8_t *pk) { return last_accepted_id_; } + bool is_noise_enabled(int conn_id) const + { + return nc_testonly_get_noise_enabled(net_crypto_.get(), conn_id); + } + ~TestNode(); private: @@ -582,4 +588,1093 @@ TEST_F(NetCryptoTest, EndToEndDataExchange_RealDHT) EXPECT_TRUE(data_received) << "Bob did not receive the correct data"; } +// --- Handshake Mode Integration Tests --- + +// Helper: run until both sides are connected, return true on success. +template +bool wait_for_connection(SimulatedEnvironment &env, NodeA &a, int a_conn_id, NodeB &b, + int &b_conn_id, std::uint64_t timeout_ms = 5000) +{ + auto start = env.clock().current_time_ms(); + while ((env.clock().current_time_ms() - start) < timeout_ms) { + a.poll(); + b.poll(); + env.advance_time(10); + + b_conn_id = b.get_connection_id_by_pk(a.real_public_key()); + if (a.is_connected(a_conn_id) && b_conn_id != -1 && b.is_connected(b_conn_id)) { + return true; + } + } + return false; +} + +// Helper: send data from a to b, wait for delivery, return true on success. +template +bool exchange_data(SimulatedEnvironment &env, NodeA &a, int a_conn_id, NodeB &b, int b_conn_id, + const std::vector &message, std::uint64_t timeout_ms = 2000) +{ + if (!a.send_data(a_conn_id, message)) { + return false; + } + auto start = env.clock().current_time_ms(); + while ((env.clock().current_time_ms() - start) < timeout_ms) { + a.poll(); + b.poll(); + env.advance_time(10); + if (b.get_last_received_data(b_conn_id) == message) { + return true; + } + } + return false; +} + +TEST_F(NetCryptoTest, NoiseHandshakeDataExchange) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id)); + + // Both should have used Noise handshake + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + // Exchange data + std::vector message = {160, 'N', 'o', 'i', 's', 'e'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +TEST_F(NetCryptoTest, LegacyHandshakeDataExchange) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id)); + + // Both should have used legacy handshake + EXPECT_FALSE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_FALSE(bob.is_noise_enabled(bob_conn_id)); + + // Exchange data + std::vector message = {160, 'L', 'e', 'g', 'a', 'c', 'y'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +TEST_F(NetCryptoTest, NoiseToLegacyFallback) +{ + // One NOISE_BOTH node connects to one LEGACY_ONLY node. + // Both sides initiate (mirrors real friend_connection behavior). + // Bob's legacy handshake triggers Alice to fall back from Noise. + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + // Bob also initiates (as friend_connection would do bidirectionally) + int bob_conn_id_init = bob.connect_to(alice); + ASSERT_NE(bob_conn_id_init, -1); + + // Run until connected + auto start = env.clock().current_time_ms(); + bool connected = false; + int bob_conn_id = -1; + while ((env.clock().current_time_ms() - start) < 10000) { + alice.poll(); + bob.poll(); + env.advance_time(10); + + // Check both possible connection IDs on Bob's side + if (alice.is_connected(alice_conn_id)) { + // Bob may have connected via his initiated connection or accepted Alice's + if (bob.is_connected(bob_conn_id_init)) { + bob_conn_id = bob_conn_id_init; + connected = true; + break; + } + int bob_accepted = bob.get_connection_id_by_pk(alice.real_public_key()); + if (bob_accepted != -1 && bob.is_connected(bob_accepted)) { + bob_conn_id = bob_accepted; + connected = true; + break; + } + } + } + + ASSERT_TRUE(connected) << "Noise-to-legacy fallback should succeed"; + + // Both should have fallen back to legacy + EXPECT_FALSE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_FALSE(bob.is_noise_enabled(bob_conn_id)); + + // Exchange data + std::vector message = {160, 'F', 'a', 'l', 'l'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +TEST_F(NetCryptoTest, NoiseSimultaneousOpen) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + // Both sides initiate simultaneously + int alice_conn_id = alice.connect_to(bob); + int bob_conn_id_init = bob.connect_to(alice); + ASSERT_NE(alice_conn_id, -1); + ASSERT_NE(bob_conn_id_init, -1); + + // Run until at least one side is connected + auto start = env.clock().current_time_ms(); + bool connected = false; + while ((env.clock().current_time_ms() - start) < 5000) { + alice.poll(); + bob.poll(); + env.advance_time(10); + + if (alice.is_connected(alice_conn_id) && bob.is_connected(bob_conn_id_init)) { + connected = true; + break; + } + // Also check if bob accepted alice's connection + int bob_accepted = bob.get_connection_id_by_pk(alice.real_public_key()); + if (alice.is_connected(alice_conn_id) && bob_accepted != -1 + && bob.is_connected(bob_accepted)) { + connected = true; + break; + } + } + + ASSERT_TRUE(connected) << "Simultaneous open should succeed"; + + // Exchange data (use alice's initiated connection) + std::vector message = {160, 'S', 'i', 'm'}; + // Try sending from Alice; Bob may have accepted via callback + int bob_conn_id = bob.get_connection_id_by_pk(alice.real_public_key()); + if (bob_conn_id != -1 && bob.is_connected(bob_conn_id)) { + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); + } +} + +TEST_F(NetCryptoTest, NoiseHandshakePacketLoss) +{ + // Declare before nodes so it outlives them (filter is invoked during destruction). + bool dropped = false; + + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + // Drop the first Noise handshake packet + env.simulation().net().add_filter([&](tox::test::Packet &p) { + if (!dropped && p.data.size() > 0 && p.data[0] == NET_PACKET_CRYPTO_NOISE_HS) { + dropped = true; + return false; // Drop it + } + return true; + }); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + // Allow more time for retransmission (handshake retransmit interval ~1s) + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id, 10000)) + << "Handshake should succeed after retransmission"; + EXPECT_TRUE(dropped) << "Filter should have dropped a handshake packet"; + + // Exchange data + std::vector message = {160, 'R', 'e', 't', 'r', 'y'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +// --- Security Regression Tests --- + +// Compute legacy HANDSHAKE_PACKET_LENGTH from public constants. +// COOKIE_LENGTH = NONCE + sizeof(uint64_t) + 2*PK + MAC = 24+8+64+16 = 112 +constexpr std::size_t kCookieLength + = CRYPTO_NONCE_SIZE + sizeof(std::uint64_t) + CRYPTO_PUBLIC_KEY_SIZE * 2 + CRYPTO_MAC_SIZE; +constexpr std::size_t kLegacyHandshakePacketLength = 1 + kCookieLength + CRYPTO_NONCE_SIZE + + CRYPTO_NONCE_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_SHA512_SIZE + kCookieLength + + CRYPTO_MAC_SIZE; + +// A NOISE_BOTH node must not destroy its Noise handshake state when it receives +// an invalid packet that happens to be legacy handshake length. The Noise state +// must survive so the handshake can complete via retransmission. +TEST_F(NetCryptoTest, NoiseStateSurvivesGarbageLegacyLengthPacket) +{ + // Declare before nodes so they outlive them (filter is invoked during destruction). + int hs_count = 0; + bool injected = false; + + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + // Replace the first Noise HS packet arriving at Alice (Bob's response) with + // garbage of exactly legacy handshake length using the legacy type (0x1a). + // The first Noise HS on the wire is Alice's initiator HS to Bob; the second + // is Bob's response to Alice. + env.simulation().net().add_filter([&](tox::test::Packet &p) { + if (p.data.size() > 0 && p.data[0] == NET_PACKET_CRYPTO_NOISE_HS) { + ++hs_count; + if (hs_count == 2 && !injected) { + p.data.assign(kLegacyHandshakePacketLength, 0xAA); + p.data[0] = NET_PACKET_CRYPTO_HS; + injected = true; + } + } + return true; + }); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id, 10000)) + << "Noise handshake must recover after receiving a garbage legacy-length packet"; + EXPECT_TRUE(injected) << "Filter should have injected a garbage packet"; + + // Connection completed via Noise (not downgraded to legacy). + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + std::vector message = {160, 'O', 'K'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +// Two nodes with the same identity key attempting simultaneous open must not +// deadlock. The connection should be cleanly rejected (self-connection). +TEST_F(NetCryptoTest, SimultaneousOpenEqualKeysIsRejected) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + // Give both nodes the same identity key. + std::uint8_t shared_sk[CRYPTO_SECRET_KEY_SIZE]; + std::memset(shared_sk, 42, CRYPTO_SECRET_KEY_SIZE); + load_secret_key(alice.get_net_crypto(), shared_sk); + load_secret_key(bob.get_net_crypto(), shared_sk); + ASSERT_EQ( + std::memcmp(alice.real_public_key(), bob.real_public_key(), CRYPTO_PUBLIC_KEY_SIZE), 0); + + // Both sides initiate simultaneously. + int alice_conn_id = alice.connect_to(bob); + int bob_conn_id_init = bob.connect_to(alice); + ASSERT_NE(alice_conn_id, -1); + ASSERT_NE(bob_conn_id_init, -1); + + // Neither side should reach CONNECTED, so the equal-key case must be detected + // and the connection rejected rather than silently deadlocking. + auto start = env.clock().current_time_ms(); + bool connected = false; + while ((env.clock().current_time_ms() - start) < 10000) { + alice.poll(); + bob.poll(); + env.advance_time(10); + + if (alice.is_connected(alice_conn_id) || bob.is_connected(bob_conn_id_init)) { + connected = true; + break; + } + } + + EXPECT_FALSE(connected) << "Equal-key nodes must not establish a connection"; +} + +// --- Additional Noise IK Handshake Tests --- + +TEST_F(NetCryptoTest, NoiseOnlyBothSidesConnect) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id)); + + // Both should have used Noise handshake + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + // Exchange data in both directions + std::vector msg_ab = {160, 'N', 'O', 'a', 'b'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, msg_ab)); + + std::vector msg_ba = {160, 'N', 'O', 'b', 'a'}; + EXPECT_TRUE(exchange_data(env, bob, bob_conn_id, alice, alice_conn_id, msg_ba)); +} + +TEST_F(NetCryptoTest, NoiseOnlyVsLegacyOnlyFails) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY); + + // Both sides initiate (mirrors real friend_connection behavior) + int alice_conn_id = alice.connect_to(bob); + int bob_conn_id_init = bob.connect_to(alice); + ASSERT_NE(alice_conn_id, -1); + ASSERT_NE(bob_conn_id_init, -1); + + // Poll for 15s simulated time, exceeding handshake retry budget + auto start = env.clock().current_time_ms(); + bool connected = false; + while ((env.clock().current_time_ms() - start) < 15000) { + alice.poll(); + bob.poll(); + env.advance_time(50); + + if (alice.is_connected(alice_conn_id) || bob.is_connected(bob_conn_id_init)) { + connected = true; + break; + } + int bob_accepted = bob.get_connection_id_by_pk(alice.real_public_key()); + if (bob_accepted != -1 && bob.is_connected(bob_accepted)) { + connected = true; + break; + } + int alice_accepted = alice.get_connection_id_by_pk(bob.real_public_key()); + if (alice_accepted != -1 && alice.is_connected(alice_accepted)) { + connected = true; + break; + } + } + + EXPECT_FALSE(connected) << "NOISE_ONLY and LEGACY_ONLY should not be able to connect"; +} + +TEST_F(NetCryptoTest, LegacyOnlyVsNoiseOnlyFails) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY); + + // Only LEGACY_ONLY side initiates to NOISE_ONLY + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + // Poll for 15s simulated time + auto start = env.clock().current_time_ms(); + bool connected = false; + while ((env.clock().current_time_ms() - start) < 15000) { + alice.poll(); + bob.poll(); + env.advance_time(50); + + if (alice.is_connected(alice_conn_id)) { + connected = true; + break; + } + int bob_accepted = bob.get_connection_id_by_pk(alice.real_public_key()); + if (bob_accepted != -1 && bob.is_connected(bob_accepted)) { + connected = true; + break; + } + } + + EXPECT_FALSE(connected) << "LEGACY_ONLY initiator should not connect to NOISE_ONLY responder"; +} + +TEST_F(NetCryptoTest, SimultaneousOpenForcedRoleSwitch) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + // Set up controlled keys so that memcmp(alice_pk, bob_pk) < 0. + // Alice gets the "lower" key → she is forced through the initiator→responder + // switch path during simultaneous open. + std::uint8_t sk_a[CRYPTO_SECRET_KEY_SIZE]; + std::uint8_t sk_b[CRYPTO_SECRET_KEY_SIZE]; + std::memset(sk_a, 1, CRYPTO_SECRET_KEY_SIZE); + std::memset(sk_b, 2, CRYPTO_SECRET_KEY_SIZE); + + load_secret_key(alice.get_net_crypto(), sk_a); + load_secret_key(bob.get_net_crypto(), sk_b); + + // Verify ordering: alice_pk < bob_pk. If not, swap the keys. + if (std::memcmp(alice.real_public_key(), bob.real_public_key(), CRYPTO_PUBLIC_KEY_SIZE) >= 0) { + load_secret_key(alice.get_net_crypto(), sk_b); + load_secret_key(bob.get_net_crypto(), sk_a); + } + ASSERT_LT( + std::memcmp(alice.real_public_key(), bob.real_public_key(), CRYPTO_PUBLIC_KEY_SIZE), 0) + << "Test setup error: alice_pk must be less than bob_pk"; + + // Both sides initiate simultaneously + int alice_conn_id = alice.connect_to(bob); + int bob_conn_id_init = bob.connect_to(alice); + ASSERT_NE(alice_conn_id, -1); + ASSERT_NE(bob_conn_id_init, -1); + + // Run until connected + auto start = env.clock().current_time_ms(); + bool connected = false; + int bob_conn_id = -1; + while ((env.clock().current_time_ms() - start) < 10000) { + alice.poll(); + bob.poll(); + env.advance_time(10); + + if (alice.is_connected(alice_conn_id)) { + if (bob.is_connected(bob_conn_id_init)) { + bob_conn_id = bob_conn_id_init; + connected = true; + break; + } + int bob_accepted = bob.get_connection_id_by_pk(alice.real_public_key()); + if (bob_accepted != -1 && bob.is_connected(bob_accepted)) { + bob_conn_id = bob_accepted; + connected = true; + break; + } + } + } + + ASSERT_TRUE(connected) << "Simultaneous open with forced role switch should succeed"; + + // Connection must use Noise + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + // Bidirectional data exchange + std::vector msg_ab = {160, 'R', 'o', 'l', 'e'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, msg_ab)); + + std::vector msg_ba = {160, 'S', 'w', 'a', 'p'}; + EXPECT_TRUE(exchange_data(env, bob, bob_conn_id, alice, alice_conn_id, msg_ba)); +} + +TEST_F(NetCryptoTest, InvalidPacketLengthDoesNotCorruptState) +{ + // Declare before nodes so it outlives them (filter is invoked during destruction). + bool injected = false; + + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + // Inject a 50-byte NET_PACKET_CRYPTO_NOISE_HS packet (not a valid handshake length) + // targeting Bob's port. This should be silently rejected without corrupting state. + env.simulation().net().add_filter([&](tox::test::Packet &p) { + if (!injected && p.data.size() > 0 && p.data[0] == NET_PACKET_CRYPTO_NOISE_HS + && net_ntohs(p.to.port) == 33446) { + // Replace with a garbage packet of invalid length (50 bytes) + p.data.assign(50, 0xBB); + p.data[0] = NET_PACKET_CRYPTO_NOISE_HS; + injected = true; + } + return true; + }); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id, 10000)) + << "Connection should succeed via retransmission after invalid-length packet"; + EXPECT_TRUE(injected) << "Filter should have injected the invalid-length packet"; + + // Noise should still be used + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + // Data exchange should work + std::vector message = {160, 'O', 'K'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +TEST_F(NetCryptoTest, HighPacketCountNonceWrap) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id)); + + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + // Send many small packets to exercise the 2-byte nonce counter well past + // DATA_NUM_THRESHOLD (21845) and past the buffer size (32768), verifying + // that nonce management and buffer cycling work correctly under sustained + // high-throughput transfer. + constexpr int kTargetPackets = 34000; + int packets_sent = 0; + std::vector small_msg = {160, 'X'}; + + auto start = env.clock().current_time_ms(); + while (packets_sent < kTargetPackets && (env.clock().current_time_ms() - start) < 600000) { + // Try to send as many packets as the buffer allows + bool sent_any = false; + while (packets_sent < kTargetPackets && alice.send_data(alice_conn_id, small_msg)) { + ++packets_sent; + sent_any = true; + } + + // Poll to drain buffers and advance time. + // When buffer is full, poll more aggressively to drain it. + int drain_rounds = sent_any ? 5 : 20; + for (int i = 0; i < drain_rounds; ++i) { + alice.poll(); + bob.poll(); + env.advance_time(5); + } + } + + ASSERT_GE(packets_sent, kTargetPackets) << "Should have sent at least " << kTargetPackets + << " packets (sent " << packets_sent << ")"; + + // Connection must still be alive. + ASSERT_TRUE(alice.is_connected(alice_conn_id)) + << "Alice connection should still be alive after high packet count"; + ASSERT_TRUE(bob.is_connected(bob_conn_id)) + << "Bob connection should still be alive after high packet count"; + + // Drain buffered packets before the final exchange. The congestion control + // rate-limits sending, so we need significant simulated time. + std::vector final_msg = {160, 'F', 'i', 'n', 'a', 'l'}; + bool final_ok = false; + auto drain_start = env.clock().current_time_ms(); + while ((env.clock().current_time_ms() - drain_start) < 120000) { + alice.poll(); + bob.poll(); + env.advance_time(50); + + // Try the final exchange once the buffer has drained enough. + if (!final_ok && alice.send_data(alice_conn_id, final_msg)) { + // Send succeeded, now wait for delivery. + for (int i = 0; i < 200; ++i) { + alice.poll(); + bob.poll(); + env.advance_time(10); + if (bob.get_last_received_data(bob_conn_id) == final_msg) { + final_ok = true; + break; + } + } + } + if (final_ok) { + break; + } + } + + EXPECT_TRUE(final_ok) << "Data exchange should succeed after high packet count"; +} + +// --- Noise IK Handshake Regression & Edge Case Tests --- + +// Regression test for the bug where handle_noise_hs returned 0 in the +// "stay initiator" simultaneous open path (bob_pk > alice_pk) without +// populating dht_public_key, leading to use of uninitialized stack data. +TEST_F(NetCryptoTest, SimultaneousOpenStayInitiatorCompletes) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY); + + // Force bob_pk > alice_pk so bob takes the "stay initiator, ignore packet" + // path during simultaneous open. + std::uint8_t sk_a[CRYPTO_SECRET_KEY_SIZE]; + std::uint8_t sk_b[CRYPTO_SECRET_KEY_SIZE]; + std::memset(sk_a, 1, CRYPTO_SECRET_KEY_SIZE); + std::memset(sk_b, 2, CRYPTO_SECRET_KEY_SIZE); + + load_secret_key(alice.get_net_crypto(), sk_a); + load_secret_key(bob.get_net_crypto(), sk_b); + + // Ensure bob_pk > alice_pk; swap if needed. + if (std::memcmp(bob.real_public_key(), alice.real_public_key(), CRYPTO_PUBLIC_KEY_SIZE) <= 0) { + load_secret_key(alice.get_net_crypto(), sk_b); + load_secret_key(bob.get_net_crypto(), sk_a); + } + ASSERT_GT( + std::memcmp(bob.real_public_key(), alice.real_public_key(), CRYPTO_PUBLIC_KEY_SIZE), 0) + << "Test setup error: bob_pk must be greater than alice_pk"; + + // Both sides initiate simultaneously + int alice_conn_id = alice.connect_to(bob); + int bob_conn_id_init = bob.connect_to(alice); + ASSERT_NE(alice_conn_id, -1); + ASSERT_NE(bob_conn_id_init, -1); + + // Wait for connection with extended timeout for simultaneous open + auto start = env.clock().current_time_ms(); + bool connected = false; + int bob_conn_id = -1; + while ((env.clock().current_time_ms() - start) < 10000) { + alice.poll(); + bob.poll(); + env.advance_time(10); + + if (alice.is_connected(alice_conn_id)) { + if (bob.is_connected(bob_conn_id_init)) { + bob_conn_id = bob_conn_id_init; + connected = true; + break; + } + int bob_accepted = bob.get_connection_id_by_pk(alice.real_public_key()); + if (bob_accepted != -1 && bob.is_connected(bob_accepted)) { + bob_conn_id = bob_accepted; + connected = true; + break; + } + } + } + + ASSERT_TRUE(connected) << "Simultaneous open with bob_pk > alice_pk should succeed"; + + // Must use Noise + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + // Bidirectional data exchange + std::vector msg_ab = {160, 'S', 't', 'a', 'y'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, msg_ab)); + + std::vector msg_ba = {160, 'I', 'n', 'i', 't'}; + EXPECT_TRUE(exchange_data(env, bob, bob_conn_id, alice, alice_conn_id, msg_ba)); +} + +// NOISE_ONLY simultaneous open with random keys (non-deterministic role). +// Existing simultaneous open tests use NOISE_BOTH; this verifies NOISE_ONLY. +TEST_F(NetCryptoTest, NoiseOnlySimultaneousOpen) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY); + + // Both sides initiate simultaneously + int alice_conn_id = alice.connect_to(bob); + int bob_conn_id_init = bob.connect_to(alice); + ASSERT_NE(alice_conn_id, -1); + ASSERT_NE(bob_conn_id_init, -1); + + // Wait for connection + auto start = env.clock().current_time_ms(); + bool connected = false; + int bob_conn_id = -1; + while ((env.clock().current_time_ms() - start) < 10000) { + alice.poll(); + bob.poll(); + env.advance_time(10); + + if (alice.is_connected(alice_conn_id)) { + if (bob.is_connected(bob_conn_id_init)) { + bob_conn_id = bob_conn_id_init; + connected = true; + break; + } + int bob_accepted = bob.get_connection_id_by_pk(alice.real_public_key()); + if (bob_accepted != -1 && bob.is_connected(bob_accepted)) { + bob_conn_id = bob_accepted; + connected = true; + break; + } + } + } + + ASSERT_TRUE(connected) << "NOISE_ONLY simultaneous open should succeed"; + + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + // Data exchange + std::vector msg_ab = {160, 'N', 'O', 's', 'i', 'm'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, msg_ab)); + + std::vector msg_ba = {160, 'N', 'O', 'b', 'a'}; + EXPECT_TRUE(exchange_data(env, bob, bob_conn_id, alice, alice_conn_id, msg_ba)); +} + +// Flipping a byte in the Noise HS encrypted payload simulates AEAD authentication +// failure (wrong peer identity or packet tampering). The receiver rejects the +// corrupted packet, but Noise state survives for retransmission. +TEST_F(NetCryptoTest, NoiseHandshakeCorruptedPayloadRejected) +{ + // Declare before nodes so it outlives them (filter is invoked during destruction). + bool corrupted = false; + + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + // Flip a byte in the first Noise HS encrypted payload arriving at Bob. + // Offset > 34 to hit AEAD-protected data, not version byte or unencrypted + // ephemeral key. + env.simulation().net().add_filter([&](tox::test::Packet &p) { + if (!corrupted && p.data.size() > 40 && p.data[0] == NET_PACKET_CRYPTO_NOISE_HS + && net_ntohs(p.to.port) == 33446) { + p.data[40] ^= 0xFF; + corrupted = true; + } + return true; + }); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id, 10000)) + << "Connection should succeed via retransmission after corrupted handshake"; + EXPECT_TRUE(corrupted) << "Filter should have corrupted a handshake packet"; + + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + std::vector message = {160, 'A', 'E', 'A', 'D'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +// After handshake completes and noise_split_and_cleanup destroys handshake +// state, a replayed Noise HS packet should be rejected without disrupting the +// established connection. +TEST_F(NetCryptoTest, NoiseHandshakeReplayAfterCompletion) +{ + // Declare before nodes so they outlive them (filter is invoked during destruction). + std::vector saved_hs_data; + IP_Port saved_to{}; + IP_Port saved_from{}; + + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + // Capture a copy of a Noise HS packet seen on the wire. + env.simulation().net().add_filter([&](tox::test::Packet &p) { + if (saved_hs_data.empty() && p.data.size() > 0 && p.data[0] == NET_PACKET_CRYPTO_NOISE_HS) { + saved_hs_data = p.data; + saved_to = p.to; + saved_from = p.from; + } + return true; + }); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id)); + ASSERT_FALSE(saved_hs_data.empty()) << "Should have captured a Noise HS packet"; + + // Re-inject the saved handshake packet after the connection is established. + tox::test::Packet replay{}; + replay.to = saved_to; + replay.from = saved_from; + replay.data = saved_hs_data; + env.simulation().net().send_packet(replay); + + // Poll for a bit to ensure the replayed packet is processed. + auto start = env.clock().current_time_ms(); + while ((env.clock().current_time_ms() - start) < 2000) { + alice.poll(); + bob.poll(); + env.advance_time(10); + } + + // Connection must remain established. + EXPECT_TRUE(alice.is_connected(alice_conn_id)) + << "Replayed HS packet must not disrupt established connection"; + EXPECT_TRUE(bob.is_connected(bob_conn_id)) + << "Replayed HS packet must not disrupt established connection"; + + // Data exchange still works. + std::vector message = {160, 'R', 'p', 'l', 'y'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +// A NOISE_ONLY node receiving a legacy handshake packet should reject it +// without corrupting its Noise state. The connection succeeds via +// retransmission of the real Noise HS. +TEST_F(NetCryptoTest, NoiseOnlyStateIntactAfterLegacyPacket) +{ + // Declare before nodes so it outlives them (filter is invoked during destruction). + bool replaced = false; + + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY); + + // Replace the first Noise HS packet arriving at Bob with a legacy-type + // packet of correct legacy handshake length. + env.simulation().net().add_filter([&](tox::test::Packet &p) { + if (!replaced && p.data.size() > 0 && p.data[0] == NET_PACKET_CRYPTO_NOISE_HS + && net_ntohs(p.to.port) == 33446) { + p.data.assign(kLegacyHandshakePacketLength, 0xCC); + p.data[0] = NET_PACKET_CRYPTO_HS; + replaced = true; + } + return true; + }); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id, 10000)) + << "NOISE_ONLY should recover via retransmission after receiving legacy packet"; + EXPECT_TRUE(replaced) << "Filter should have replaced a Noise HS with legacy packet"; + + // Both sides must still be using Noise (not downgraded) + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + // Data exchange works + std::vector message = {160, 'N', 'O', 'l', 'g'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +// --- Parameterised reconnect test (all compatible mode pairs) --- + +using ModePair = std::pair; + +class NetCryptoReconnectTest : public ::testing::TestWithParam { +protected: + SimulatedEnvironment env{12345}; +}; + +static const char *mode_name(Crypto_Handshake_Mode m) +{ + switch (m) { + case CRYPTO_HANDSHAKE_MODE_NOISE_ONLY: + return "NoiseOnly"; + case CRYPTO_HANDSHAKE_MODE_NOISE_BOTH: + return "NoiseBoth"; + case CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY: + return "LegacyOnly"; + } + return "Unknown"; +} + +// After total packet loss (simulating extreme network outage), a new +// handshake must succeed and data exchange must resume normally. +// +// Note: net_crypto does not currently time out established connections +// (TODO in net_crypto.c). This test verifies the reconnect path by +// simulating the outage as a blackout period and then initiating a new +// connection explicitly, as the upper layer (friend_connection) would. +TEST_P(NetCryptoReconnectTest, ReconnectAfterExtremePacketLoss) +{ + const auto [alice_mode, bob_mode] = GetParam(); + bool blackout = false; + + NetCryptoNode alice(env, 33445, alice_mode); + NetCryptoNode bob(env, 33446, bob_mode); + + // Drop all packets while blackout is active. + env.simulation().net().add_filter([&](tox::test::Packet & /*p*/) { return !blackout; }); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id, 15000)) + << "Initial connection should establish before blackout"; + + // Simulate a total network outage for 5 seconds. + blackout = true; + for (int i = 0; i < 50; ++i) { + alice.poll(); + bob.poll(); + env.advance_time(100); + } + blackout = false; + + // Reconnect on the restored network (as the upper layer would do). + alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id, 15000)) + << "Reconnection should succeed after packet loss cleared"; + + std::vector message = {160, 'R', 'e', 'c', 'o', 'n'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)) + << "Data exchange should work on reconnected session"; +} + +INSTANTIATE_TEST_SUITE_P(AllCompatiblePairs, NetCryptoReconnectTest, + ::testing::Values( + // Both Noise-only + ModePair{CRYPTO_HANDSHAKE_MODE_NOISE_ONLY, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY}, + // Noise-only with BOTH (negotiates Noise) + ModePair{CRYPTO_HANDSHAKE_MODE_NOISE_ONLY, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH}, + ModePair{CRYPTO_HANDSHAKE_MODE_NOISE_BOTH, CRYPTO_HANDSHAKE_MODE_NOISE_ONLY}, + // Both BOTH + ModePair{CRYPTO_HANDSHAKE_MODE_NOISE_BOTH, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH}, + // Legacy-only initiating to BOTH (BOTH accepts legacy) + // Note: BOTH initiating to legacy-only requires both sides to initiate + // for the fallback to trigger; see NoiseToLegacyFallback for that case. + ModePair{CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH}, + // Both legacy-only + ModePair{CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY, CRYPTO_HANDSHAKE_MODE_LEGACY_ONLY}), + [](const ::testing::TestParamInfo ¶m_info) { + std::string name = mode_name(param_info.param.first); + name += "_"; + name += mode_name(param_info.param.second); + return name; + }); + +TEST_F(NetCryptoTest, NoiseHandshakeResponderReplyDropRecovers) +{ + bool dropped = false; + + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + env.simulation().net().add_filter([&](tox::test::Packet &p) { + if (!dropped && p.data.size() > 0 && p.data[0] == NET_PACKET_CRYPTO_NOISE_HS + && net_ntohs(p.to.port) == 33445) { + dropped = true; + return false; + } + return true; + }); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id, 15000)) + << "Connection must recover after responder reply is dropped"; + EXPECT_TRUE(dropped); + + std::vector message = {160, 'R', 'p', 'D', 'r', 'p'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +TEST_F(NetCryptoTest, NoiseHandshakeInitiatorReplayBeforeCompletion) +{ + std::vector saved; + IP_Port saved_to{}; + IP_Port saved_from{}; + bool injected = false; + + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + env.simulation().net().add_filter([&](tox::test::Packet &p) { + if (saved.empty() && p.data.size() > 0 && p.data[0] == NET_PACKET_CRYPTO_NOISE_HS + && net_ntohs(p.to.port) == 33446) { + saved = p.data; + saved_to = p.to; + saved_from = p.from; + } + return true; + }); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + auto start = env.clock().current_time_ms(); + while ((env.clock().current_time_ms() - start) < 200) { + alice.poll(); + bob.poll(); + env.advance_time(5); + if (!injected && !saved.empty()) { + tox::test::Packet replay{}; + replay.to = saved_to; + replay.from = saved_from; + replay.data = saved; + env.simulation().net().send_packet(replay); + injected = true; + } + } + ASSERT_TRUE(injected) << "Should have captured + replayed initiator HS"; + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id, 10000)) + << "Replay during handshake must not break completion"; + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); + + std::vector message = {160, 'R', 'p', 'l', 'y'}; + EXPECT_TRUE(exchange_data(env, alice, alice_conn_id, bob, bob_conn_id, message)); +} + +class NoiseHandshakeTamperTest : public ::testing::TestWithParam { +protected: + SimulatedEnvironment env{12345}; +}; + +TEST_P(NoiseHandshakeTamperTest, ByteFlipRejectedAndRecovers) +{ + const std::size_t offset = GetParam(); + bool tampered = false; + + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + env.simulation().net().add_filter([&](tox::test::Packet &p) { + if (!tampered && p.data.size() > offset && p.data[0] == NET_PACKET_CRYPTO_NOISE_HS + && net_ntohs(p.to.port) == 33446) { + p.data[offset] ^= 0xFF; + tampered = true; + } + return true; + }); + + int alice_conn_id = alice.connect_to(bob); + ASSERT_NE(alice_conn_id, -1); + + int bob_conn_id = -1; + ASSERT_TRUE(wait_for_connection(env, alice, alice_conn_id, bob, bob_conn_id, 15000)) + << "Tampered handshake at offset " << offset << " should recover via retransmission"; + EXPECT_TRUE(tampered); + EXPECT_TRUE(alice.is_noise_enabled(alice_conn_id)); + EXPECT_TRUE(bob.is_noise_enabled(bob_conn_id)); +} + +INSTANTIATE_TEST_SUITE_P(EachRegion, NoiseHandshakeTamperTest, + ::testing::Values( + std::size_t{4}, // ephemeral region (offset 2-33) + std::size_t{50}, // encrypted static region (offset 34-81) + std::size_t{200} // encrypted payload region (offset 82+) + )); + +TEST_F(NetCryptoTest, NoiseInitiatorSurvivesGarbageInitiatorLengthPacket) +{ + NetCryptoNode alice(env, 33445, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + NetCryptoNode bob(env, 33446, CRYPTO_HANDSHAKE_MODE_NOISE_BOTH); + + // Attack only triggers when initiator_pk < peer_pk (cmp<0 branch in dispatcher). + const bool alice_lower = std::memcmp(alice.real_public_key(), bob.real_public_key(), + CRYPTO_PUBLIC_KEY_SIZE) + < 0; + NetCryptoNode &victim = alice_lower ? alice : bob; + NetCryptoNode &peer = alice_lower ? bob : alice; + const std::uint16_t victim_port = alice_lower ? 33445 : 33446; + + constexpr std::size_t kNoiseInitLen + = 1 + 1 + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_MAC_SIZE + + kCookieLength + kCookieLength + CRYPTO_MAC_SIZE; + + bool injected = false; + env.simulation().net().add_filter([&](tox::test::Packet &p) { + if (!injected && p.data.size() > 0 && p.data[0] == NET_PACKET_CRYPTO_NOISE_HS + && net_ntohs(p.from.port) == victim_port) { + tox::test::Packet attack{}; + attack.from = p.to; + attack.to = p.from; + attack.data.assign(kNoiseInitLen, 0xCC); + attack.data[0] = NET_PACKET_CRYPTO_NOISE_HS; + attack.data[1] = 0x01; + env.simulation().net().send_packet(attack); + injected = true; + } + return true; + }); + + int victim_conn_id = victim.connect_to(peer); + ASSERT_NE(victim_conn_id, -1); + + int peer_conn_id = -1; + EXPECT_TRUE(wait_for_connection(env, victim, victim_conn_id, peer, peer_conn_id, 10000)) + << "Initiator must not be bricked by one garbage initiator-length packet"; + EXPECT_TRUE(injected) << "Filter should have seen a NOISE_HS from victim"; +} + } // namespace diff --git a/toxcore/net_log.c b/toxcore/net_log.c index f68581d70d..5b9f6a131d 100644 --- a/toxcore/net_log.c +++ b/toxcore/net_log.c @@ -41,6 +41,9 @@ static const char *_Nonnull net_packet_type_name(Net_Packet_Type type) case NET_PACKET_CRYPTO_DATA: return "CRYPTO_DATA"; + case NET_PACKET_CRYPTO_NOISE_HS: + return "CRYPTO_NOISE_HS"; + case NET_PACKET_CRYPTO: return "CRYPTO"; diff --git a/toxcore/network.h b/toxcore/network.h index 1cd7adaa82..0e3110b81c 100644 --- a/toxcore/network.h +++ b/toxcore/network.h @@ -36,6 +36,7 @@ typedef enum Net_Packet_Type { NET_PACKET_COOKIE_RESPONSE = 0x19, /* Cookie response packet */ NET_PACKET_CRYPTO_HS = 0x1a, /* Crypto handshake packet */ NET_PACKET_CRYPTO_DATA = 0x1b, /* Crypto data packet */ + NET_PACKET_CRYPTO_NOISE_HS = 0x1c, /* Noise IK handshake packet */ NET_PACKET_CRYPTO = 0x20, /* Encrypted data packet ID. */ NET_PACKET_LAN_DISCOVERY = 0x21, /* LAN discovery packet ID. */ diff --git a/toxcore/noise.c b/toxcore/noise.c new file mode 100644 index 0000000000..28ced2ed74 --- /dev/null +++ b/toxcore/noise.c @@ -0,0 +1,290 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2026 The TokTok team. + */ + +#include "noise.h" + +#include + +#include + +#include "attributes.h" +#include "ccompat.h" +#include "crypto_core.h" + +/** BLAKE2b block size in bytes (128), used only by crypto_hmac_blake2b_512. */ +#define NOISE_BLAKE2B_BLOCK_SIZE 128 + +static_assert(CRYPTO_NOISE_BLAKE2B_HASH_SIZE == crypto_generichash_blake2b_BYTES_MAX, + "CRYPTO_NOISE_BLAKE2B_HASH_SIZE should be equal to crypto_generichash_blake2b_BYTES_MAX"); + +/** + * cf. Noise sections 4.3, 5.1 and 12.8: HMAC-BLAKE2b-512 + * HASH(input): BLAKE2b with digest length 64 + * HASHLEN = 64 + * BLOCKLEN = 128 + * Applies HMAC from RFC2104 (https://www.ietf.org/rfc/rfc2104.txt) using the HASH() (=BLAKE2b) function. + * This function is only called via `noise_hkdf()`. + * Necessary for Noise (cf. sections 4.3 and 12.8) to return 64 bytes (BLAKE2b HASHLEN). + * Cf. https://doc.libsodium.org/hashing/generic_hashing + * key is CRYPTO_NOISE_BLAKE2B_HASH_SIZE bytes because this function is only called via noise_hkdf() where the key (ck, temp_key) + * is always HASHLEN bytes. + */ +static void crypto_hmac_blake2b_512(uint8_t *out_hmac, const uint8_t *in, size_t in_length, const uint8_t *key, + size_t key_length) +{ + crypto_generichash_blake2b_state state; + + /* + * (1) append zeros to the end of K to create a B byte string (e.g., if K is of length 20 bytes and B=64, + * then K will be appended with 44 zero bytes 0x00) + * B = Blake2b block length = 128 + * L the byte-length of Blake2b hash output = 64 + */ + uint8_t x_key[NOISE_BLAKE2B_BLOCK_SIZE] = { 0 }; + uint8_t i_hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + + /* + * The authentication key K can be of any length up to B, the + * block length of the hash function. Applications that use keys longer + * than B bytes will first hash the key using H and then use the + * resultant L byte string as the actual key to HMAC. + * In any case the minimal recommended length for K is L bytes (as the hash output + * length). + */ + if (key_length > NOISE_BLAKE2B_BLOCK_SIZE) { + crypto_generichash_blake2b_init(&state, nullptr, 0, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + crypto_generichash_blake2b_update(&state, key, key_length); + crypto_generichash_blake2b_final(&state, x_key, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + } else { + memcpy(x_key, key, key_length); + } + + /* + * K XOR ipad, ipad = the byte 0x36 repeated B times + * (2) XOR (bitwise exclusive-OR) the B byte string computed in step + * (1) with ipad + */ + for (int i = 0; i < NOISE_BLAKE2B_BLOCK_SIZE; ++i) { + x_key[i] ^= 0x36; + } + + /* + * H(K XOR ipad, text) + * (3) append the stream of data `text` to the B byte string resulting + * from step (2) + * (4) apply H to the stream generated in step (3) + */ + crypto_generichash_blake2b_init(&state, nullptr, 0, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + crypto_generichash_blake2b_update(&state, x_key, NOISE_BLAKE2B_BLOCK_SIZE); + crypto_generichash_blake2b_update(&state, in, in_length); + crypto_generichash_blake2b_final(&state, i_hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + + /* + * K XOR opad, opad = the byte 0x5C repeated B times + * (5) XOR (bitwise exclusive-OR) the B byte string computed in + * step (1) with opad + */ + for (int i = 0; i < NOISE_BLAKE2B_BLOCK_SIZE; ++i) { + x_key[i] ^= 0x5c ^ 0x36; + } + + /* + * H(K XOR opad, H(K XOR ipad, text)) + * (6) append the H result from step (4) to the B byte string + * resulting from step (5) + * (7) apply H to the stream generated in step (6) and output + * the result + */ + crypto_generichash_blake2b_init(&state, nullptr, 0, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + crypto_generichash_blake2b_update(&state, x_key, NOISE_BLAKE2B_BLOCK_SIZE); + crypto_generichash_blake2b_update(&state, i_hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + crypto_generichash_blake2b_final(&state, i_hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + + memcpy(out_hmac, i_hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + + /* Clear sensitive data from stack */ + crypto_memzero(x_key, NOISE_BLAKE2B_BLOCK_SIZE); + crypto_memzero(i_hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); +} + +bool noise_hkdf(uint8_t *output1, size_t first_len, uint8_t *output2, + size_t second_len, const uint8_t *data, + size_t data_len, const uint8_t chaining_key[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]) +{ + if (output1 == nullptr || first_len == 0 || first_len > CRYPTO_NOISE_BLAKE2B_HASH_SIZE + || output2 == nullptr || second_len == 0 || second_len > CRYPTO_NOISE_BLAKE2B_HASH_SIZE + || (data_len > 0 && data == nullptr) || chaining_key == nullptr) { + return false; + } + uint8_t output[CRYPTO_NOISE_BLAKE2B_HASH_SIZE + 1]; + uint8_t temp_key[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + + /* HKDF-Extract: temp_key = HMAC-HASH(chaining_key, data) cf. RFC5869 section 2.2 */ + crypto_hmac_blake2b_512(temp_key, data, data_len, chaining_key, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + + /* HKDF-Expand T(1): output1 = HMAC-HASH(temp_key, 0x01) cf. RFC5869 section 2.3 */ + output[0] = 1; + crypto_hmac_blake2b_512(output, output, 1, temp_key, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + memcpy(output1, output, first_len); + + /* HKDF-Expand T(2): output2 = HMAC-HASH(temp_key, T(1) || 0x02); OKM = T(1) || T(2) cf. RFC5869 section 2.3 */ + output[CRYPTO_NOISE_BLAKE2B_HASH_SIZE] = 2; + crypto_hmac_blake2b_512(output, output, CRYPTO_NOISE_BLAKE2B_HASH_SIZE + 1, temp_key, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + memcpy(output2, output, second_len); + + /* output3 (for pre-shared symmetric keys) is not needed and therefore not computed. */ + + /* Clear sensitive data from stack */ + crypto_memzero(temp_key, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + crypto_memzero(output, CRYPTO_NOISE_BLAKE2B_HASH_SIZE + 1); + return true; +} + +/* + * cf. Noise section 5.2: based on HKDF-BLAKE2b + * Executes the following steps: + * - Sets ck, temp_k = HKDF(ck, input_key_material, 2). + * - If HASHLEN is 64, then truncates temp_k to 32 bytes + * - Calls InitializeKey(temp_k). + * input_key_material = DH_X25519(private, public) + * + */ +int32_t noise_mix_key(uint8_t chaining_key[CRYPTO_NOISE_BLAKE2B_HASH_SIZE], + uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE], + const uint8_t private_key[CRYPTO_SECRET_KEY_SIZE], + const uint8_t public_key[CRYPTO_PUBLIC_KEY_SIZE]) +{ + uint8_t dh_calculation[CRYPTO_SHARED_KEY_SIZE]; + memset(dh_calculation, 0, CRYPTO_SHARED_KEY_SIZE); + + /* X25519: returns plain DH result, afterwards hashed with HKDF (necessary for NoiseIK). + * Also reject all-zero DH output (low-order input point), cf. Noise spec section 12.1. */ + if (crypto_scalarmult_curve25519(dh_calculation, private_key, public_key) != 0) { + crypto_memzero(dh_calculation, CRYPTO_SHARED_KEY_SIZE); + return -1; + } + + if (sodium_is_zero(dh_calculation, CRYPTO_SHARED_KEY_SIZE) != 0) { + crypto_memzero(dh_calculation, CRYPTO_SHARED_KEY_SIZE); + return -1; + } + + /* chaining_key is HKDF output1 and shared_key is HKDF output2 => different values/results! */ + /* If HASHLEN is 64, then truncates temp_k (= shared_key) to 32 bytes. => done via call to noise_hkdf() */ + noise_hkdf(chaining_key, CRYPTO_NOISE_BLAKE2B_HASH_SIZE, shared_key, CRYPTO_SHARED_KEY_SIZE, dh_calculation, + CRYPTO_SHARED_KEY_SIZE, chaining_key); + + crypto_memzero(dh_calculation, CRYPTO_SHARED_KEY_SIZE); + + return 0; +} + +/* + * Noise MixHash(data): Sets h = HASH(h || data). + * Blake2b + * + * cf. Noise section 5.2 + */ +void noise_mix_hash(uint8_t hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE], const uint8_t *data, size_t data_len) +{ + crypto_generichash_blake2b_state state; + crypto_generichash_blake2b_init(&state, nullptr, 0, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + crypto_generichash_blake2b_update(&state, hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + crypto_generichash_blake2b_update(&state, data, data_len); + crypto_generichash_blake2b_final(&state, hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); +} + +/* + * cf. Noise section 5.2. Unlike the spec, k is never empty in Tox. + */ +int noise_encrypt_and_hash(uint8_t *ciphertext, const uint8_t *plaintext, + size_t plain_length, uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE], + uint8_t hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]) +{ + uint8_t nonce_chacha20_ietf[CRYPTO_NOISE_NONCE_SIZE] = {0}; + + const int32_t encrypted_length = encrypt_data_symmetric_aead(shared_key, nonce_chacha20_ietf, + plaintext, plain_length, ciphertext, + hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + + if (encrypted_length == -1) { + return -1; + } + + noise_mix_hash(hash, ciphertext, encrypted_length); + return 0; +} + +/* + * cf. Noise section 5.2. Unlike the spec, k is never empty in Tox. + */ +int noise_decrypt_and_hash(uint8_t *plaintext, const uint8_t *ciphertext, + size_t encrypted_length, uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE], + uint8_t hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]) +{ + uint8_t nonce_chacha20_ietf[CRYPTO_NOISE_NONCE_SIZE] = {0}; + + const int32_t plaintext_length = decrypt_data_symmetric_aead(shared_key, nonce_chacha20_ietf, + ciphertext, encrypted_length, plaintext, + hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + + if (plaintext_length == -1) { + return -1; + } + + noise_mix_hash(hash, ciphertext, encrypted_length); + + return plaintext_length; +} + +int noise_handshake_init(Noise_Handshake *noise_handshake, const uint8_t self_id_public_key[CRYPTO_PUBLIC_KEY_SIZE], + const uint8_t peer_id_public_key[CRYPTO_PUBLIC_KEY_SIZE], bool initiator, + const uint8_t *prologue, size_t prologue_length) +{ + /* Zero the entire struct to prevent use of uninitialized key material + * (ephemeral keys and remote_ephemeral are set later by callers). */ + crypto_memzero(noise_handshake, sizeof(Noise_Handshake)); + + if (self_id_public_key == nullptr) { + return -1; + } + + /* The protocol name is exactly 33 ASCII bytes. The NUL terminator occupies byte 34, + * which becomes a zero-pad byte when copied into the 64-byte hash/ck initial state. + * This matches the noise-c test vectors. */ + const uint8_t noise_protocol[34] = "Noise_IK_25519_ChaChaPoly_BLAKE2b"; + + /* IntializeSymmetric(protocol_name) => set h to NOISE_PROTOCOL_NAME and append zero bytes to make 64 bytes, sets ck = h + * Nothing gets hashed in Tox case because NOISE_PROTOCOL_NAME < HASHLEN */ + uint8_t temp_hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + memset(temp_hash, 0, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + memcpy(temp_hash, noise_protocol, sizeof(noise_protocol)); + memcpy(noise_handshake->hash, temp_hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + memcpy(noise_handshake->chaining_key, temp_hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE); + + /* IMPORTANT needs to be called with (empty/zero-length) prologue! */ + noise_mix_hash(noise_handshake->hash, prologue, prologue_length); + + noise_handshake->initiator = initiator; + + /* <- s: pre-message from responder to initiator => sets rs (only initiator) */ + if (initiator) { + if (peer_id_public_key != nullptr) { + memcpy(noise_handshake->remote_static, peer_id_public_key, CRYPTO_PUBLIC_KEY_SIZE); + + /* Calls MixHash() once for each public key listed in the pre-messages from Noise IK */ + noise_mix_hash(noise_handshake->hash, peer_id_public_key, CRYPTO_PUBLIC_KEY_SIZE); + } else { + return -1; + } + } else { + /* Noise RESPONDER */ + /* Calls MixHash() once for each public key listed in the pre-messages from Noise IK */ + noise_mix_hash(noise_handshake->hash, self_id_public_key, CRYPTO_PUBLIC_KEY_SIZE); + // TODO(goldroom): precompute DH(s, rs) here? cf. WireGuard wg_noise_handshake_init() + } + + /* Ready to go */ + return 0; +} diff --git a/toxcore/noise.h b/toxcore/noise.h new file mode 100644 index 0000000000..1d8111d8df --- /dev/null +++ b/toxcore/noise.h @@ -0,0 +1,168 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later + * Copyright © 2026 The TokTok team. + */ + +/** @file + * @brief Noise IK protocol primitives. + * + * Implements the symmetric-state and handshake-state operations from the + * Noise protocol framework (https://noiseprotocol.org/noise.html, rev. 34), + * instantiated as Noise_IK_25519_ChaChaPoly_BLAKE2b. + */ +#ifndef C_TOXCORE_TOXCORE_NOISE_H +#define C_TOXCORE_TOXCORE_NOISE_H + +#include +#include +#include + +#include "attributes.h" +#include "crypto_core.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The number of bytes in a BLAKE2b-512 hash (as defined for Noise in section 12.8). + */ +#define CRYPTO_NOISE_BLAKE2B_HASH_SIZE 64 + +/** @brief NoiseIK handshake state. */ +typedef struct Noise_Handshake { + uint8_t ephemeral_private[CRYPTO_SECRET_KEY_SIZE]; + uint8_t ephemeral_public[CRYPTO_PUBLIC_KEY_SIZE]; + uint8_t remote_static[CRYPTO_PUBLIC_KEY_SIZE]; + uint8_t remote_ephemeral[CRYPTO_PUBLIC_KEY_SIZE]; + // TODO(goldroom): precompute DH(s, rs) to avoid recomputing during handshake? cf. WireGuard + + uint8_t hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + uint8_t chaining_key[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + + bool initiator; +} Noise_Handshake; + +/** + * @brief Computes two HKDF-BLAKE2b-512 outputs (chaining key and derived key). + * + * cf. Noise sections 4.3 and 5.1. This is Hugo Krawczyk's HKDF + * (https://eprint.iacr.org/2010/264.pdf, https://tools.ietf.org/html/rfc5869) + * with BLAKE2b as the hash function (HASHLEN=64, BLOCKLEN=128). + * + * Sets temp_key = HMAC-HASH(chaining_key, data), then: + * – output1 = HMAC-HASH(temp_key, byte(0x01)) (truncated to first_len) + * – output2 = HMAC-HASH(temp_key, output1 || byte(0x02)) (truncated to second_len) + * + * Verified using Noise_IK_25519_ChaChaPoly_BLAKE2b test vectors. + * + * @param output1 Buffer of length first_len to receive the first output key. + * @param first_len Length of output1 in bytes (must be > 0 and <= @ref CRYPTO_NOISE_BLAKE2B_HASH_SIZE). + * @param output2 Buffer of length second_len to receive the second output key. + * @param second_len Length of output2 in bytes (must be > 0 and <= @ref CRYPTO_NOISE_BLAKE2B_HASH_SIZE). + * @param data HKDF input_key_material (may be null if data_len == 0). + * @param data_len Length of data (zero, 32, or DHLEN bytes per Noise spec). + * @param chaining_key 64-byte Noise chaining key used as HKDF salt. + * @return false on invalid arguments, true on success. + */ +bool noise_hkdf(uint8_t *_Nonnull output1, size_t first_len, uint8_t *_Nonnull output2, + size_t second_len, const uint8_t *_Nullable data, + size_t data_len, const uint8_t chaining_key[_Nonnull CRYPTO_NOISE_BLAKE2B_HASH_SIZE]); + +/** + * @brief Noise MixKey(input_key_material) + * + * cf. Noise section 5.2 + * Executes the following steps: + * - Sets ck, temp_k = HKDF(ck, input_key_material, 2). + * - If HASHLEN is 64, then truncates temp_k to 32 bytes + * - Calls InitializeKey(temp_k). + * input_key_material = DH_X25519(private, public) + * + * @param chaining_key 64 byte Noise ck + * @param shared_key 32 byte secret key to be calculated + * @param private_key X25519 private key + * @param public_key X25519 public key + */ +int32_t noise_mix_key(uint8_t chaining_key[_Nonnull CRYPTO_NOISE_BLAKE2B_HASH_SIZE], uint8_t shared_key[_Nonnull CRYPTO_SHARED_KEY_SIZE], + const uint8_t private_key[_Nonnull CRYPTO_SECRET_KEY_SIZE], + const uint8_t public_key[_Nonnull CRYPTO_PUBLIC_KEY_SIZE]); + +/** + * @brief Noise MixHash(data): Sets h = HASH(h || data). + * + * cf. Noise section 5.2 + * + * @param hash Contains current hash, is updated with new hash + * @param data to add to hash + * @param data_len length of data to hash + */ +void noise_mix_hash(uint8_t hash[_Nonnull CRYPTO_NOISE_BLAKE2B_HASH_SIZE], const uint8_t *_Nullable data, size_t data_len); + +/** + * @brief Noise EncryptAndHash(plaintext): Sets ciphertext = EncryptWithAd(h, + * plaintext), then calls MixHash(ciphertext). + * + * cf. Noise section 5.2. Unlike the spec, k is never empty in Tox. + * + * @param ciphertext stores encrypted plaintext + * @param plaintext to be encrypted + * @param plain_length length of plaintext + * @param shared_key used for AEAD encryption + * @param hash stores hash value, used as associated data in AEAD + * + * @retval -1 on encryption failure. + * @retval 0 on success. + */ +int noise_encrypt_and_hash(uint8_t *_Nonnull ciphertext, const uint8_t *_Nonnull plaintext, + size_t plain_length, uint8_t shared_key[_Nonnull CRYPTO_SHARED_KEY_SIZE], + uint8_t hash[_Nonnull CRYPTO_NOISE_BLAKE2B_HASH_SIZE]); + +/** + * @brief DecryptAndHash(ciphertext): Sets plaintext = DecryptWithAd(h, + * ciphertext), then calls MixHash(ciphertext). + * + * cf. Noise section 5.2. Unlike the spec, k is never empty in Tox. + * + * @param ciphertext contains ciphertext to decrypt + * @param plaintext stores decrypted ciphertext + * @param encrypted_length length of ciphertext+MAC + * @param shared_key used for AEAD decryption + * @param hash stores hash value, used as associated data in AEAD + */ +int noise_decrypt_and_hash(uint8_t *_Nonnull plaintext, const uint8_t *_Nonnull ciphertext, + size_t encrypted_length, uint8_t shared_key[_Nonnull CRYPTO_SHARED_KEY_SIZE], + uint8_t hash[_Nonnull CRYPTO_NOISE_BLAKE2B_HASH_SIZE]); + +/** + * @brief Initializes a Noise Handshake State. + * + * The long-term identity keys are NOT stored in the handshake struct; they are + * only used here for the pre-message MixHash and must be passed separately to + * create_crypto_handshake / handle_crypto_handshake. + * + * cf. Noise section 5.3 + * Calls InitializeSymmetric(protocol_name). + * Calls MixHash(prologue). + * Sets the initiator, e, rs, and re variables to the corresponding arguments. + * Calls MixHash() once for each public key listed in the pre-messages. + * + * @param noise_handshake Noise handshake struct to save the necessary values to + * @param self_id_public_key static public ID X25519 key of this Tox instance + * @param peer_id_public_key static public ID X25519 key from the peer to connect to (initiator only) + * @param initiator specifies if this Tox instance is the initiator of this crypto connection + * @param prologue specifies the Noise prologue, used in call to MixHash(prologue) which maybe zero-length + * @param prologue_length length of Noise prologue in bytes + * + * @return -1 on failure + * @return 0 on success + */ +int noise_handshake_init(Noise_Handshake *_Nonnull noise_handshake, + const uint8_t self_id_public_key[_Nonnull CRYPTO_PUBLIC_KEY_SIZE], + const uint8_t peer_id_public_key[_Nullable CRYPTO_PUBLIC_KEY_SIZE], bool initiator, + const uint8_t *_Nullable prologue, size_t prologue_length); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* C_TOXCORE_TOXCORE_NOISE_H */ diff --git a/toxcore/noise_test.cc b/toxcore/noise_test.cc new file mode 100644 index 0000000000..a286217669 --- /dev/null +++ b/toxcore/noise_test.cc @@ -0,0 +1,484 @@ +// clang-format off +#include "../testing/support/public/simulated_environment.hh" +#include "noise.h" +// clang-format on + +#include + +#include +#include +#include +#include + +#include "crypto_core.h" +#include "crypto_core_test_util.hh" + +namespace { + +using SecretKey = std::array; + +using tox::test::SimulatedEnvironment; + +TEST(Noise, HKDF) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + uint8_t chaining_key[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + random_bytes(&c_rng, chaining_key, sizeof(chaining_key)); + + uint8_t data[32]; + random_bytes(&c_rng, data, sizeof(data)); + + uint8_t out1[32], out2[32]; + EXPECT_TRUE( + noise_hkdf(out1, sizeof(out1), out2, sizeof(out2), data, sizeof(data), chaining_key)); + + uint8_t zeros[32] = {0}; + EXPECT_NE(memcmp(out1, zeros, sizeof(out1)), 0); + EXPECT_NE(memcmp(out2, zeros, sizeof(out2)), 0); + // Outputs should be different (usually) + EXPECT_NE(memcmp(out1, out2, sizeof(out1)), 0); +} + +TEST(Noise, MixKey) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + PublicKey pk; + SecretKey sk; + crypto_new_keypair(&c_rng, pk.data(), sk.data()); + + uint8_t chaining_key[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + random_bytes(&c_rng, chaining_key, sizeof(chaining_key)); + + uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; + + EXPECT_EQ(noise_mix_key(chaining_key, shared_key, sk.data(), pk.data()), 0); + + uint8_t zeros[CRYPTO_SHARED_KEY_SIZE] = {0}; + EXPECT_NE(memcmp(shared_key, zeros, CRYPTO_SHARED_KEY_SIZE), 0); +} + +TEST(Noise, MixHash) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + uint8_t hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + random_bytes(&c_rng, hash, sizeof(hash)); + + uint8_t data[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + uint8_t original_hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + memcpy(original_hash, hash, sizeof(hash)); + + noise_mix_hash(hash, data, sizeof(data)); + + EXPECT_NE(memcmp(hash, original_hash, sizeof(hash)), 0); +} + +TEST(Noise, MixHashNullData) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + uint8_t hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + random_bytes(&c_rng, hash, sizeof(hash)); + + uint8_t original_hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + memcpy(original_hash, hash, sizeof(hash)); + + // Passing nullptr with length 0 should not crash + noise_mix_hash(hash, nullptr, 0); + + // The hash state should still be updated (hash(hash || empty)) + EXPECT_NE(memcmp(hash, original_hash, sizeof(hash)), 0); +} + +TEST(Noise, EncryptDecryptHash) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; + new_symmetric_key(&c_rng, shared_key); + + uint8_t hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + random_bytes(&c_rng, hash, sizeof(hash)); + uint8_t decrypt_hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + memcpy(decrypt_hash, hash, sizeof(hash)); + + const std::string plaintext = "Noise Message"; + std::vector ciphertext(plaintext.size() + CRYPTO_MAC_SIZE); + std::vector decrypted(plaintext.size()); + + noise_encrypt_and_hash(ciphertext.data(), reinterpret_cast(plaintext.data()), + plaintext.size(), shared_key, hash); + + int len = noise_decrypt_and_hash( + decrypted.data(), ciphertext.data(), ciphertext.size(), shared_key, decrypt_hash); + + EXPECT_EQ(len, static_cast(plaintext.size())); + EXPECT_EQ(memcmp(plaintext.data(), decrypted.data(), plaintext.size()), 0); + // Hashes should be updated identically + EXPECT_EQ(memcmp(hash, decrypt_hash, sizeof(hash)), 0); +} + +TEST(Noise, HandshakeInit) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + Noise_Handshake handshake; + + PublicKey self_pk, peer_pk; + SecretKey self_sk, peer_sk; + crypto_new_keypair(&c_rng, self_pk.data(), self_sk.data()); + crypto_new_keypair(&c_rng, peer_pk.data(), peer_sk.data()); + + const uint8_t prologue[] = "Prologue"; + + // Initiator + EXPECT_EQ(noise_handshake_init( + &handshake, self_pk.data(), peer_pk.data(), true, prologue, sizeof(prologue)), + 0); + EXPECT_TRUE(handshake.initiator); + EXPECT_EQ(memcmp(handshake.remote_static, peer_pk.data(), CRYPTO_PUBLIC_KEY_SIZE), 0); + + // Responder + EXPECT_EQ(noise_handshake_init( + &handshake, peer_pk.data(), nullptr, false, prologue, sizeof(prologue)), + 0); + EXPECT_FALSE(handshake.initiator); +} + +// clang-format off +// Noise_IK_25519_ChaChaPoly_BLAKE2b test vectors from: +// https://github.com/rweather/noise-c/blob/cfe25410979a87391bb9ac8d4d4bef64e9f268c6/tests/vector/noise-c-basic.txt + +// "init_prologue": "50726f6c6f677565313233" (same as resp_prologue) +static const uint8_t tv_prologue[11] = { + 0x50, 0x72, 0x6f, 0x6c, 0x6f, 0x67, 0x75, 0x65, + 0x31, 0x32, 0x33, +}; + +// "init_static": "e61ef9919cde45dd5f82166404bd08e38bceb5dfdfded0a34c8df7ed542214d1" +static const uint8_t tv_init_static[CRYPTO_SECRET_KEY_SIZE] = { + 0xe6, 0x1e, 0xf9, 0x91, 0x9c, 0xde, 0x45, 0xdd, + 0x5f, 0x82, 0x16, 0x64, 0x04, 0xbd, 0x08, 0xe3, + 0x8b, 0xce, 0xb5, 0xdf, 0xdf, 0xde, 0xd0, 0xa3, + 0x4c, 0x8d, 0xf7, 0xed, 0x54, 0x22, 0x14, 0xd1, +}; + +// "init_ephemeral": "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a" +static const uint8_t tv_init_ephemeral[CRYPTO_SECRET_KEY_SIZE] = { + 0x89, 0x3e, 0x28, 0xb9, 0xdc, 0x6c, 0xa8, 0xd6, + 0x11, 0xab, 0x66, 0x47, 0x54, 0xb8, 0xce, 0xb7, + 0xba, 0xc5, 0x11, 0x73, 0x49, 0xa4, 0x43, 0x9a, + 0x6b, 0x05, 0x69, 0xda, 0x97, 0x7c, 0x46, 0x4a, +}; + +// "init_remote_static": "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62" +static const uint8_t tv_init_remote_static[CRYPTO_PUBLIC_KEY_SIZE] = { + 0x31, 0xe0, 0x30, 0x3f, 0xd6, 0x41, 0x8d, 0x2f, + 0x8c, 0x0e, 0x78, 0xb9, 0x1f, 0x22, 0xe8, 0xca, + 0xed, 0x0f, 0xbe, 0x48, 0x65, 0x6d, 0xcf, 0x47, + 0x67, 0xe4, 0x83, 0x4f, 0x70, 0x1b, 0x8f, 0x62, +}; + +// "resp_static": "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893" +static const uint8_t tv_resp_static[CRYPTO_SECRET_KEY_SIZE] = { + 0x4a, 0x3a, 0xcb, 0xfd, 0xb1, 0x63, 0xde, 0xc6, + 0x51, 0xdf, 0xa3, 0x19, 0x4d, 0xec, 0xe6, 0x76, + 0xd4, 0x37, 0x02, 0x9c, 0x62, 0xa4, 0x08, 0xb4, + 0xc5, 0xea, 0x91, 0x14, 0x24, 0x6e, 0x48, 0x93, +}; + +// "resp_ephemeral": "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b" +static const uint8_t tv_resp_ephemeral[CRYPTO_SECRET_KEY_SIZE] = { + 0xbb, 0xdb, 0x4c, 0xdb, 0xd3, 0x09, 0xf1, 0xa1, + 0xf2, 0xe1, 0x45, 0x69, 0x67, 0xfe, 0x28, 0x8c, + 0xad, 0xd6, 0xf7, 0x12, 0xd6, 0x5d, 0xc7, 0xb7, + 0x79, 0x3d, 0x5e, 0x63, 0xda, 0x6b, 0x37, 0x5b, +}; + +// Initiator ephemeral public: "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c7944" +static const uint8_t tv_init_ephemeral_public[CRYPTO_PUBLIC_KEY_SIZE] = { + 0xca, 0x35, 0xde, 0xf5, 0xae, 0x56, 0xce, 0xc3, + 0x3d, 0xc2, 0x03, 0x67, 0x31, 0xab, 0x14, 0x89, + 0x6b, 0xc4, 0xc7, 0x5d, 0xbb, 0x07, 0xa6, 0x1f, + 0x87, 0x9f, 0x8e, 0x3a, 0xfa, 0x4c, 0x79, 0x44, +}; + +// Encrypted initiator static public: "ba83a447b38c83e327ad936929812f624884847b7831e95e197b2f797088efdd232fe541af156ec6d0657602902a8c3e" +static const uint8_t tv_init_encrypted_static_public[CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_MAC_SIZE] = { + 0xba, 0x83, 0xa4, 0x47, 0xb3, 0x8c, 0x83, 0xe3, + 0x27, 0xad, 0x93, 0x69, 0x29, 0x81, 0x2f, 0x62, + 0x48, 0x84, 0x84, 0x7b, 0x78, 0x31, 0xe9, 0x5e, + 0x19, 0x7b, 0x2f, 0x79, 0x70, 0x88, 0xef, 0xdd, + 0x23, 0x2f, 0xe5, 0x41, 0xaf, 0x15, 0x6e, 0xc6, + 0xd0, 0x65, 0x76, 0x02, 0x90, 0x2a, 0x8c, 0x3e, +}; + +// "payload": "4c756477696720766f6e204d69736573" +static const uint8_t tv_init_payload_hs[16] = { + 0x4c, 0x75, 0x64, 0x77, 0x69, 0x67, 0x20, 0x76, + 0x6f, 0x6e, 0x20, 0x4d, 0x69, 0x73, 0x65, 0x73, +}; + +// Encrypted initiator payload: "e64e470f4b6fcd9298ce0b56fe20f86e60d9d933ec6e103ffb09e6001d6abb64" +static const uint8_t tv_init_payload_hs_encrypted[sizeof(tv_init_payload_hs) + CRYPTO_MAC_SIZE] = { + 0xe6, 0x4e, 0x47, 0x0f, 0x4b, 0x6f, 0xcd, 0x92, + 0x98, 0xce, 0x0b, 0x56, 0xfe, 0x20, 0xf8, 0x6e, + 0x60, 0xd9, 0xd9, 0x33, 0xec, 0x6e, 0x10, 0x3f, + 0xfb, 0x09, 0xe6, 0x00, 0x1d, 0x6a, 0xbb, 0x64, +}; + +// "payload": "4d757272617920526f746862617264" +static const uint8_t tv_resp_payload_hs[15] = { + 0x4d, 0x75, 0x72, 0x72, 0x61, 0x79, 0x20, 0x52, + 0x6f, 0x74, 0x68, 0x62, 0x61, 0x72, 0x64, +}; + +// Responder ephemeral public: "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843" +static const uint8_t tv_resp_ephemeral_public[CRYPTO_PUBLIC_KEY_SIZE] = { + 0x95, 0xeb, 0xc6, 0x0d, 0x2b, 0x1f, 0xa6, 0x72, + 0xc1, 0xf4, 0x6a, 0x8a, 0xa2, 0x65, 0xef, 0x51, + 0xbf, 0xe3, 0x8e, 0x7c, 0xcb, 0x39, 0xec, 0x5b, + 0xe3, 0x40, 0x69, 0xf1, 0x44, 0x80, 0x88, 0x43, +}; + +// Encrypted responder payload: "9f069b267a06b3de3ecb1043bcb09807c6cd101f3826192a65f11ef3fe4317" +static const uint8_t tv_resp_payload_hs_encrypted[sizeof(tv_resp_payload_hs) + CRYPTO_MAC_SIZE] = { + 0x9f, 0x06, 0x9b, 0x26, 0x7a, 0x06, 0xb3, 0xde, + 0x3e, 0xcb, 0x10, 0x43, 0xbc, 0xb0, 0x98, 0x07, + 0xc6, 0xcd, 0x10, 0x1f, 0x38, 0x26, 0x19, 0x2a, + 0x65, 0xf1, 0x1e, 0xf3, 0xfe, 0x43, 0x17, +}; + +// "payload": "462e20412e20486179656b" +static const uint8_t tv_init_payload_transport1[11] = { + 0x46, 0x2e, 0x20, 0x41, 0x2e, 0x20, 0x48, 0x61, + 0x79, 0x65, 0x6b, +}; + +// "ciphertext": "cd54383060e7a28434cca27fb1cc524cfbabeb18181589df219d07" +static const uint8_t tv_init_payload_transport1_encrypted[sizeof(tv_init_payload_transport1) + CRYPTO_MAC_SIZE] = { + 0xcd, 0x54, 0x38, 0x30, 0x60, 0xe7, 0xa2, 0x84, + 0x34, 0xcc, 0xa2, 0x7f, 0xb1, 0xcc, 0x52, 0x4c, + 0xfb, 0xab, 0xeb, 0x18, 0x18, 0x15, 0x89, 0xdf, + 0x21, 0x9d, 0x07, +}; + +// "payload": "4361726c204d656e676572" +static const uint8_t tv_resp_payload_transport1[11] = { + 0x43, 0x61, 0x72, 0x6c, 0x20, 0x4d, 0x65, 0x6e, + 0x67, 0x65, 0x72, +}; + +// "ciphertext": "a856d3bf0246bfc476c655009cd1ed677b8dcc5b349ae8ef2a05f2" +static const uint8_t tv_resp_payload_transport1_encrypted[sizeof(tv_resp_payload_transport1) + CRYPTO_MAC_SIZE] = { + 0xa8, 0x56, 0xd3, 0xbf, 0x02, 0x46, 0xbf, 0xc4, + 0x76, 0xc6, 0x55, 0x00, 0x9c, 0xd1, 0xed, 0x67, + 0x7b, 0x8d, 0xcc, 0x5b, 0x34, 0x9a, 0xe8, 0xef, + 0x2a, 0x05, 0xf2, +}; + +// "handshake_hash": "00e51d2aac81a9b8ebe441d6af3e1c8efc0f030cc608332edcb42588ff6a0ce26415ddc106e95277a5e6d54132f1e5245976b89caf96d262f1fe5a7f0c55c078" +static const uint8_t tv_handshake_hash[CRYPTO_NOISE_BLAKE2B_HASH_SIZE] = { + 0x00, 0xe5, 0x1d, 0x2a, 0xac, 0x81, 0xa9, 0xb8, + 0xeb, 0xe4, 0x41, 0xd6, 0xaf, 0x3e, 0x1c, 0x8e, + 0xfc, 0x0f, 0x03, 0x0c, 0xc6, 0x08, 0x33, 0x2e, + 0xdc, 0xb4, 0x25, 0x88, 0xff, 0x6a, 0x0c, 0xe2, + 0x64, 0x15, 0xdd, 0xc1, 0x06, 0xe9, 0x52, 0x77, + 0xa5, 0xe6, 0xd5, 0x41, 0x32, 0xf1, 0xe5, 0x24, + 0x59, 0x76, 0xb8, 0x9c, 0xaf, 0x96, 0xd2, 0x62, + 0xf1, 0xfe, 0x5a, 0x7f, 0x0c, 0x55, 0xc0, 0x78, +}; +// clang-format on + +TEST(Noise, IKHandshakeTestVectors) +{ + // Derive public keys from the test vector secret keys + uint8_t init_static_pub[CRYPTO_PUBLIC_KEY_SIZE]; + crypto_derive_public_key(init_static_pub, tv_init_static); + + uint8_t resp_static_pub[CRYPTO_PUBLIC_KEY_SIZE]; + crypto_derive_public_key(resp_static_pub, tv_resp_static); + ASSERT_EQ(memcmp(resp_static_pub, tv_init_remote_static, CRYPTO_PUBLIC_KEY_SIZE), 0) + << "responder static public keys differ"; + + /* INITIATOR: Create handshake packet for responder */ + Noise_Handshake hs_init = {}; + noise_handshake_init( + &hs_init, init_static_pub, tv_init_remote_static, true, tv_prologue, sizeof(tv_prologue)); + + memcpy(hs_init.ephemeral_private, tv_init_ephemeral, CRYPTO_SECRET_KEY_SIZE); + crypto_derive_public_key(hs_init.ephemeral_public, tv_init_ephemeral); + ASSERT_EQ(memcmp(hs_init.ephemeral_public, tv_init_ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE), 0) + << "initiator ephemeral public keys differ"; + + /* e */ + noise_mix_hash(hs_init.hash, hs_init.ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE); + + /* es */ + uint8_t temp_key_init[CRYPTO_SHARED_KEY_SIZE]; + noise_mix_key( + hs_init.chaining_key, temp_key_init, hs_init.ephemeral_private, hs_init.remote_static); + + /* s */ + uint8_t ciphertext1[CRYPTO_PUBLIC_KEY_SIZE + CRYPTO_MAC_SIZE]; + noise_encrypt_and_hash( + ciphertext1, init_static_pub, CRYPTO_PUBLIC_KEY_SIZE, temp_key_init, hs_init.hash); + ASSERT_EQ(memcmp(ciphertext1, tv_init_encrypted_static_public, + sizeof(tv_init_encrypted_static_public)), + 0) + << "initiator encrypted static public keys differ"; + + /* ss */ + noise_mix_key(hs_init.chaining_key, temp_key_init, tv_init_static, hs_init.remote_static); + + /* Handshake payload */ + uint8_t ciphertext2[sizeof(tv_init_payload_hs) + CRYPTO_MAC_SIZE]; + noise_encrypt_and_hash( + ciphertext2, tv_init_payload_hs, sizeof(tv_init_payload_hs), temp_key_init, hs_init.hash); + ASSERT_EQ( + memcmp(ciphertext2, tv_init_payload_hs_encrypted, sizeof(tv_init_payload_hs_encrypted)), 0) + << "initiator encrypted handshake payloads differ"; + + /* RESPONDER: Consume handshake packet from initiator */ + Noise_Handshake hs_resp = {}; + noise_handshake_init( + &hs_resp, resp_static_pub, nullptr, false, tv_prologue, sizeof(tv_prologue)); + + /* e */ + memcpy(hs_resp.remote_ephemeral, hs_init.ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE); + noise_mix_hash(hs_resp.hash, hs_init.ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE); + + /* es */ + uint8_t temp_key_resp[CRYPTO_SHARED_KEY_SIZE]; + noise_mix_key(hs_resp.chaining_key, temp_key_resp, tv_resp_static, hs_resp.remote_ephemeral); + + /* s */ + noise_decrypt_and_hash( + hs_resp.remote_static, ciphertext1, sizeof(ciphertext1), temp_key_resp, hs_resp.hash); + + /* ss */ + noise_mix_key(hs_resp.chaining_key, temp_key_resp, tv_resp_static, hs_resp.remote_static); + + /* Payload decryption */ + uint8_t payload_plain_init[sizeof(tv_init_payload_hs)]; + noise_decrypt_and_hash( + payload_plain_init, ciphertext2, sizeof(ciphertext2), temp_key_resp, hs_resp.hash); + + /* RESPONDER: Create handshake packet for initiator */ + memcpy(hs_resp.ephemeral_private, tv_resp_ephemeral, CRYPTO_SECRET_KEY_SIZE); + crypto_derive_public_key(hs_resp.ephemeral_public, tv_resp_ephemeral); + ASSERT_EQ(memcmp(hs_resp.ephemeral_public, tv_resp_ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE), 0) + << "responder ephemeral public keys differ"; + + /* e */ + noise_mix_hash(hs_resp.hash, hs_resp.ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE); + + /* ee */ + uint8_t temp_key_resp2[CRYPTO_SHARED_KEY_SIZE]; + noise_mix_key( + hs_resp.chaining_key, temp_key_resp2, hs_resp.ephemeral_private, hs_resp.remote_ephemeral); + + /* se */ + noise_mix_key( + hs_resp.chaining_key, temp_key_resp2, hs_resp.ephemeral_private, hs_resp.remote_static); + + uint8_t ciphertext3[sizeof(tv_resp_payload_hs) + CRYPTO_MAC_SIZE]; + noise_encrypt_and_hash( + ciphertext3, tv_resp_payload_hs, sizeof(tv_resp_payload_hs), temp_key_resp2, hs_resp.hash); + ASSERT_EQ( + memcmp(ciphertext3, tv_resp_payload_hs_encrypted, sizeof(tv_resp_payload_hs_encrypted)), 0) + << "responder encrypted handshake payloads differ"; + + /* INITIATOR: Consume handshake packet from responder */ + memcpy(hs_init.remote_ephemeral, hs_resp.ephemeral_public, CRYPTO_PUBLIC_KEY_SIZE); + noise_mix_hash(hs_init.hash, hs_init.remote_ephemeral, CRYPTO_PUBLIC_KEY_SIZE); + + /* ee */ + uint8_t temp_key_init2[CRYPTO_SHARED_KEY_SIZE]; + noise_mix_key( + hs_init.chaining_key, temp_key_init2, hs_init.ephemeral_private, hs_init.remote_ephemeral); + + /* se */ + noise_mix_key(hs_init.chaining_key, temp_key_init2, tv_init_static, hs_init.remote_ephemeral); + + uint8_t payload_plain_resp[sizeof(tv_resp_payload_hs)]; + ASSERT_EQ(noise_decrypt_and_hash(payload_plain_resp, ciphertext3, sizeof(ciphertext3), + temp_key_init2, hs_init.hash), + static_cast(sizeof(tv_resp_payload_hs))) + << "initiator: HS decryption failed"; + + /* Split: derive transport keys */ + uint8_t initiator_send_key[CRYPTO_SHARED_KEY_SIZE]; + uint8_t initiator_recv_key[CRYPTO_SHARED_KEY_SIZE]; + ASSERT_TRUE(noise_hkdf(initiator_send_key, CRYPTO_SHARED_KEY_SIZE, initiator_recv_key, + CRYPTO_SHARED_KEY_SIZE, nullptr, 0, hs_init.chaining_key)); + + ASSERT_EQ(memcmp(hs_init.hash, tv_handshake_hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE), 0) + << "initiator handshake hash differs"; + + uint8_t nonce[CRYPTO_NOISE_NONCE_SIZE] = {0}; + uint8_t ciphertext4[sizeof(tv_init_payload_transport1) + CRYPTO_MAC_SIZE]; + encrypt_data_symmetric_aead(initiator_send_key, nonce, tv_init_payload_transport1, + sizeof(tv_init_payload_transport1), ciphertext4, nullptr, 0); + ASSERT_EQ(memcmp(ciphertext4, tv_init_payload_transport1_encrypted, + sizeof(tv_init_payload_transport1_encrypted)), + 0) + << "initiator transport1 ciphertext differs"; + + uint8_t responder_recv_key[CRYPTO_SHARED_KEY_SIZE]; + uint8_t responder_send_key[CRYPTO_SHARED_KEY_SIZE]; + ASSERT_TRUE(noise_hkdf(responder_recv_key, CRYPTO_SYMMETRIC_KEY_SIZE, responder_send_key, + CRYPTO_SYMMETRIC_KEY_SIZE, nullptr, 0, hs_resp.chaining_key)); + + ASSERT_EQ(memcmp(hs_resp.hash, tv_handshake_hash, CRYPTO_NOISE_BLAKE2B_HASH_SIZE), 0) + << "responder handshake hash differs"; + + uint8_t ciphertext5[sizeof(tv_resp_payload_transport1) + CRYPTO_MAC_SIZE]; + encrypt_data_symmetric_aead(responder_send_key, nonce, tv_resp_payload_transport1, + sizeof(tv_resp_payload_transport1), ciphertext5, nullptr, 0); + ASSERT_EQ(memcmp(ciphertext5, tv_resp_payload_transport1_encrypted, + sizeof(tv_resp_payload_transport1_encrypted)), + 0) + << "responder transport1 ciphertext differs"; + + EXPECT_NE(memcmp(initiator_send_key, initiator_recv_key, CRYPTO_SHARED_KEY_SIZE), 0) + << "Split must produce direction-asymmetric keys (zero-nonce safety invariant)"; + EXPECT_EQ(memcmp(initiator_send_key, responder_recv_key, CRYPTO_SHARED_KEY_SIZE), 0); + EXPECT_EQ(memcmp(initiator_recv_key, responder_send_key, CRYPTO_SHARED_KEY_SIZE), 0); +} + +TEST(Noise, MixKeyRejectsLowOrderPoint) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + PublicKey self_pk; + SecretKey self_sk; + crypto_new_keypair(&c_rng, self_pk.data(), self_sk.data()); + + uint8_t chaining_key[CRYPTO_NOISE_BLAKE2B_HASH_SIZE]; + random_bytes(&c_rng, chaining_key, sizeof(chaining_key)); + + uint8_t shared_key[CRYPTO_SHARED_KEY_SIZE]; + const uint8_t low_order_pk[CRYPTO_PUBLIC_KEY_SIZE] = {0}; + + EXPECT_EQ(noise_mix_key(chaining_key, shared_key, self_sk.data(), low_order_pk), -1); +} + +TEST(Noise, HandshakeInitInitiatorRequiresPeerKey) +{ + SimulatedEnvironment env{12345}; + auto c_rng = env.fake_random().c_random(); + Noise_Handshake handshake; + PublicKey self_pk; + SecretKey self_sk; + crypto_new_keypair(&c_rng, self_pk.data(), self_sk.data()); + const uint8_t prologue[] = "Prologue"; + + EXPECT_EQ(noise_handshake_init( + &handshake, self_pk.data(), nullptr, true, prologue, sizeof(prologue)), + -1); +} + +} // namespace diff --git a/toxcore/onion_client_fuzz_test.cc b/toxcore/onion_client_fuzz_test.cc index a229be2ab4..17fc672b78 100644 --- a/toxcore/onion_client_fuzz_test.cc +++ b/toxcore/onion_client_fuzz_test.cc @@ -98,7 +98,8 @@ class OnionClientFuzzer { TCP_Proxy_Info proxy_info = {{0}, TCP_PROXY_NONE}; net_crypto_.reset(new_net_crypto(dht_.logger(), &dht_.node().c_memory, &dht_.node().c_random, &dht_.node().c_network, dht_.mono_time(), dht_.networking(), - dht_.get_dht(), &FuzzDHT::funcs, &proxy_info, net_profile_.get())); + dht_.get_dht(), &FuzzDHT::funcs, &proxy_info, net_profile_.get(), + CRYPTO_HANDSHAKE_MODE_NOISE_BOTH)); onion_client_.reset( new_onion_client(dht_.logger(), &dht_.node().c_memory, &dht_.node().c_random, diff --git a/toxcore/onion_client_test.cc b/toxcore/onion_client_test.cc index eb0512124e..b73f365dbe 100644 --- a/toxcore/onion_client_test.cc +++ b/toxcore/onion_client_test.cc @@ -49,7 +49,7 @@ class OnionTestNode { net_crypto_.reset(new_net_crypto(dht_wrapper_.logger(), &dht_wrapper_.node().c_memory, &dht_wrapper_.node().c_random, &dht_wrapper_.node().c_network, dht_wrapper_.mono_time(), dht_wrapper_.networking(), dht_wrapper_.get_dht(), &DHTWrapper::funcs, &proxy_info, - net_profile_.get())); + net_profile_.get(), CRYPTO_HANDSHAKE_MODE_NOISE_BOTH)); // Setup Onion Client onion_client_.reset(new_onion_client(dht_wrapper_.logger(), &dht_wrapper_.node().c_memory, diff --git a/toxcore/tox.c b/toxcore/tox.c index 4a5ce14365..0cc7a934e1 100644 --- a/toxcore/tox.c +++ b/toxcore/tox.c @@ -715,6 +715,7 @@ static Tox *_Nullable tox_new_system(const struct Tox_Options *_Nullable options } m_options.ipv6enabled = tox_options_get_ipv6_enabled(opts); + m_options.handshake_mode = (Crypto_Handshake_Mode)tox_options_get_handshake_mode(opts); m_options.udp_disabled = !tox_options_get_udp_enabled(opts); m_options.port_range[0] = tox_options_get_start_port(opts); m_options.port_range[1] = tox_options_get_end_port(opts); diff --git a/toxcore/tox_api.c b/toxcore/tox_api.c index b738f5d132..930ac189e7 100644 --- a/toxcore/tox_api.c +++ b/toxcore/tox_api.c @@ -189,6 +189,21 @@ const char *_Nonnull tox_savedata_type_to_string(Tox_Savedata_Type value) return ""; } +const char *_Nonnull tox_handshake_mode_to_string(Tox_Handshake_Mode value) +{ + switch (value) { + case TOX_HANDSHAKE_MODE_NOISE_ONLY: + return "TOX_HANDSHAKE_MODE_NOISE_ONLY"; + + case TOX_HANDSHAKE_MODE_NOISE_AND_LEGACY: + return "TOX_HANDSHAKE_MODE_NOISE_AND_LEGACY"; + + case TOX_HANDSHAKE_MODE_LEGACY_ONLY: + return "TOX_HANDSHAKE_MODE_LEGACY_ONLY"; + } + + return ""; +} const char *_Nonnull tox_err_options_new_to_string(Tox_Err_Options_New value) { switch (value) { @@ -1569,6 +1584,8 @@ const char *tox_netprof_packet_id_to_string(Tox_Netprof_Packet_Id value) return "TOX_NETPROF_PACKET_ID_CRYPTO_HS"; case TOX_NETPROF_PACKET_ID_CRYPTO_DATA: return "TOX_NETPROF_PACKET_ID_CRYPTO_DATA"; + case TOX_NETPROF_PACKET_ID_CRYPTO_NOISE_HS: + return "TOX_NETPROF_PACKET_ID_CRYPTO_NOISE_HS"; case TOX_NETPROF_PACKET_ID_CRYPTO: return "TOX_NETPROF_PACKET_ID_CRYPTO"; case TOX_NETPROF_PACKET_ID_LAN_DISCOVERY: diff --git a/toxcore/tox_options.c b/toxcore/tox_options.c index 9a007c8327..dc296f57ef 100644 --- a/toxcore/tox_options.c +++ b/toxcore/tox_options.c @@ -181,6 +181,15 @@ void tox_options_set_experimental_groups_persistence( { options->experimental_groups_persistence = experimental_groups_persistence; } +Tox_Handshake_Mode tox_options_get_handshake_mode(const Tox_Options *_Nonnull options) +{ + return options->handshake_mode; +} +void tox_options_set_handshake_mode( + Tox_Options *_Nonnull options, Tox_Handshake_Mode handshake_mode) +{ + options->handshake_mode = handshake_mode; +} bool tox_options_get_experimental_disable_dns(const Tox_Options *_Nonnull options) { return options->experimental_disable_dns; @@ -255,6 +264,7 @@ void tox_options_default(Tox_Options *_Nonnull options) tox_options_set_dht_announcements_enabled(options, true); tox_options_set_experimental_thread_safety(options, false); tox_options_set_experimental_groups_persistence(options, false); + tox_options_set_handshake_mode(options, TOX_HANDSHAKE_MODE_NOISE_AND_LEGACY); tox_options_set_experimental_disable_dns(options, false); tox_options_set_experimental_owned_data(options, false); } diff --git a/toxcore/tox_options.h b/toxcore/tox_options.h index dbf070e0f2..4b9b8a2f12 100644 --- a/toxcore/tox_options.h +++ b/toxcore/tox_options.h @@ -66,6 +66,30 @@ typedef enum Tox_Savedata_Type { const char *tox_savedata_type_to_string(Tox_Savedata_Type value); +/** + * @brief Handshake mode for crypto connections. + */ +typedef enum Tox_Handshake_Mode { + /** + * Only use the Noise IK handshake. Connections with peers that + * do not support Noise will fail. + */ + TOX_HANDSHAKE_MODE_NOISE_ONLY, + + /** + * Prefer Noise IK, but fall back to the legacy handshake when the + * peer does not support Noise. This is the default. + */ + TOX_HANDSHAKE_MODE_NOISE_AND_LEGACY, + + /** + * Only use the legacy handshake. Noise is completely disabled. + */ + TOX_HANDSHAKE_MODE_LEGACY_ONLY, +} Tox_Handshake_Mode; + +const char *tox_handshake_mode_to_string(Tox_Handshake_Mode value); + /** * @brief This event is triggered when Tox logs an internal message. * @@ -266,6 +290,16 @@ struct Tox_Options { */ bool experimental_groups_persistence; + /** + * @brief Handshake mode for crypto connections. + * + * Controls whether Noise IK, legacy, or both handshake protocols + * are used when establishing encrypted connections with peers. + * + * Default: TOX_HANDSHAKE_MODE_NOISE_AND_LEGACY. + */ + Tox_Handshake_Mode handshake_mode; + /** * @brief Disable DNS hostname resolution. * @@ -394,6 +428,10 @@ bool tox_options_get_experimental_groups_persistence(const Tox_Options *options) void tox_options_set_experimental_groups_persistence( Tox_Options *options, bool experimental_groups_persistence); +Tox_Handshake_Mode tox_options_get_handshake_mode(const Tox_Options *options); + +void tox_options_set_handshake_mode(Tox_Options *options, Tox_Handshake_Mode handshake_mode); + bool tox_options_get_experimental_disable_dns(const Tox_Options *options); void tox_options_set_experimental_disable_dns(Tox_Options *options, bool experimental_disable_dns); diff --git a/toxcore/tox_private.h b/toxcore/tox_private.h index 625a07deaf..93ad0d3f5f 100644 --- a/toxcore/tox_private.h +++ b/toxcore/tox_private.h @@ -318,6 +318,11 @@ typedef enum Tox_Netprof_Packet_Id { */ TOX_NETPROF_PACKET_ID_CRYPTO_DATA = 0x1b, + /** + * Noise IK handshake packet. + */ + TOX_NETPROF_PACKET_ID_CRYPTO_NOISE_HS = 0x1c, + /** * Encrypted data packet. */