From 3a09f353098772cecb32637ba3c1fd8680e1c470 Mon Sep 17 00:00:00 2001 From: shijing xian Date: Mon, 15 Dec 2025 19:50:47 -0800 Subject: [PATCH 1/2] Get E2EE functional --- CMakeLists.txt | 2 + README.md | 13 +++ examples/simple_data_stream/main.cpp | 28 +++-- examples/simple_room/main.cpp | 100 ++++++++++++---- examples/simple_rpc/main.cpp | 65 ++++++----- include/livekit/e2ee.h | 166 +++++++++++++++++++++++++++ include/livekit/livekit.h | 1 + include/livekit/room.h | 34 +++--- include/livekit/track_publication.h | 8 +- src/e2ee.cpp | 134 +++++++++++++++++++++ src/ffi_client.cpp | 27 ++++- src/local_track_publication.cpp | 3 +- src/remote_track_publication.cpp | 3 +- src/room.cpp | 11 ++ src/track_proto_converter.cpp | 14 --- src/track_proto_converter.h | 3 - src/video_utils.cpp | 16 +++ 17 files changed, 512 insertions(+), 116 deletions(-) create mode 100644 include/livekit/e2ee.h create mode 100644 src/e2ee.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7745c13..3e9a47d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,7 @@ add_library(livekit include/livekit/audio_source.h include/livekit/audio_stream.h include/livekit/data_stream.h + include/livekit/e2ee.h include/livekit/room.h include/livekit/room_event_types.h include/livekit/room_delegate.h @@ -185,6 +186,7 @@ add_library(livekit src/audio_source.cpp src/audio_stream.cpp src/data_stream.cpp + src/e2ee.cpp src/ffi_handle.cpp src/ffi_client.cpp src/ffi_client.h diff --git a/README.md b/README.md index c6937a4..c3da39b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,19 @@ export LIVEKIT_TOKEN= ./build/examples/SimpleRoom ``` +**End-to-End Encryption (E2EE)** +You can enable E2E encryption for the streams via --enable_e2ee and --e2ee_key flags, +by running the following cmds in two terminals or computers. **Note, jwt_token needs to be different identity** +```bash +./build/examples/SimpleRoom --url $URL --token --enable_e2ee --e2ee_key="your_key" +``` +**Note**, **all participants must use the exact same E2EE configuration and shared key.** +If the E2EE keys do not match between participants: +- Media cannot be decrypted +- Video tracks will appear as a black screen +- Audio will be silent +- No explicit error may be shown at the UI level + Press Ctrl-C to exit the example. ### SimpleRpc diff --git a/examples/simple_data_stream/main.cpp b/examples/simple_data_stream/main.cpp index 094eb88..b5ecbce 100644 --- a/examples/simple_data_stream/main.cpp +++ b/examples/simple_data_stream/main.cpp @@ -12,7 +12,8 @@ #include #include "livekit/livekit.h" -#include "livekit_ffi.h" +// TODO, remove the ffi_client from the public usage. +#include "ffi_client.h" using namespace livekit; @@ -46,10 +47,10 @@ std::string randomHexId(std::size_t nbytes = 16) { } // Greeting: send text + image -void greetParticipant(Room &room, const std::string &identity) { +void greetParticipant(Room *room, const std::string &identity) { std::cout << "[DataStream] Greeting participant: " << identity << "\n"; - LocalParticipant *lp = room.localParticipant(); + LocalParticipant *lp = room->localParticipant(); if (!lp) { std::cerr << "[DataStream] No local participant, cannot greet.\n"; return; @@ -209,12 +210,12 @@ int main(int argc, char *argv[]) { std::signal(SIGTERM, handleSignal); #endif - Room room{}; + auto room = std::make_unique(); RoomOptions options; options.auto_subscribe = true; options.dynacast = false; - bool ok = room.Connect(url, token, options); + bool ok = room->Connect(url, token, options); std::cout << "[DataStream] Connect result: " << std::boolalpha << ok << "\n"; if (!ok) { std::cerr << "[DataStream] Failed to connect to room\n"; @@ -222,12 +223,12 @@ int main(int argc, char *argv[]) { return 1; } - auto info = room.room_info(); + auto info = room->room_info(); std::cout << "[DataStream] Connected to room '" << info.name << "', participants: " << info.num_participants << "\n"; // Register stream handlers - room.registerTextStreamHandler( + room->registerTextStreamHandler( "chat", [](std::shared_ptr reader, const std::string &participant_identity) { std::thread t(handleChatMessage, std::move(reader), @@ -235,7 +236,7 @@ int main(int argc, char *argv[]) { t.detach(); }); - room.registerByteStreamHandler( + room->registerByteStreamHandler( "files", [](std::shared_ptr reader, const std::string &participant_identity) { std::thread t(handleWelcomeImage, std::move(reader), @@ -245,12 +246,12 @@ int main(int argc, char *argv[]) { // Greet existing participants { - auto remotes = room.remoteParticipants(); + auto remotes = room->remoteParticipants(); for (const auto &rp : remotes) { if (!rp) continue; std::cout << "Remote: " << rp->identity() << "\n"; - greetParticipant(room, rp->identity()); + greetParticipant(room.get(), rp->identity()); } } @@ -258,12 +259,12 @@ int main(int argc, char *argv[]) { // // If Room API exposes a participant-connected callback, you could do: // - // room.onParticipantConnected( + // room->onParticipantConnected( // [&](RemoteParticipant& participant) { // std::cout << "[DataStream] participant connected: " // << participant.sid() << " " << participant.identity() // << "\n"; - // greetParticipant(room, participant.identity()); + // greetParticipant(room.get(), participant.identity()); // }); // // Adjust to your actual event API. @@ -274,6 +275,9 @@ int main(int argc, char *argv[]) { } std::cout << "[DataStream] Shutting down...\n"; + // It is important to clean up the delegate and room in order. + room->setDelegate(nullptr); + room.reset(); FfiClient::instance().shutdown(); return 0; } diff --git a/examples/simple_room/main.cpp b/examples/simple_room/main.cpp index dd865c5..05386cf 100644 --- a/examples/simple_room/main.cpp +++ b/examples/simple_room/main.cpp @@ -29,9 +29,8 @@ #include "livekit/livekit.h" #include "sdl_media_manager.h" #include "wav_audio_source.h" - -// TODO(shijing), remove this livekit_ffi.h as it should be internal only. -#include "livekit_ffi.h" +// TODO, remove the ffi_client from the public usage. +#include "ffi_client.h" // Consider expose this video_utils.h to public ? #include "video_utils.h" @@ -43,19 +42,32 @@ namespace { std::atomic g_running{true}; void printUsage(const char *prog) { - std::cerr << "Usage:\n" - << " " << prog << " \n" - << "or:\n" - << " " << prog << " --url= --token=\n" - << " " << prog << " --url --token \n\n" - << "Env fallbacks:\n" - << " LIVEKIT_URL, LIVEKIT_TOKEN\n"; + std::cerr + << "Usage:\n" + << " " << prog + << " [--enable_e2ee] [--e2ee_key ]\n" + << "or:\n" + << " " << prog + << " --url= --token= [--enable_e2ee] [--e2ee_key=]\n" + << " " << prog + << " --url --token [--enable_e2ee] [--e2ee_key " + "]\n\n" + << "E2EE:\n" + << " --enable_e2ee Enable end-to-end encryption (E2EE)\n" + << " --e2ee_key Optional shared key (UTF-8). If omitted, " + "E2EE is enabled\n" + << " but no shared key is set (advanced " + "usage).\n\n" + << "Env fallbacks:\n" + << " LIVEKIT_URL, LIVEKIT_TOKEN, LIVEKIT_E2EE_KEY\n"; } void handleSignal(int) { g_running.store(false); } -bool parseArgs(int argc, char *argv[], std::string &url, std::string &token) { - // 1) --help +bool parseArgs(int argc, char *argv[], std::string &url, std::string &token, + bool &enable_e2ee, std::string &e2ee_key) { + enable_e2ee = false; + // --help for (int i = 1; i < argc; ++i) { std::string a = argv[i]; if (a == "-h" || a == "--help") { @@ -63,7 +75,7 @@ bool parseArgs(int argc, char *argv[], std::string &url, std::string &token) { } } - // 2) flags: --url= / --token= or split form + // flags: --url= / --token= or split form auto get_flag_value = [&](const std::string &name, int &i) -> std::string { std::string arg = argv[i]; const std::string eq = name + "="; @@ -79,7 +91,9 @@ bool parseArgs(int argc, char *argv[], std::string &url, std::string &token) { for (int i = 1; i < argc; ++i) { const std::string a = argv[i]; - if (a.rfind("--url", 0) == 0) { + if (a == "--enable_e2ee") { + enable_e2ee = true; + } else if (a.rfind("--url", 0) == 0) { auto v = get_flag_value("--url", i); if (!v.empty()) url = v; @@ -87,10 +101,14 @@ bool parseArgs(int argc, char *argv[], std::string &url, std::string &token) { auto v = get_flag_value("--token", i); if (!v.empty()) token = v; + } else if (a.rfind("--e2ee_key", 0) == 0) { + auto v = get_flag_value("--e2ee_key", i); + if (!v.empty()) + e2ee_key = v; } } - // 3) positional if still empty + // positional if still empty if (url.empty() || token.empty()) { std::vector pos; for (int i = 1; i < argc; ++i) { @@ -118,6 +136,11 @@ bool parseArgs(int argc, char *argv[], std::string &url, std::string &token) { if (e) token = e; } + if (e2ee_key.empty()) { + const char *e = std::getenv("LIVEKIT_E2EE_KEY"); + if (e) + e2ee_key = e; + } return !(url.empty() || token.empty()); } @@ -211,11 +234,17 @@ class SimpleRoomDelegate : public livekit::RoomDelegate { SDLMediaManager &media_; }; +static std::vector toBytes(const std::string &s) { + return std::vector(s.begin(), s.end()); +} + } // namespace int main(int argc, char *argv[]) { std::string url, token; - if (!parseArgs(argc, argv, url, token)) { + bool enable_e2ee = false; + std::string e2ee_key; + if (!parseArgs(argc, argv, url, token, enable_e2ee, e2ee_key)) { printUsage(argv[0]); return 1; } @@ -240,14 +269,33 @@ int main(int argc, char *argv[]) { // Handle Ctrl-C to exit the idle loop std::signal(SIGINT, handleSignal); - livekit::Room room{}; + auto room = std::make_unique(); SimpleRoomDelegate delegate(media); - room.setDelegate(&delegate); + room->setDelegate(&delegate); RoomOptions options; options.auto_subscribe = true; options.dynacast = false; - bool res = room.Connect(url, token, options); + + if (enable_e2ee) { + livekit::E2EEOptions e2ee; + e2ee.encryption_type = livekit::EncryptionType::GCM; + // Optional shared key: if empty, we enable E2EE without setting a shared + // key. (Advanced use: keys can be set/ratcheted later via + // E2EEManager/KeyProvider.) + if (!e2ee_key.empty()) { + e2ee.shared_key = toBytes(e2ee_key); + } + options.e2ee = e2ee; + if (!e2ee_key.empty()) { + std::cout << "[E2EE] enabled : (shared key length=" << e2ee_key.size() + << ")\n"; + } else { + std::cout << "[E2EE] enabled: (no shared key set)\n"; + } + } + + bool res = room->Connect(url, token, options); std::cout << "Connect result is " << std::boolalpha << res << std::endl; if (!res) { std::cerr << "Failed to connect to room\n"; @@ -255,7 +303,7 @@ int main(int argc, char *argv[]) { return 1; } - auto info = room.room_info(); + auto info = room->room_info(); std::cout << "Connected to room:\n" << " SID: " << (info.sid ? *info.sid : "(none)") << "\n" << " Name: " << info.name << "\n" @@ -286,7 +334,7 @@ int main(int argc, char *argv[]) { try { // publishTrack takes std::shared_ptr, LocalAudioTrack derives from // Track - audioPub = room.localParticipant()->publishTrack(audioTrack, audioOpts); + audioPub = room->localParticipant()->publishTrack(audioTrack, audioOpts); std::cout << "Published track:\n" << " SID: " << audioPub->sid() << "\n" @@ -314,7 +362,7 @@ int main(int argc, char *argv[]) { try { // publishTrack takes std::shared_ptr, LocalAudioTrack derives from // Track - videoPub = room.localParticipant()->publishTrack(videoTrack, videoOpts); + videoPub = room->localParticipant()->publishTrack(videoTrack, videoOpts); std::cout << "Published track:\n" << " SID: " << videoPub->sid() << "\n" @@ -341,12 +389,16 @@ int main(int argc, char *argv[]) { media.stopMic(); // Clean up the audio track publishment - room.localParticipant()->unpublishTrack(audioPub->sid()); + room->localParticipant()->unpublishTrack(audioPub->sid()); media.stopCamera(); // Clean up the video track publishment - room.localParticipant()->unpublishTrack(videoPub->sid()); + room->localParticipant()->unpublishTrack(videoPub->sid()); + + // Must be cleaned up before FfiClient::instance().shutdown(); + room->setDelegate(nullptr); + room.reset(); FfiClient::instance().shutdown(); std::cout << "Exiting.\n"; diff --git a/examples/simple_rpc/main.cpp b/examples/simple_rpc/main.cpp index f93f8f7..b3ed6d4 100644 --- a/examples/simple_rpc/main.cpp +++ b/examples/simple_rpc/main.cpp @@ -33,7 +33,8 @@ #include #include "livekit/livekit.h" -#include "livekit_ffi.h" +// TODO, remove the ffi_client from the public usage. +#include "ffi_client.h" using namespace livekit; using namespace std::chrono_literals; @@ -67,12 +68,12 @@ inline double nowMs() { // Poll the room until a remote participant with the given identity appears, // or until 'timeout' elapses. Returns true if found, false on timeout. -bool waitForParticipant(Room &room, const std::string &identity, +bool waitForParticipant(Room *room, const std::string &identity, std::chrono::milliseconds timeout) { auto start = std::chrono::steady_clock::now(); while (std::chrono::steady_clock::now() - start < timeout) { - if (room.remoteParticipant(identity) != nullptr) { + if (room->remoteParticipant(identity) != nullptr) { return true; } std::this_thread::sleep_for(100ms); @@ -82,7 +83,7 @@ bool waitForParticipant(Room &room, const std::string &identity, // For the caller: wait for a specific peer, and if they don't show up, // explain why and how to start them in another terminal. -bool ensurePeerPresent(Room &room, const std::string &identity, +bool ensurePeerPresent(Room *room, const std::string &identity, const std::string &friendly_role, const std::string &url, std::chrono::seconds timeout) { std::cout << "[Caller] Waiting up to " << timeout.count() << "s for " @@ -96,7 +97,7 @@ bool ensurePeerPresent(Room &room, const std::string &identity, return true; } // Timed out - auto info = room.room_info(); + auto info = room->room_info(); const std::string room_name = info.name; std::cout << "[Caller] Timed out after " << timeout.count() << "s waiting for " << friendly_role << " (identity=\"" << identity @@ -232,9 +233,9 @@ std::string parseStringFromJson(const std::string &json) { } // RPC handler registration -void registerReceiverMethods(Room &greeters_room, Room &math_genius_room) { - LocalParticipant *greeter_lp = greeters_room.localParticipant(); - LocalParticipant *math_genius_lp = math_genius_room.localParticipant(); +void registerReceiverMethods(Room *greeters_room, Room *math_genius_room) { + LocalParticipant *greeter_lp = greeters_room->localParticipant(); + LocalParticipant *math_genius_lp = math_genius_room->localParticipant(); // arrival greeter_lp->registerRpcMethod( @@ -308,11 +309,11 @@ void registerReceiverMethods(Room &greeters_room, Room &math_genius_room) { // so the caller sees UNSUPPORTED_METHOD } -void performGreeting(Room &room) { +void performGreeting(Room *room) { std::cout << "[Caller] Letting the greeter know that I've arrived\n"; double t0 = nowMs(); try { - std::string response = room.localParticipant()->performRpc( + std::string response = room->localParticipant()->performRpc( "greeter", "arrival", "Hello", std::nullopt); double t1 = nowMs(); std::cout << "[Caller] RTT: " << (t1 - t0) << " ms\n"; @@ -326,12 +327,12 @@ void performGreeting(Room &room) { } } -void performSquareRoot(Room &room) { +void performSquareRoot(Room *room) { std::cout << "[Caller] What's the square root of 16?\n"; double t0 = nowMs(); try { std::string payload = makeNumberJson("number", 16.0); - std::string response = room.localParticipant()->performRpc( + std::string response = room->localParticipant()->performRpc( "math-genius", "square-root", payload, std::nullopt); double t1 = nowMs(); std::cout << "[Caller] RTT: " << (t1 - t0) << " ms\n"; @@ -345,7 +346,7 @@ void performSquareRoot(Room &room) { } } -void performQuantumHyperGeometricSeries(Room &room) { +void performQuantumHyperGeometricSeries(Room *room) { std::cout << "\n=== Unsupported Method Example ===\n"; std::cout << "[Caller] Asking math-genius for 'quantum-hypergeometric-series'. " @@ -353,7 +354,7 @@ void performQuantumHyperGeometricSeries(Room &room) { double t0 = nowMs(); try { std::string payload = makeNumberJson("number", 42.0); - std::string response = room.localParticipant()->performRpc( + std::string response = room->localParticipant()->performRpc( "math-genius", "quantum-hypergeometric-series", payload, std::nullopt); double t1 = nowMs(); std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; @@ -373,14 +374,14 @@ void performQuantumHyperGeometricSeries(Room &room) { } } -void performDivide(Room &room) { +void performDivide(Room *room) { std::cout << "\n=== Divide Example ===\n"; std::cout << "[Caller] Asking math-genius to divide 10 by 0. " "This is EXPECTED to FAIL with an APPLICATION_ERROR.\n"; double t0 = nowMs(); try { std::string payload = "{\"dividend\":10,\"divisor\":0}"; - std::string response = room.localParticipant()->performRpc( + std::string response = room->localParticipant()->performRpc( "math-genius", "divide", payload, std::nullopt); double t1 = nowMs(); std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; @@ -401,7 +402,7 @@ void performDivide(Room &room) { } } -void performLongCalculation(Room &room) { +void performLongCalculation(Room *room) { std::cout << "\n=== Long Calculation Example ===\n"; std::cout << "[Caller] Asking math-genius for a calculation that takes 30s.\n"; @@ -409,7 +410,7 @@ void performLongCalculation(Room &room) { << "[Caller] Giving only 10s to respond. EXPECTED RESULT: TIMEOUT.\n"; double t0 = nowMs(); try { - std::string response = room.localParticipant()->performRpc( + std::string response = room->localParticipant()->performRpc( "math-genius", "long-calculation", "{}", 10.0); double t1 = nowMs(); std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; @@ -452,12 +453,12 @@ int main(int argc, char *argv[]) { // Ctrl-C to quit the program std::signal(SIGINT, handleSignal); - Room room{}; + auto room = std::make_unique(); RoomOptions options; options.auto_subscribe = true; options.dynacast = false; - bool res = room.Connect(url, token, options); + bool res = room->Connect(url, token, options); std::cout << "Connect result is " << std::boolalpha << res << "\n"; if (!res) { std::cerr << "Failed to connect to room\n"; @@ -465,7 +466,7 @@ int main(int argc, char *argv[]) { return 1; } - auto info = room.room_info(); + auto info = room->room_info(); std::cout << "Connected to room:\n" << " Name: " << info.name << "\n" << " Metadata: " << info.metadata << "\n" @@ -474,31 +475,32 @@ int main(int argc, char *argv[]) { try { if (role == "caller") { // Check that both peers are present (or explain how to start them). - bool has_greeter = ensurePeerPresent(room, "greeter", "greeter", url, 8s); + bool has_greeter = + ensurePeerPresent(room.get(), "greeter", "greeter", url, 8s); bool has_math_genius = - ensurePeerPresent(room, "math-genius", "math-genius", url, 8s); + ensurePeerPresent(room.get(), "math-genius", "math-genius", url, 8s); if (!has_greeter || !has_math_genius) { std::cout << "\n[Caller] One or more RPC peers are missing. " << "Some examples may be skipped.\n"; } if (has_greeter) { std::cout << "\n\nRunning greeting example...\n"; - performGreeting(room); + performGreeting(room.get()); } else { std::cout << "[Caller] Skipping greeting example because greeter is " "not present.\n"; } if (has_math_genius) { std::cout << "\n\nRunning error handling example...\n"; - performDivide(room); + performDivide(room.get()); std::cout << "\n\nRunning math example...\n"; - performSquareRoot(room); + performSquareRoot(room.get()); std::this_thread::sleep_for(2s); - performQuantumHyperGeometricSeries(room); + performQuantumHyperGeometricSeries(room.get()); std::cout << "\n\nRunning long calculation with timeout...\n"; - performLongCalculation(room); + performLongCalculation(room.get()); } else { std::cout << "[Caller] Skipping math examples because math-genius is " "not present.\n"; @@ -517,10 +519,10 @@ int main(int argc, char *argv[]) { if (role == "greeter") { // Use the same room object for both arguments; only "arrival" is used. - registerReceiverMethods(room, room); + registerReceiverMethods(room.get(), room.get()); } else { // math-genius // We only need math handlers; greeter handlers won't be used. - registerReceiverMethods(room, room); + registerReceiverMethods(room.get(), room.get()); } std::cout << "RPC handlers registered for role=" << role @@ -537,6 +539,9 @@ int main(int argc, char *argv[]) { std::cerr << "Unexpected error in main: " << e.what() << "\n"; } + // It is important to clean up the delegate and room in order. + room->setDelegate(nullptr); + room.reset(); FfiClient::instance().shutdown(); return 0; } diff --git a/include/livekit/e2ee.h b/include/livekit/e2ee.h new file mode 100644 index 0000000..9e4d479 --- /dev/null +++ b/include/livekit/e2ee.h @@ -0,0 +1,166 @@ +/* + * Copyright 2025 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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace livekit { + +/* Encryption algorithm type used by the underlying stack. Keep this aligned + * with your proto enum. */ +enum class EncryptionType { NONE = 0, GCM = 1, CUSTOM = 2 }; + +/// End-to-end encryption (E2EE) configuration. +/// +/// When enabled, media frames are encrypted before being sent and +/// decrypted on receipt. Keys may be provided up-front (shared-key mode) +/// or supplied by other mechanisms supported by the underlying runtime. +struct E2EEOptions { + bool enabled; + /// Encryption algorithm to use. + /// + /// GCM is the default and recommended option. + EncryptionType encryption_type = EncryptionType::GCM; + + /// Shared static key for shared-key E2EE. + /// + /// When using shared-key E2EE, this key must be provided and must be + /// identical (byte-for-byte) for all participants in the room in order + /// to successfully encrypt and decrypt media. + /// + /// If this is empty while E2EE is enabled, media cannot be decrypted + /// and participants will not be able to communicate (e.g. black video / + /// silent audio). + std::vector shared_key; + + /// Optional salt used when deriving ratcheted encryption keys. + /// + /// If empty, a default salt is used by the underlying implementation. + std::vector ratchet_salt; + + /// Optional ratchet window size. + /// + /// Controls how many previous keys are retained during ratcheting. + /// A value of 0 indicates that the implementation default is used. + int ratchet_window_size = 0; + + /// Optional failure tolerance for ratcheting. + /// + /// Specifies how many consecutive ratcheting failures are tolerated + /// before encryption errors are reported. A value of 0 indicates + /// that the implementation default is used. + int failure_tolerance = 0; +}; + +class E2EEManager { +public: + virtual ~E2EEManager(); + + E2EEManager(const E2EEManager &) = delete; + E2EEManager &operator=(const E2EEManager &) = delete; + + E2EEManager(E2EEManager &&) noexcept; + E2EEManager &operator=(E2EEManager &&) noexcept; + + /** + * Returns whether end-to-end encryption (E2EE) is currently enabled. + * + * This reflects the runtime encryption state for media tracks + * associated with the room. + */ + bool enabled() const; + + /** + * Enable or disable end-to-end encryption at runtime. + * + * Disabling E2EE will stop encrypting outgoing media and stop + * decrypting incoming media. + * + * NOTE: + * - All participants must agree on E2EE state and keys in order + * to successfully exchange media. + * - Disabling E2EE while other participants still have it enabled + * will result in media being undecodable. + */ + void setEnabled(bool enabled); + + /** + * Set or replace the shared encryption key at the given key index. + * + * This is typically used for: + * - Manual key rotation + * + * The provided key MUST be identical across all participants + * using shared-key E2EE, otherwise media decryption will fail. + * + * @param key Raw key bytes + * @param key_index Index of the key to set (default: 0) + */ + void setSharedKey(const std::vector &key, int key_index = 0); + + /** + * Export the currently active shared key at the given key index. + * + * This API is primarily intended for debugging, verification, + * or diagnostics. Applications should avoid exporting keys + * unless absolutely necessary. + * + * @param key_index Index of the key to export (default: 0) + * @return Raw key bytes + */ + std::vector exportSharedKey(int key_index = 0) const; + + /** + * Ratchet (derive) a new shared key at the given key index. + * + * This advances the key forward and returns the newly derived key. + * All participants must ratchet keys in the same order to remain + * in sync. + * + * @param key_index Index of the key to ratchet (default: 0) + * @return Newly derived key bytes + */ + std::vector ratchetSharedKey(int key_index = 0); + +protected: + /* + * Construct an E2EE manager for a connected room. + * + * This constructor are intended for internal use by room. + * Applications should NOT create their own E2EEManager instances. + * + * After successfully connecting to a room with E2EE enabled, + * obtain the E2EE manager via the Room: + * + * auto e2ee_manager = room->e2eeManager(); + * + * The Room owns and manages the lifetime of the E2EEManager and ensures + * it is correctly wired to the underlying room handle and track lifecycle. + */ + explicit E2EEManager(std::uint64_t room_handle, E2EEOptions config); + friend class Room; + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace livekit diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index 6bb4152..9666d13 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -17,6 +17,7 @@ #include "audio_frame.h" #include "audio_source.h" #include "audio_stream.h" +#include "e2ee.h" #include "local_audio_track.h" #include "local_participant.h" #include "local_track_publication.h" diff --git a/include/livekit/room.h b/include/livekit/room.h index ec8b74e..0bf2aef 100644 --- a/include/livekit/room.h +++ b/include/livekit/room.h @@ -17,8 +17,8 @@ #ifndef LIVEKIT_ROOM_H #define LIVEKIT_ROOM_H -#include "ffi_client.h" #include "livekit/data_stream.h" +#include "livekit/e2ee.h" #include "livekit/ffi_handle.h" #include "livekit/room_event_types.h" #include @@ -32,27 +32,11 @@ namespace proto { class FfiEvent; } +struct E2EEOptions; +class E2EEManager; class LocalParticipant; class RemoteParticipant; -/// Represents end-to-end encryption (E2EE) settings. -struct E2EEOptions { - // Encryption algorithm type. - int encryption_type = 0; - - // Shared static key. If provided, this key is used for encryption. - std::string shared_key; - - // Salt used when deriving ratcheted encryption keys. - std::string ratchet_salt; - - // How many consecutive ratcheting failures are tolerated before an error. - int failure_tolerance = 0; - - // Maximum size of the ratchet window. - int ratchet_window_size = 0; -}; - // Represents a single ICE server configuration. struct IceServer { // TURN/STUN server URL (e.g. "stun:stun.l.google.com:19302"). @@ -234,6 +218,16 @@ class Room { */ void unregisterByteStreamHandler(const std::string &topic); + /** + * Returns the room's E2EE manager, or nullptr if E2EE was not enabled at + * connect time. + * + * Notes: + * - The manager is created after a successful Connect(). + * - If E2EE was not configured in RoomOptions, this will return nullptr. + */ + E2EEManager *e2eeManager() const; + private: mutable std::mutex lock_; bool connected_{false}; @@ -251,6 +245,8 @@ class Room { text_stream_readers_; std::unordered_map> byte_stream_readers_; + // E2EE + std::unique_ptr e2ee_manager_; void OnEvent(const proto::FfiEvent &event); }; diff --git a/include/livekit/track_publication.h b/include/livekit/track_publication.h index b0365bf..5d0ff47 100644 --- a/include/livekit/track_publication.h +++ b/include/livekit/track_publication.h @@ -21,18 +21,12 @@ #include #include +#include "livekit/e2ee.h" #include "livekit/ffi_handle.h" #include "livekit/track.h" namespace livekit { -// TODO, move this EncryptionType to e2ee_types.h -enum class EncryptionType { - NONE = 0, - GCM = 1, - CUSTOM = 2, -}; - class Track; class LocalTrack; class RemoteTrack; diff --git a/src/e2ee.cpp b/src/e2ee.cpp new file mode 100644 index 0000000..e27b603 --- /dev/null +++ b/src/e2ee.cpp @@ -0,0 +1,134 @@ +/* + * Copyright 2025 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 "livekit/e2ee.h" + +#include +#include + +#include "e2ee.pb.h" +#include "ffi.pb.h" +#include "ffi_client.h" + +namespace livekit { + +namespace { + +std::string bytesToProtoBytes(const std::vector &b) { + return std::string(reinterpret_cast(b.data()), b.size()); +} + +static std::vector protoBytesToBytes(const std::string &s) { + return std::vector(s.begin(), s.end()); +} + +} // namespace + +struct E2EEManager::Impl { + std::uint64_t room_handle = 0; + E2EEOptions options; + bool enabled() const { return options.enabled; } + void managerSetEnabled(bool enabled) { + options.enabled = enabled; + proto::FfiRequest req; + auto *e2 = req.mutable_e2ee(); + e2->set_room_handle(room_handle); + e2->mutable_manager_set_enabled()->set_enabled(enabled); + FfiClient::instance().sendRequest(req); + } + + void setSharedKey(const std::vector &key, int key_index) { + options.shared_key = key; + proto::FfiRequest req; + auto *e2 = req.mutable_e2ee(); + e2->set_room_handle(room_handle); + auto *set = e2->mutable_set_shared_key(); + set->set_key_index(key_index); + set->set_shared_key(bytesToProtoBytes(key)); + FfiClient::instance().sendRequest(req); + } + + std::vector getSharedKey(int key_index) const { + proto::FfiRequest req; + auto *e2 = req.mutable_e2ee(); + e2->set_room_handle(room_handle); + e2->mutable_get_shared_key()->set_key_index(key_index); + auto resp = FfiClient::instance().sendRequest(req); + const auto &r = resp.e2ee().get_shared_key(); + if (!r.has_key()) { + return {}; + } + return protoBytesToBytes(r.key()); + } + + std::vector ratchetSharedKey(int key_index) { + proto::FfiRequest req; + auto *e2 = req.mutable_e2ee(); + e2->set_room_handle(room_handle); + e2->mutable_ratchet_shared_key()->set_key_index(key_index); + auto resp = FfiClient::instance().sendRequest(req); + const auto &r = resp.e2ee().ratchet_shared_key(); + if (!r.has_new_key()) { + return {}; + } + return protoBytesToBytes(r.new_key()); + } + + void applyOptionsOnceAfterConnect() { + if (!options.enabled) + return; + managerSetEnabled(true); + // If user provided a shared key, install it at key index 0. + if (!options.shared_key.empty()) { + setSharedKey(options.shared_key, /*key_index=*/0); + } + // Note, ratchet_window_size / ratchet_salt / failure_tolerance) must be + // sent as part of connect options (RoomOptions -> E2eeOptions) room.cpp / + // connect request, not here. + } +}; + +E2EEManager::E2EEManager(std::uint64_t room_handle, E2EEOptions options) + : impl_(std::make_unique()) { + impl_->room_handle = room_handle; + impl_->options = std::move(options); + impl_->applyOptionsOnceAfterConnect(); +} + +E2EEManager::~E2EEManager() = default; +E2EEManager::E2EEManager(E2EEManager &&) noexcept = default; +E2EEManager &E2EEManager::operator=(E2EEManager &&) noexcept = default; + +bool E2EEManager::enabled() const { return impl_->enabled(); } + +void E2EEManager::setEnabled(bool enabled) { + impl_->managerSetEnabled(enabled); +} + +void E2EEManager::setSharedKey(const std::vector &key, + int key_index) { + impl_->setSharedKey(key, key_index); +} + +std::vector E2EEManager::exportSharedKey(int key_index) const { + return impl_->getSharedKey(key_index); +} + +std::vector E2EEManager::ratchetSharedKey(int key_index) { + return impl_->ratchetSharedKey(key_index); +} + +} // namespace livekit diff --git a/src/ffi_client.cpp b/src/ffi_client.cpp index 41304f2..c59bc14 100644 --- a/src/ffi_client.cpp +++ b/src/ffi_client.cpp @@ -20,8 +20,9 @@ #include "e2ee.pb.h" #include "ffi.pb.h" #include "ffi_client.h" +#include "livekit/e2ee.h" #include "livekit/ffi_handle.h" -#include "livekit/room.h" // TODO, maybe avoid circular deps by moving RoomOptions to a room_types.h ? +#include "livekit/room.h" #include "livekit/rpc_error.h" #include "livekit/track.h" #include "livekit_ffi.h" @@ -30,6 +31,14 @@ namespace livekit { +namespace { + +std::string bytesToString(const std::vector &b) { + return std::string(reinterpret_cast(b.data()), b.size()); +} + +} // namespace + FfiClient::FfiClient() { livekit_ffi_initialize(&LivekitFfiCallback, false, LIVEKIT_BUILD_FLAVOR, LIVEKIT_BUILD_VERSION_FULL); @@ -160,10 +169,18 @@ FfiClient::connectAsync(const std::string &url, const std::string &token, static_cast(eo.encryption_type)); auto *kp = enc->mutable_key_provider_options(); - kp->set_shared_key(eo.shared_key); - kp->set_ratchet_salt(eo.ratchet_salt); - kp->set_failure_tolerance(eo.failure_tolerance); - kp->set_ratchet_window_size(eo.ratchet_window_size); + if (!eo.shared_key.empty()) { + kp->set_shared_key(bytesToString(eo.shared_key)); + } + if (!eo.ratchet_salt.empty()) { + kp->set_ratchet_salt(bytesToString(eo.ratchet_salt)); + } + if (eo.ratchet_window_size > 0) { + kp->set_ratchet_window_size(eo.ratchet_window_size); + } + if (eo.failure_tolerance != 0) { + kp->set_failure_tolerance(eo.failure_tolerance); + } } // --- RTC configuration (optional) --- diff --git a/src/local_track_publication.cpp b/src/local_track_publication.cpp index d1f3ece..24fc0de 100644 --- a/src/local_track_publication.cpp +++ b/src/local_track_publication.cpp @@ -28,7 +28,8 @@ LocalTrackPublication::LocalTrackPublication( owned.info().name(), fromProto(owned.info().kind()), fromProto(owned.info().source()), owned.info().simulcasted(), owned.info().width(), owned.info().height(), owned.info().mime_type(), - owned.info().muted(), fromProto(owned.info().encryption_type()), + owned.info().muted(), + static_cast(owned.info().encryption_type()), convertAudioFeatures(owned.info().audio_features())) {} std::shared_ptr LocalTrackPublication::track() const noexcept { diff --git a/src/remote_track_publication.cpp b/src/remote_track_publication.cpp index 3616af6..10cf962 100644 --- a/src/remote_track_publication.cpp +++ b/src/remote_track_publication.cpp @@ -30,7 +30,8 @@ RemoteTrackPublication::RemoteTrackPublication( owned.info().name(), fromProto(owned.info().kind()), fromProto(owned.info().source()), owned.info().simulcasted(), owned.info().width(), owned.info().height(), owned.info().mime_type(), - owned.info().muted(), fromProto(owned.info().encryption_type()), + owned.info().muted(), + static_cast(owned.info().encryption_type()), convertAudioFeatures(owned.info().audio_features())) {} std::shared_ptr RemoteTrackPublication::track() const noexcept { diff --git a/src/room.cpp b/src/room.cpp index 538156f..267b89e 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -17,6 +17,7 @@ #include "livekit/room.h" #include "livekit/audio_stream.h" +#include "livekit/e2ee.h" #include "livekit/local_participant.h" #include "livekit/local_track_publication.h" #include "livekit/remote_audio_track.h" @@ -134,6 +135,15 @@ bool Room::Connect(const std::string &url, const std::string &token, } } + // Setup e2eeManager + if (options.e2ee) { + std::cout << "creating E2eeManager " << std::endl; + e2ee_manager_ = std::unique_ptr( + new E2EEManager(room_handle_->get(), options.e2ee.value())); + } else { + e2ee_manager_.reset(); + } + return true; } catch (const std::exception &e) { // On error, remove the listener and rethrow @@ -872,6 +882,7 @@ void Room::OnEvent(const FfiEvent &event) { case proto::RoomEvent::kE2EeStateChanged: { E2eeStateChangedEvent ev; { + std::cerr << "e2ee_state_changed for participant: " << std::endl; std::lock_guard guard(lock_); const auto &es = re.e2ee_state_changed(); const std::string &identity = es.participant_identity(); diff --git a/src/track_proto_converter.cpp b/src/track_proto_converter.cpp index f84658c..9c8358f 100644 --- a/src/track_proto_converter.cpp +++ b/src/track_proto_converter.cpp @@ -141,18 +141,4 @@ ParticipantKind fromProto(proto::ParticipantKind in) { } } -EncryptionType fromProto(proto::EncryptionType in) { - switch (in) { - case proto::NONE: - return EncryptionType::NONE; - case proto::GCM: - return EncryptionType::GCM; - case proto::CUSTOM: - return EncryptionType::CUSTOM; - default: - // Defensive fallback - return EncryptionType::NONE; - } -} - } // namespace livekit \ No newline at end of file diff --git a/src/track_proto_converter.h b/src/track_proto_converter.h index a1192a1..ef4180c 100644 --- a/src/track_proto_converter.h +++ b/src/track_proto_converter.h @@ -35,7 +35,4 @@ proto::ParticipantTrackPermission toProto(const ParticipantTrackPermission &in); ParticipantTrackPermission fromProto(const proto::ParticipantTrackPermission &in); -// Track Publication Utils. -EncryptionType fromProto(proto::EncryptionType in); - } // namespace livekit \ No newline at end of file diff --git a/src/video_utils.cpp b/src/video_utils.cpp index d75b775..247f39e 100644 --- a/src/video_utils.cpp +++ b/src/video_utils.cpp @@ -1,3 +1,19 @@ +/* + * Copyright 2025 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 "livekit/video_frame.h" #include From be4141eb32e357d80a72b04b60088fb7fb5fa75f Mon Sep 17 00:00:00 2001 From: shijing xian Date: Thu, 18 Dec 2025 19:39:29 -0800 Subject: [PATCH 2/2] update the code to adapt the style / apis like other sdk --- examples/simple_room/main.cpp | 22 +-- include/livekit/e2ee.h | 281 ++++++++++++++++++++-------------- include/livekit/room.h | 6 +- src/e2ee.cpp | 216 ++++++++++++++++---------- src/ffi_client.cpp | 46 ++++-- src/room.cpp | 4 +- 6 files changed, 353 insertions(+), 222 deletions(-) diff --git a/examples/simple_room/main.cpp b/examples/simple_room/main.cpp index 05386cf..b4bfcce 100644 --- a/examples/simple_room/main.cpp +++ b/examples/simple_room/main.cpp @@ -278,15 +278,15 @@ int main(int argc, char *argv[]) { options.dynacast = false; if (enable_e2ee) { - livekit::E2EEOptions e2ee; - e2ee.encryption_type = livekit::EncryptionType::GCM; + livekit::E2EEOptions encryption; + encryption.encryption_type = livekit::EncryptionType::GCM; // Optional shared key: if empty, we enable E2EE without setting a shared // key. (Advanced use: keys can be set/ratcheted later via // E2EEManager/KeyProvider.) if (!e2ee_key.empty()) { - e2ee.shared_key = toBytes(e2ee_key); + encryption.key_provider_options.shared_key = toBytes(e2ee_key); } - options.e2ee = e2ee; + options.encryption = encryption; if (!e2ee_key.empty()) { std::cout << "[E2EE] enabled : (shared key length=" << e2ee_key.size() << ")\n"; @@ -385,19 +385,23 @@ int main(int argc, char *argv[]) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } - // Shutdown the audio thread. + // Shutdown the audio / video capture threads. media.stopMic(); + media.stopCamera(); + + // Drain any queued tasks that might still try to update the renderer / + // speaker + MainThreadDispatcher::update(); + + // Must be cleaned up before FfiClient::instance().shutdown(); + room->setDelegate(nullptr); // Clean up the audio track publishment room->localParticipant()->unpublishTrack(audioPub->sid()); - media.stopCamera(); - // Clean up the video track publishment room->localParticipant()->unpublishTrack(videoPub->sid()); - // Must be cleaned up before FfiClient::instance().shutdown(); - room->setDelegate(nullptr); room.reset(); FfiClient::instance().shutdown(); diff --git a/include/livekit/e2ee.h b/include/livekit/e2ee.h index 9e4d479..cf14265 100644 --- a/include/livekit/e2ee.h +++ b/include/livekit/e2ee.h @@ -24,143 +24,198 @@ namespace livekit { -/* Encryption algorithm type used by the underlying stack. Keep this aligned - * with your proto enum. */ -enum class EncryptionType { NONE = 0, GCM = 1, CUSTOM = 2 }; - -/// End-to-end encryption (E2EE) configuration. -/// -/// When enabled, media frames are encrypted before being sent and -/// decrypted on receipt. Keys may be provided up-front (shared-key mode) -/// or supplied by other mechanisms supported by the underlying runtime. -struct E2EEOptions { - bool enabled; - /// Encryption algorithm to use. - /// - /// GCM is the default and recommended option. - EncryptionType encryption_type = EncryptionType::GCM; +/* Encryption algorithm type used by the underlying stack. + * Keep this aligned with your proto enum values. */ +enum class EncryptionType { + NONE = 0, + GCM = 1, + CUSTOM = 2, +}; - /// Shared static key for shared-key E2EE. +/* Defaults (match other SDKs / Python defaults). */ +inline constexpr const char *kDefaultRatchetSalt = "LKFrameEncryptionKey"; +inline constexpr int kDefaultRatchetWindowSize = 16; +inline constexpr int kDefaultFailureTolerance = -1; + +/** + * Options for configuring the key provider used by E2EE. + * + * Notes: + * - `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. + */ +struct EncryptionKeyProviderOptions { + /// Shared static key for "shared-key E2EE" (optional). /// - /// When using shared-key E2EE, this key must be provided and must be - /// identical (byte-for-byte) for all participants in the room in order - /// to successfully encrypt and decrypt media. + /// If set, it must be identical (byte-for-byte) across all participants + /// that are expected to decrypt each other’s media. /// - /// If this is empty while E2EE is enabled, media cannot be decrypted - /// and participants will not be able to communicate (e.g. black video / - /// silent audio). - std::vector shared_key; + /// If not set, keys must be provided out-of-band later (e.g. via KeyProvider + /// APIs). + std::optional> shared_key; - /// Optional salt used when deriving ratcheted encryption keys. + /// Salt used when deriving ratcheted keys. /// - /// If empty, a default salt is used by the underlying implementation. - std::vector ratchet_salt; + /// If empty, the underlying implementation default is used. + std::vector ratchet_salt = std::vector( + kDefaultRatchetSalt, kDefaultRatchetSalt + std::char_traits::length( + kDefaultRatchetSalt)); - /// Optional ratchet window size. - /// /// Controls how many previous keys are retained during ratcheting. - /// A value of 0 indicates that the implementation default is used. - int ratchet_window_size = 0; + int ratchet_window_size = kDefaultRatchetWindowSize; - /// Optional failure tolerance for ratcheting. - /// - /// Specifies how many consecutive ratcheting failures are tolerated - /// before encryption errors are reported. A value of 0 indicates - /// that the implementation default is used. - int failure_tolerance = 0; + /// Number of tolerated ratchet failures before reporting encryption errors. + int failure_tolerance = kDefaultFailureTolerance; +}; + +/** + * End-to-end encryption (E2EE) configuration for a room. + * + * Provide this in RoomOptions to initialize E2EE support. + * + * IMPORTANT: + * - Providing E2EEOptions means "E2EE support is configured for this room". + * - Whether encryption is actively applied can still be toggled at runtime via + * E2EEManager::setEnabled(). + * - A room can be configured for E2EE even if no shared key is provided yet. + * In that case, the app must supply keys later via KeyProvider (shared-key or + * per-participant). + */ +struct E2EEOptions { + EncryptionKeyProviderOptions key_provider_options{}; + EncryptionType encryption_type = EncryptionType::GCM; // default & recommended }; +/** + * E2EE manager for a connected room. + * + * Lifetime: + * - Owned by Room. Applications must not construct E2EEManager directly. + * + * Enablement model: + * - If the Room was created with `RoomOptions.e2ee` set, the room will expose + * a non-null E2EEManager via Room::E2eeManager(). + * - If the Room was created without E2EE options, Room::E2eeManager() may be + * null. + * + * Key model: + * - Keys are managed via KeyProvider (shared-key or per-participant). + * - Providing a shared key up-front is convenient for shared-key E2EE, but is + * not required by the API shape (keys may be supplied later). + */ class E2EEManager { public: - virtual ~E2EEManager(); - + /** If your application requires key rotation during the lifetime of a single + * room or unique keys per participant (such as when implementing the MEGOLM + * or MLS protocol), you' can do it via key provider and frame cryptor. refer + * https://docs.livekit.io/home/client/encryption/#custom-key-provider doe + * details + * */ + class KeyProvider { + public: + ~KeyProvider() = default; + + KeyProvider(const KeyProvider &) = delete; + KeyProvider &operator=(const KeyProvider &) = delete; + KeyProvider(KeyProvider &&) noexcept = default; + KeyProvider &operator=(KeyProvider &&) noexcept = default; + + /// Returns the options used to initialize this KeyProvider. + const EncryptionKeyProviderOptions &options() const; + + /// Sets the shared key for the given key slot. + void setSharedKey(const std::vector &key, int key_index = 0); + + /// Exports the shared key for a given key slot. + std::vector exportSharedKey(int key_index = 0) const; + + /// Ratchets the shared key at key_index and returns the newly derived key. + std::vector ratchetSharedKey(int key_index = 0); + + /// Sets a key for a specific participant identity. + void setKey(const std::string &participant_identity, + const std::vector &key, int key_index = 0); + + /// Exports a participant-specific key. + std::vector exportKey(const std::string &participant_identity, + int key_index = 0) const; + + /// Ratchets a participant-specific key and returns the new key. + std::vector + ratchetKey(const std::string &participant_identity, int key_index = 0); + + private: + friend class E2EEManager; + KeyProvider(std::uint64_t room_handle, + EncryptionKeyProviderOptions options); + std::uint64_t room_handle_{0}; + EncryptionKeyProviderOptions options_; + }; + + class FrameCryptor { + public: + FrameCryptor(std::uint64_t room_handle, std::string participant_identity, + int key_index, bool enabled); + ~FrameCryptor() = default; + FrameCryptor(const FrameCryptor &) = delete; + FrameCryptor &operator=(const FrameCryptor &) = delete; + FrameCryptor(FrameCryptor &&) noexcept = default; + FrameCryptor &operator=(FrameCryptor &&) noexcept = default; + + const std::string &participantIdentity() const; + int keyIndex() const; + bool enabled() const; + + /// Enables or disables frame encryption/decryption for this participant. + void setEnabled(bool enabled); + + /// Sets the active key index for this participant cryptor. + void setKeyIndex(int key_index); + + private: + std::uint64_t room_handle_{0}; + bool enabled_{false}; + std::string participant_identity_; + int key_index_{0}; + }; + + ~E2EEManager() = default; E2EEManager(const E2EEManager &) = delete; E2EEManager &operator=(const E2EEManager &) = delete; + E2EEManager(E2EEManager &&) noexcept = delete; + E2EEManager &operator=(E2EEManager &&) noexcept = delete; - E2EEManager(E2EEManager &&) noexcept; - E2EEManager &operator=(E2EEManager &&) noexcept; - - /** - * Returns whether end-to-end encryption (E2EE) is currently enabled. - * - * This reflects the runtime encryption state for media tracks - * associated with the room. - */ + /// Returns whether E2EE is currently enabled for this room at runtime. bool enabled() const; - /** - * Enable or disable end-to-end encryption at runtime. - * - * Disabling E2EE will stop encrypting outgoing media and stop - * decrypting incoming media. - * - * NOTE: - * - All participants must agree on E2EE state and keys in order - * to successfully exchange media. - * - Disabling E2EE while other participants still have it enabled - * will result in media being undecodable. - */ + /// Enable or disable E2EE at runtime. + /// + /// NOTE: + /// - Enabling E2EE without having compatible keys set across participants + /// will result in undecodable media (black video / silent audio). void setEnabled(bool enabled); - /** - * Set or replace the shared encryption key at the given key index. - * - * This is typically used for: - * - Manual key rotation - * - * The provided key MUST be identical across all participants - * using shared-key E2EE, otherwise media decryption will fail. - * - * @param key Raw key bytes - * @param key_index Index of the key to set (default: 0) - */ - void setSharedKey(const std::vector &key, int key_index = 0); - - /** - * Export the currently active shared key at the given key index. - * - * This API is primarily intended for debugging, verification, - * or diagnostics. Applications should avoid exporting keys - * unless absolutely necessary. - * - * @param key_index Index of the key to export (default: 0) - * @return Raw key bytes - */ - std::vector exportSharedKey(int key_index = 0) const; - - /** - * Ratchet (derive) a new shared key at the given key index. - * - * This advances the key forward and returns the newly derived key. - * All participants must ratchet keys in the same order to remain - * in sync. - * - * @param key_index Index of the key to ratchet (default: 0) - * @return Newly derived key bytes - */ - std::vector ratchetSharedKey(int key_index = 0); + /// Returns the key provider if E2EE was configured for the room; otherwise + /// nullptr. + KeyProvider *keyProvider(); + const KeyProvider *keyProvider() const; + + /// Retrieves the current list of frame cryptors from the underlying runtime. + std::vector frameCryptors() const; protected: - /* - * Construct an E2EE manager for a connected room. - * - * This constructor are intended for internal use by room. - * Applications should NOT create their own E2EEManager instances. - * - * After successfully connecting to a room with E2EE enabled, - * obtain the E2EE manager via the Room: - * - * auto e2ee_manager = room->e2eeManager(); - * - * The Room owns and manages the lifetime of the E2EEManager and ensures - * it is correctly wired to the underlying room handle and track lifecycle. - */ - explicit E2EEManager(std::uint64_t room_handle, E2EEOptions config); + /// Internal constructor used by Room when E2EEOptions are provided. + explicit E2EEManager(std::uint64_t room_handle, const E2EEOptions &options); friend class Room; private: - struct Impl; - std::unique_ptr impl_; + std::uint64_t room_handle_{0}; + bool enabled_{false}; + E2EEOptions options_; + KeyProvider key_provider_; }; } // namespace livekit diff --git a/include/livekit/room.h b/include/livekit/room.h index 0bf2aef..a0d3d3d 100644 --- a/include/livekit/room.h +++ b/include/livekit/room.h @@ -73,11 +73,11 @@ struct RoomOptions { // Enable dynacast (server sends optimal layers depending on subscribers). bool dynacast = false; - // Optional end-to-end encryption settings. - std::optional e2ee; - // Optional WebRTC configuration (ICE policy, servers, etc.) std::optional rtc_config; + + // Optional end-to-end encryption settings. + std::optional encryption; }; /// Represents a LiveKit room session. diff --git a/src/e2ee.cpp b/src/e2ee.cpp index e27b603..dbadb50 100644 --- a/src/e2ee.cpp +++ b/src/e2ee.cpp @@ -22,113 +22,169 @@ #include "e2ee.pb.h" #include "ffi.pb.h" #include "ffi_client.h" +#include "livekit/ffi_handle.h" namespace livekit { namespace { -std::string bytesToProtoBytes(const std::vector &b) { - return std::string(reinterpret_cast(b.data()), b.size()); +std::string bytesToString(const std::vector &v) { + return std::string(reinterpret_cast(v.data()), v.size()); } -static std::vector protoBytesToBytes(const std::string &s) { +std::vector stringToBytes(const std::string &s) { return std::vector(s.begin(), s.end()); } } // namespace -struct E2EEManager::Impl { - std::uint64_t room_handle = 0; - E2EEOptions options; - bool enabled() const { return options.enabled; } - void managerSetEnabled(bool enabled) { - options.enabled = enabled; - proto::FfiRequest req; - auto *e2 = req.mutable_e2ee(); - e2->set_room_handle(room_handle); - e2->mutable_manager_set_enabled()->set_enabled(enabled); - FfiClient::instance().sendRequest(req); - } +// ============================================================================ +// KeyProvider +// ============================================================================ - void setSharedKey(const std::vector &key, int key_index) { - options.shared_key = key; - proto::FfiRequest req; - auto *e2 = req.mutable_e2ee(); - e2->set_room_handle(room_handle); - auto *set = e2->mutable_set_shared_key(); - set->set_key_index(key_index); - set->set_shared_key(bytesToProtoBytes(key)); - FfiClient::instance().sendRequest(req); - } +E2EEManager::KeyProvider::KeyProvider(std::uint64_t room_handle, + EncryptionKeyProviderOptions options) + : room_handle_(room_handle), options_(std::move(options)) {} - std::vector getSharedKey(int key_index) const { - proto::FfiRequest req; - auto *e2 = req.mutable_e2ee(); - e2->set_room_handle(room_handle); - e2->mutable_get_shared_key()->set_key_index(key_index); - auto resp = FfiClient::instance().sendRequest(req); - const auto &r = resp.e2ee().get_shared_key(); - if (!r.has_key()) { - return {}; - } - return protoBytesToBytes(r.key()); - } +const EncryptionKeyProviderOptions &E2EEManager::KeyProvider::options() const { + return options_; +} - std::vector ratchetSharedKey(int key_index) { - proto::FfiRequest req; - auto *e2 = req.mutable_e2ee(); - e2->set_room_handle(room_handle); - e2->mutable_ratchet_shared_key()->set_key_index(key_index); - auto resp = FfiClient::instance().sendRequest(req); - const auto &r = resp.e2ee().ratchet_shared_key(); - if (!r.has_new_key()) { - return {}; - } - return protoBytesToBytes(r.new_key()); - } +void E2EEManager::KeyProvider::setSharedKey( + const std::vector &key, int key_index) { + proto::FfiRequest req; + req.mutable_e2ee()->set_room_handle(room_handle_); + req.mutable_e2ee()->mutable_set_shared_key()->set_key_index(key_index); + req.mutable_e2ee()->mutable_set_shared_key()->set_shared_key( + bytesToString(key)); + FfiClient::instance().sendRequest(req); +} - void applyOptionsOnceAfterConnect() { - if (!options.enabled) - return; - managerSetEnabled(true); - // If user provided a shared key, install it at key index 0. - if (!options.shared_key.empty()) { - setSharedKey(options.shared_key, /*key_index=*/0); - } - // Note, ratchet_window_size / ratchet_salt / failure_tolerance) must be - // sent as part of connect options (RoomOptions -> E2eeOptions) room.cpp / - // connect request, not here. - } -}; +std::vector +E2EEManager::KeyProvider::exportSharedKey(int key_index) const { + proto::FfiRequest req; + req.mutable_e2ee()->set_room_handle(room_handle_); + req.mutable_e2ee()->mutable_get_shared_key()->set_key_index(key_index); + auto resp = FfiClient::instance().sendRequest(req); + return stringToBytes(resp.e2ee().get_shared_key().key()); +} -E2EEManager::E2EEManager(std::uint64_t room_handle, E2EEOptions options) - : impl_(std::make_unique()) { - impl_->room_handle = room_handle; - impl_->options = std::move(options); - impl_->applyOptionsOnceAfterConnect(); +std::vector +E2EEManager::KeyProvider::ratchetSharedKey(int key_index) { + proto::FfiRequest req; + req.mutable_e2ee()->set_room_handle(room_handle_); + req.mutable_e2ee()->mutable_ratchet_shared_key()->set_key_index(key_index); + auto resp = FfiClient::instance().sendRequest(req); + return stringToBytes(resp.e2ee().ratchet_shared_key().new_key()); } -E2EEManager::~E2EEManager() = default; -E2EEManager::E2EEManager(E2EEManager &&) noexcept = default; -E2EEManager &E2EEManager::operator=(E2EEManager &&) noexcept = default; +void E2EEManager::KeyProvider::setKey(const std::string &participant_identity, + const std::vector &key, + int key_index) { + proto::FfiRequest req; + req.mutable_e2ee()->set_room_handle(room_handle_); + req.mutable_e2ee()->mutable_set_key()->set_participant_identity( + participant_identity); + req.mutable_e2ee()->mutable_set_key()->set_key_index(key_index); + req.mutable_e2ee()->mutable_set_key()->set_key(bytesToString(key)); + FfiClient::instance().sendRequest(req); +} -bool E2EEManager::enabled() const { return impl_->enabled(); } +std::vector +E2EEManager::KeyProvider::exportKey(const std::string &participant_identity, + int key_index) const { + proto::FfiRequest req; + req.mutable_e2ee()->set_room_handle(room_handle_); + req.mutable_e2ee()->mutable_get_key()->set_participant_identity( + participant_identity); + req.mutable_e2ee()->mutable_get_key()->set_key_index(key_index); + auto resp = FfiClient::instance().sendRequest(req); + return stringToBytes(resp.e2ee().get_key().key()); +} -void E2EEManager::setEnabled(bool enabled) { - impl_->managerSetEnabled(enabled); +std::vector +E2EEManager::KeyProvider::ratchetKey(const std::string &participant_identity, + int key_index) { + proto::FfiRequest req; + req.mutable_e2ee()->set_room_handle(room_handle_); + req.mutable_e2ee()->mutable_ratchet_key()->set_participant_identity( + participant_identity); + req.mutable_e2ee()->mutable_ratchet_key()->set_key_index(key_index); + auto resp = FfiClient::instance().sendRequest(req); + return stringToBytes(resp.e2ee().ratchet_key().new_key()); } -void E2EEManager::setSharedKey(const std::vector &key, - int key_index) { - impl_->setSharedKey(key, key_index); +// ============================================================================ +// FrameCryptor +// ============================================================================ + +E2EEManager::FrameCryptor::FrameCryptor(std::uint64_t room_handle, + std::string participant_identity, + int key_index, bool enabled) + : room_handle_(room_handle), enabled_(enabled), + participant_identity_(std::move(participant_identity)), + key_index_(key_index) {} + +const std::string &E2EEManager::FrameCryptor::participantIdentity() const { + return participant_identity_; +} +int E2EEManager::FrameCryptor::keyIndex() const { return key_index_; } +bool E2EEManager::FrameCryptor::enabled() const { return enabled_; } + +void E2EEManager::FrameCryptor::setEnabled(bool enabled) { + proto::FfiRequest req; + req.mutable_e2ee()->set_room_handle(room_handle_); + req.mutable_e2ee()->mutable_cryptor_set_enabled()->set_participant_identity( + participant_identity_); + req.mutable_e2ee()->mutable_cryptor_set_enabled()->set_enabled(enabled); + FfiClient::instance().sendRequest(req); } -std::vector E2EEManager::exportSharedKey(int key_index) const { - return impl_->getSharedKey(key_index); +void E2EEManager::FrameCryptor::setKeyIndex(int key_index) { + proto::FfiRequest req; + req.mutable_e2ee()->set_room_handle(room_handle_); + req.mutable_e2ee()->mutable_cryptor_set_key_index()->set_participant_identity( + participant_identity_); + req.mutable_e2ee()->mutable_cryptor_set_key_index()->set_key_index(key_index); + FfiClient::instance().sendRequest(req); } -std::vector E2EEManager::ratchetSharedKey(int key_index) { - return impl_->ratchetSharedKey(key_index); +// ============================================================================ +// E2EEManager +// ============================================================================ + +E2EEManager::E2EEManager(std::uint64_t room_handle, const E2EEOptions &options) + : room_handle_(room_handle), + enabled_(true), // or false, depending on your desired default behavior + options_(options), + key_provider_(room_handle, options.key_provider_options) {} + +bool E2EEManager::enabled() const { return enabled_; } + +void E2EEManager::setEnabled(bool enabled) { + proto::FfiRequest req; + req.mutable_e2ee()->set_room_handle(room_handle_); + req.mutable_e2ee()->mutable_manager_set_enabled()->set_enabled(enabled); + FfiClient::instance().sendRequest(req); +} + +E2EEManager::KeyProvider *E2EEManager::keyProvider() { return &key_provider_; } +const E2EEManager::KeyProvider *E2EEManager::keyProvider() const { + return &key_provider_; +} + +std::vector E2EEManager::frameCryptors() const { + proto::FfiRequest req; + req.mutable_e2ee()->set_room_handle(room_handle_); + auto resp = FfiClient::instance().sendRequest(req); + std::vector out; + const auto &list = resp.e2ee().manager_get_frame_cryptors().frame_cryptors(); + out.reserve(static_cast(list.size())); + for (const auto &fc : list) { + out.emplace_back(room_handle_, fc.participant_identity(), fc.key_index(), + fc.enabled()); + } + return out; } } // namespace livekit diff --git a/src/ffi_client.cpp b/src/ffi_client.cpp index c59bc14..6892fce 100644 --- a/src/ffi_client.cpp +++ b/src/ffi_client.cpp @@ -158,28 +158,44 @@ FfiClient::connectAsync(const std::string &url, const std::string &token, opts->set_dynacast(options.dynacast); std::cout << "connectAsync " << std::endl; // --- E2EE / encryption (optional) --- - if (options.e2ee.has_value()) { + if (options.encryption.has_value()) { std::cout << "connectAsync e2ee " << std::endl; - const E2EEOptions &eo = *options.e2ee; + const E2EEOptions &e2ee = *options.encryption; + const auto &kpo = e2ee.key_provider_options; - // Use the non-deprecated encryption field auto *enc = opts->mutable_encryption(); - enc->set_encryption_type( - static_cast(eo.encryption_type)); - + static_cast(e2ee.encryption_type)); auto *kp = enc->mutable_key_provider_options(); - if (!eo.shared_key.empty()) { - kp->set_shared_key(bytesToString(eo.shared_key)); + // 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(); } - if (!eo.ratchet_salt.empty()) { - kp->set_ratchet_salt(bytesToString(eo.ratchet_salt)); + // 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(); } - if (eo.ratchet_window_size > 0) { - kp->set_ratchet_window_size(eo.ratchet_window_size); + // 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 (eo.failure_tolerance != 0) { - kp->set_failure_tolerance(eo.failure_tolerance); + if (kpo.failure_tolerance != kDefaultFailureTolerance) { + kp->set_failure_tolerance(kpo.failure_tolerance); + } else { + kp->clear_failure_tolerance(); } } @@ -229,7 +245,7 @@ FfiClient::connectAsync(const std::string &url, const std::string &token, [](const proto::FfiEvent &event, std::promise &pr) { const auto &connectCb = event.connect(); - + std::cout << "connectAsync e2ee done " << std::endl; if (!connectCb.error().empty()) { pr.set_exception( std::make_exception_ptr(std::runtime_error(connectCb.error()))); diff --git a/src/room.cpp b/src/room.cpp index 267b89e..a14a4d9 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -136,10 +136,10 @@ bool Room::Connect(const std::string &url, const std::string &token, } // Setup e2eeManager - if (options.e2ee) { + if (options.encryption) { std::cout << "creating E2eeManager " << std::endl; e2ee_manager_ = std::unique_ptr( - new E2EEManager(room_handle_->get(), options.e2ee.value())); + new E2EEManager(room_handle_->get(), options.encryption.value())); } else { e2ee_manager_.reset(); }