From 438394c14ac4c52b5f48836616ba5da2c9496439 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Tue, 12 May 2026 14:25:14 -0600 Subject: [PATCH 1/2] fix e2ee defaults masked in release builds float up missing e2ee fields e2ee: toProto --- include/livekit/e2ee.h | 19 +++++- src/ffi_client.cpp | 35 +---------- src/room_proto_converter.cpp | 24 ++++++- src/room_proto_converter.h | 4 ++ src/tests/unit/test_e2ee_options.cpp | 94 ++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 38 deletions(-) create mode 100644 src/tests/unit/test_e2ee_options.cpp diff --git a/include/livekit/e2ee.h b/include/livekit/e2ee.h index dfd55a60..fe087c38 100644 --- a/include/livekit/e2ee.h +++ b/include/livekit/e2ee.h @@ -34,10 +34,18 @@ enum class EncryptionType { CUSTOM = 2, }; -/* Defaults (match other SDKs / Python defaults). */ +/* Key derivation algorithm used by the key provider. */ +enum class KeyDerivationFunction { + PBKDF2 = 0, + HKDF = 1, +}; + +/* Defaults (match Rust KeyProviderOptions::default()). */ inline constexpr const char* kDefaultRatchetSalt = "LKFrameEncryptionKey"; inline constexpr int kDefaultRatchetWindowSize = 16; inline constexpr int kDefaultFailureTolerance = -1; +inline constexpr int kDefaultKeyRingSize = 16; +inline constexpr KeyDerivationFunction kDefaultKeyDerivationFunction = KeyDerivationFunction::PBKDF2; /** * Options for configuring the key provider used by E2EE. @@ -46,8 +54,7 @@ inline constexpr int kDefaultFailureTolerance = -1; * - `shared_key` is optional. If omitted, the application may set keys later * (e.g. via KeyProvider::setSharedKey / per-participant keys). * - `ratchet_salt` may be empty to indicate "use implementation default". - * - `ratchet_window_size` and `failure_tolerance` use SDK defaults unless - * overridden. + * - Other key provider fields use SDK defaults unless overridden. */ struct KeyProviderOptions { /// Shared static key for "shared-key E2EE" (optional). @@ -70,6 +77,12 @@ struct KeyProviderOptions { /// Number of tolerated ratchet failures before reporting encryption errors. int failure_tolerance = kDefaultFailureTolerance; + + /// Number of key slots retained by the key provider. + int key_ring_size = kDefaultKeyRingSize; + + /// Algorithm used when deriving ratcheted keys. + KeyDerivationFunction key_derivation_function = kDefaultKeyDerivationFunction; }; /** diff --git a/src/ffi_client.cpp b/src/ffi_client.cpp index 5e719dc6..f18c87aa 100644 --- a/src/ffi_client.cpp +++ b/src/ffi_client.cpp @@ -27,7 +27,6 @@ #include "livekit/ffi_handle.h" #include "livekit/room.h" #include "livekit/rpc_error.h" -#include "livekit/track.h" #include "livekit_ffi.h" #include "lk_log.h" #include "room.pb.h" @@ -37,10 +36,6 @@ namespace livekit { namespace { -std::string bytesToString(const std::vector& b) { - return std::string(reinterpret_cast(b.data()), b.size()); -} - inline void logAndThrow(const std::string& error_msg) { LK_LOG_ERROR("LiveKit SDK Error: {}", error_msg); throw std::runtime_error(error_msg); @@ -333,35 +328,7 @@ std::future FfiClient::connectAsync(const std::string& u auto* enc = opts->mutable_encryption(); enc->set_encryption_type(static_cast(e2ee.encryption_type)); - auto* kp = enc->mutable_key_provider_options(); - // shared_key is optional. If not set, leave the field unset/cleared. - if (kpo.shared_key && !kpo.shared_key->empty()) { - kp->set_shared_key(bytesToString(*kpo.shared_key)); - } else { - kp->clear_shared_key(); - } - // Only set ratchet_salt if caller overrides. Otherwise clear so Rust side - // uses default. - if (!kpo.ratchet_salt.empty() && - kpo.ratchet_salt != - std::vector(kDefaultRatchetSalt, - kDefaultRatchetSalt + std::char_traits::length(kDefaultRatchetSalt))) { - kp->set_ratchet_salt(bytesToString(kpo.ratchet_salt)); - } else { - kp->clear_ratchet_salt(); - } - // Same idea for window size / tolerance: set only on override; otherwise - // clear. - if (kpo.ratchet_window_size != kDefaultRatchetWindowSize) { - kp->set_ratchet_window_size(kpo.ratchet_window_size); - } else { - kp->clear_ratchet_window_size(); - } - if (kpo.failure_tolerance != kDefaultFailureTolerance) { - kp->set_failure_tolerance(kpo.failure_tolerance); - } else { - kp->clear_failure_tolerance(); - } + enc->mutable_key_provider_options()->CopyFrom(toProto(kpo)); } // --- RTC configuration (optional) --- diff --git a/src/room_proto_converter.cpp b/src/room_proto_converter.cpp index 048b855c..92918fa2 100644 --- a/src/room_proto_converter.cpp +++ b/src/room_proto_converter.cpp @@ -17,13 +17,16 @@ #include "room_proto_converter.h" #include "livekit/data_stream.h" -#include "livekit/local_participant.h" #include "room.pb.h" namespace livekit { namespace { +std::string bytesToString(const std::vector& bytes) { + return std::string(reinterpret_cast(bytes.data()), bytes.size()); +} + std::vector toProto(const PacketTrailerFeatures& features) { std::vector out; out.reserve(2); @@ -309,6 +312,25 @@ RoomMovedEvent roomMovedFromProto(const proto::RoomInfo& in) { // ---------------- Room Options ---------------- +proto::KeyProviderOptions toProto(const KeyProviderOptions& in) { + proto::KeyProviderOptions out; + if (in.shared_key && !in.shared_key->empty()) { + out.set_shared_key(bytesToString(*in.shared_key)); + } else { + out.clear_shared_key(); + } + if (!in.ratchet_salt.empty()) { + out.set_ratchet_salt(bytesToString(in.ratchet_salt)); + } else { + out.set_ratchet_salt(kDefaultRatchetSalt); + } + out.set_ratchet_window_size(in.ratchet_window_size); + out.set_failure_tolerance(in.failure_tolerance); + out.set_key_ring_size(in.key_ring_size); + out.set_key_derivation_function(static_cast(in.key_derivation_function)); + return out; +} + proto::AudioEncoding toProto(const AudioEncodingOptions& in) { proto::AudioEncoding msg; msg.set_max_bitrate(in.max_bitrate); diff --git a/src/room_proto_converter.h b/src/room_proto_converter.h index 8f502f31..03d1c733 100644 --- a/src/room_proto_converter.h +++ b/src/room_proto_converter.h @@ -18,6 +18,8 @@ #include +#include "e2ee.pb.h" +#include "livekit/e2ee.h" #include "livekit/room_event_types.h" #include "livekit/visibility.h" #include "room.pb.h" @@ -69,6 +71,8 @@ LIVEKIT_INTERNAL_API RoomMovedEvent roomMovedFromProto(const proto::RoomInfo& in // --------- room options conversions --------- +LIVEKIT_INTERNAL_API proto::KeyProviderOptions toProto(const KeyProviderOptions& in); + LIVEKIT_INTERNAL_API proto::AudioEncoding toProto(const AudioEncodingOptions& in); LIVEKIT_INTERNAL_API AudioEncodingOptions fromProto(const proto::AudioEncoding& in); diff --git a/src/tests/unit/test_e2ee_options.cpp b/src/tests/unit/test_e2ee_options.cpp new file mode 100644 index 00000000..6f847c04 --- /dev/null +++ b/src/tests/unit/test_e2ee_options.cpp @@ -0,0 +1,94 @@ +/* + * Copyright 2026 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include + +#include "ffi.pb.h" +#include "room_proto_converter.h" + +namespace livekit::test { +namespace { + +std::string bytesToString(const std::vector& bytes) { + return std::string(reinterpret_cast(bytes.data()), bytes.size()); +} + +} // namespace + +TEST(E2EEOptionsProtoTest, DefaultKeyProviderOptionsPopulateRequiredFields) { + const KeyProviderOptions options; + const auto proto_options = toProto(options); + + EXPECT_TRUE(proto_options.IsInitialized()); + EXPECT_FALSE(proto_options.has_shared_key()); + EXPECT_TRUE(proto_options.has_ratchet_window_size()); + EXPECT_TRUE(proto_options.has_ratchet_salt()); + EXPECT_TRUE(proto_options.has_failure_tolerance()); + EXPECT_TRUE(proto_options.has_key_ring_size()); + EXPECT_TRUE(proto_options.has_key_derivation_function()); + EXPECT_EQ(proto_options.ratchet_window_size(), kDefaultRatchetWindowSize); + EXPECT_EQ(proto_options.ratchet_salt(), kDefaultRatchetSalt); + EXPECT_EQ(proto_options.failure_tolerance(), kDefaultFailureTolerance); + EXPECT_EQ(proto_options.key_ring_size(), kDefaultKeyRingSize); + EXPECT_EQ(proto_options.key_derivation_function(), proto::PBKDF2); +} + +TEST(E2EEOptionsProtoTest, CustomKeyProviderOptionsRoundTripToProto) { + KeyProviderOptions options; + options.shared_key = std::vector{0x01, 0x02, 0x03}; + options.ratchet_salt = std::vector{0x04, 0x05}; + options.ratchet_window_size = 32; + options.failure_tolerance = 3; + options.key_ring_size = 8; + options.key_derivation_function = KeyDerivationFunction::HKDF; + + const auto proto_options = toProto(options); + + EXPECT_TRUE(proto_options.IsInitialized()); + ASSERT_TRUE(proto_options.has_shared_key()); + EXPECT_EQ(proto_options.shared_key(), bytesToString(*options.shared_key)); + EXPECT_EQ(proto_options.ratchet_salt(), bytesToString(options.ratchet_salt)); + EXPECT_EQ(proto_options.ratchet_window_size(), options.ratchet_window_size); + EXPECT_EQ(proto_options.failure_tolerance(), options.failure_tolerance); + EXPECT_EQ(proto_options.key_ring_size(), options.key_ring_size); + EXPECT_EQ(proto_options.key_derivation_function(), proto::HKDF); +} + +TEST(E2EEOptionsProtoTest, ConnectRequestWithEncryptionSerializes) { + E2EEOptions options; + options.key_provider_options.shared_key = std::vector{0x0A, 0x0B}; + + proto::FfiRequest request; + auto* connect = request.mutable_connect(); + connect->set_url("ws://localhost:7880"); + connect->set_token("test-token"); + auto* encryption = connect->mutable_options()->mutable_encryption(); + encryption->set_encryption_type(static_cast(options.encryption_type)); + encryption->mutable_key_provider_options()->CopyFrom(toProto(options.key_provider_options)); + + ASSERT_TRUE(request.IsInitialized()) << request.InitializationErrorString(); + + std::string serialized; + EXPECT_TRUE(request.SerializeToString(&serialized)); + EXPECT_FALSE(serialized.empty()); +} + +} // namespace livekit::test From add61d605c94cd870c8149a2a8f1628488c868e2 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Tue, 12 May 2026 15:36:43 -0600 Subject: [PATCH 2/2] e2ee both KeyDerivationFunction --- src/tests/integration/test_data_track.cpp | 208 +++++++++++++--------- 1 file changed, 121 insertions(+), 87 deletions(-) diff --git a/src/tests/integration/test_data_track.cpp b/src/tests/integration/test_data_track.cpp index a9acb065..f0d3dc63 100644 --- a/src/tests/integration/test_data_track.cpp +++ b/src/tests/integration/test_data_track.cpp @@ -68,20 +68,33 @@ std::size_t parseTestTrackIndex(const std::string& track_name) { return static_cast(std::stoul(track_name.substr(sizeof(kPrefix) - 1))); } -E2EEOptions makeE2EEOptions() { +E2EEOptions makeE2EEOptions(KeyDerivationFunction key_derivation_function = kDefaultKeyDerivationFunction) { E2EEOptions options; options.key_provider_options.shared_key = e2eeSharedKey(); + options.key_provider_options.key_derivation_function = key_derivation_function; return options; } -std::vector encryptedRoomConfigs(RoomDelegate* subscriber_delegate) { +std::vector encryptedRoomConfigs( + RoomDelegate* subscriber_delegate, KeyDerivationFunction key_derivation_function = kDefaultKeyDerivationFunction) { std::vector room_configs(2); - room_configs[0].room_options.encryption = makeE2EEOptions(); - room_configs[1].room_options.encryption = makeE2EEOptions(); + room_configs[0].room_options.encryption = makeE2EEOptions(key_derivation_function); + room_configs[1].room_options.encryption = makeE2EEOptions(key_derivation_function); room_configs[1].delegate = subscriber_delegate; return room_configs; } +std::string keyDerivationFunctionName(KeyDerivationFunction key_derivation_function) { + switch (key_derivation_function) { + case KeyDerivationFunction::PBKDF2: + return "PBKDF2"; + case KeyDerivationFunction::HKDF: + return "HKDF"; + default: + return "Unknown"; + } +} + template bool waitForCondition(Predicate&& predicate, std::chrono::milliseconds timeout, std::chrono::milliseconds interval = kPollingInterval) { @@ -181,6 +194,95 @@ DataTrackFrame readFrameWithTimeout(const std::shared_ptr& subs return future.get(); } +void runEncryptedDataTrackRoundTrip(KeyDerivationFunction key_derivation_function, const std::string& suffix) { + const auto track_name = makeTrackName(suffix); + + DataTrackPublishedDelegate subscriber_delegate; + auto room_configs = encryptedRoomConfigs(&subscriber_delegate, key_derivation_function); + auto rooms = testRooms(room_configs); + auto& publisher_room = rooms[0]; + auto& subscriber_room = rooms[1]; + + ASSERT_NE(publisher_room->e2eeManager(), nullptr); + ASSERT_NE(subscriber_room->e2eeManager(), nullptr); + ASSERT_NE(publisher_room->e2eeManager()->keyProvider(), nullptr); + ASSERT_NE(subscriber_room->e2eeManager()->keyProvider(), nullptr); + EXPECT_EQ(publisher_room->e2eeManager()->keyProvider()->options().key_derivation_function, key_derivation_function); + EXPECT_EQ(subscriber_room->e2eeManager()->keyProvider()->options().key_derivation_function, key_derivation_function); + publisher_room->e2eeManager()->setEnabled(true); + subscriber_room->e2eeManager()->setEnabled(true); + EXPECT_EQ(publisher_room->e2eeManager()->keyProvider()->exportSharedKey(), e2eeSharedKey()); + EXPECT_EQ(subscriber_room->e2eeManager()->keyProvider()->exportSharedKey(), e2eeSharedKey()); + + auto publish_result = publisher_room->localParticipant()->publishDataTrack(track_name); + if (!publish_result) { + FAIL() << describeDataTrackError(publish_result.error()); + } + const auto& local_track = publish_result.value(); + ASSERT_TRUE(local_track->isPublished()); + EXPECT_TRUE(local_track->info().uses_e2ee); + + auto remote_track = subscriber_delegate.waitForTrack(kTrackWaitTimeout); + ASSERT_NE(remote_track, nullptr) << "Timed out waiting for remote data track"; + EXPECT_TRUE(remote_track->isPublished()); + EXPECT_TRUE(remote_track->info().uses_e2ee); + EXPECT_EQ(remote_track->info().name, track_name); + + auto subscribe_result = remote_track->subscribe(); + if (!subscribe_result) { + FAIL() << describeDataTrackError(subscribe_result.error()); + } + const auto& subscription = subscribe_result.value(); + + std::promise frame_promise; + auto frame_future = frame_promise.get_future(); + std::thread reader([&]() { + try { + DataTrackFrame frame; + if (!subscription->read(frame)) { + throw std::runtime_error("Subscription ended before an encrypted frame arrived"); + } + frame_promise.set_value(std::move(frame)); + } catch (...) { + frame_promise.set_exception(std::current_exception()); + } + }); + + bool pushed = false; + for (int index = 0; index < 200; ++index) { + std::vector payload(kLargeFramePayloadBytes, static_cast(index + 1)); + auto push_result = local_track->tryPush(std::move(payload)); + pushed = static_cast(push_result) || pushed; + if (frame_future.wait_for(25ms) == std::future_status::ready) { + break; + } + } + + const auto frame_status = frame_future.wait_for(5s); + if (frame_status != std::future_status::ready) { + subscription->close(); + } + reader.join(); + ASSERT_TRUE(pushed) << "Failed to push encrypted data frames"; + ASSERT_EQ(frame_status, std::future_status::ready) << "Timed out waiting for encrypted frame delivery"; + + DataTrackFrame frame; + try { + frame = frame_future.get(); + } catch (const std::exception& e) { + FAIL() << e.what(); + } + ASSERT_FALSE(frame.payload.empty()); + const auto first_byte = frame.payload.front(); + EXPECT_TRUE(std::all_of(frame.payload.begin(), frame.payload.end(), [first_byte](std::uint8_t byte) { + return byte == first_byte; + })) << "Encrypted payload is not byte-consistent"; + EXPECT_FALSE(frame.user_timestamp.has_value()) << "Unexpected user timestamp on encrypted frame"; + + subscription->close(); + local_track->unpublishDataTrack(); +} + } // namespace class DataTrackE2ETest : public LiveKitTestBase {}; @@ -188,6 +290,9 @@ class DataTrackE2ETest : public LiveKitTestBase {}; class DataTrackTransportTest : public DataTrackE2ETest, public ::testing::WithParamInterface> {}; +class DataTrackKeyDerivationTest : public DataTrackE2ETest, + public ::testing::WithParamInterface {}; + TEST_P(DataTrackTransportTest, PublishesAndReceivesFramesEndToEnd) { const auto publish_fps = std::get<0>(GetParam()); const auto payload_len = std::get<1>(GetParam()); @@ -661,90 +766,11 @@ TEST_F(DataTrackE2ETest, PreservesUserTimestampEndToEnd) { } TEST_F(DataTrackE2ETest, PublishesAndReceivesEncryptedFramesEndToEnd) { - const auto track_name = makeTrackName("e2ee_transport"); - - DataTrackPublishedDelegate subscriber_delegate; - auto room_configs = encryptedRoomConfigs(&subscriber_delegate); - auto rooms = testRooms(room_configs); - auto& publisher_room = rooms[0]; - auto& subscriber_room = rooms[1]; - - ASSERT_NE(publisher_room->e2eeManager(), nullptr); - ASSERT_NE(subscriber_room->e2eeManager(), nullptr); - ASSERT_NE(publisher_room->e2eeManager()->keyProvider(), nullptr); - ASSERT_NE(subscriber_room->e2eeManager()->keyProvider(), nullptr); - publisher_room->e2eeManager()->setEnabled(true); - subscriber_room->e2eeManager()->setEnabled(true); - EXPECT_EQ(publisher_room->e2eeManager()->keyProvider()->exportSharedKey(), e2eeSharedKey()); - EXPECT_EQ(subscriber_room->e2eeManager()->keyProvider()->exportSharedKey(), e2eeSharedKey()); - - auto publish_result = publisher_room->localParticipant()->publishDataTrack(track_name); - if (!publish_result) { - FAIL() << describeDataTrackError(publish_result.error()); - } - auto local_track = publish_result.value(); - ASSERT_TRUE(local_track->isPublished()); - EXPECT_TRUE(local_track->info().uses_e2ee); - - auto remote_track = subscriber_delegate.waitForTrack(kTrackWaitTimeout); - ASSERT_NE(remote_track, nullptr) << "Timed out waiting for remote data track"; - EXPECT_TRUE(remote_track->isPublished()); - EXPECT_TRUE(remote_track->info().uses_e2ee); - EXPECT_EQ(remote_track->info().name, track_name); - - auto subscribe_result = remote_track->subscribe(); - if (!subscribe_result) { - FAIL() << describeDataTrackError(subscribe_result.error()); - } - auto subscription = subscribe_result.value(); - - std::promise frame_promise; - auto frame_future = frame_promise.get_future(); - std::thread reader([&]() { - try { - DataTrackFrame frame; - if (!subscription->read(frame)) { - throw std::runtime_error("Subscription ended before an encrypted frame arrived"); - } - frame_promise.set_value(std::move(frame)); - } catch (...) { - frame_promise.set_exception(std::current_exception()); - } - }); - - bool pushed = false; - for (int index = 0; index < 200; ++index) { - std::vector payload(kLargeFramePayloadBytes, static_cast(index + 1)); - auto push_result = local_track->tryPush(std::move(payload)); - pushed = static_cast(push_result) || pushed; - if (frame_future.wait_for(25ms) == std::future_status::ready) { - break; - } - } - - const auto frame_status = frame_future.wait_for(5s); - if (frame_status != std::future_status::ready) { - subscription->close(); - } - reader.join(); - ASSERT_TRUE(pushed) << "Failed to push encrypted data frames"; - ASSERT_EQ(frame_status, std::future_status::ready) << "Timed out waiting for encrypted frame delivery"; - - DataTrackFrame frame; - try { - frame = frame_future.get(); - } catch (const std::exception& e) { - FAIL() << e.what(); - } - ASSERT_FALSE(frame.payload.empty()); - const auto first_byte = frame.payload.front(); - EXPECT_TRUE(std::all_of(frame.payload.begin(), frame.payload.end(), [first_byte](std::uint8_t byte) { - return byte == first_byte; - })) << "Encrypted payload is not byte-consistent"; - EXPECT_FALSE(frame.user_timestamp.has_value()) << "Unexpected user timestamp on encrypted frame"; + runEncryptedDataTrackRoundTrip(kDefaultKeyDerivationFunction, "e2ee_transport"); +} - subscription->close(); - local_track->unpublishDataTrack(); +TEST_P(DataTrackKeyDerivationTest, PublishesAndReceivesEncryptedFramesEndToEnd) { + runEncryptedDataTrackRoundTrip(GetParam(), "e2ee_" + keyDerivationFunctionName(GetParam())); } TEST_F(DataTrackE2ETest, PreservesUserTimestampOnEncryptedDataTrack) { @@ -834,8 +860,16 @@ std::string dataTrackParamName(const ::testing::TestParamInfo& info) { + return keyDerivationFunctionName(info.param); +} + INSTANTIATE_TEST_SUITE_P(DataTrackScenarios, DataTrackTransportTest, ::testing::Values(std::make_tuple(120.0, size_t{8192}), std::make_tuple(10.0, size_t{196608})), dataTrackParamName); +INSTANTIATE_TEST_SUITE_P(KeyDerivationFunctions, DataTrackKeyDerivationTest, + ::testing::Values(KeyDerivationFunction::PBKDF2, KeyDerivationFunction::HKDF), + keyDerivationParamName); + } // namespace livekit::test