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..b4bfcce 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 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()) { + encryption.key_provider_options.shared_key = toBytes(e2ee_key); + } + options.encryption = encryption; + 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" @@ -337,16 +385,24 @@ 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(); - // Clean up the audio track publishment - room.localParticipant()->unpublishTrack(audioPub->sid()); + // Drain any queued tasks that might still try to update the renderer / + // speaker + MainThreadDispatcher::update(); - media.stopCamera(); + // Must be cleaned up before FfiClient::instance().shutdown(); + room->setDelegate(nullptr); + + // Clean up the audio track publishment + room->localParticipant()->unpublishTrack(audioPub->sid()); // Clean up the video track publishment - room.localParticipant()->unpublishTrack(videoPub->sid()); + room->localParticipant()->unpublishTrack(videoPub->sid()); + + 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..cf14265 --- /dev/null +++ b/include/livekit/e2ee.h @@ -0,0 +1,221 @@ +/* + * 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 values. */ +enum class EncryptionType { + NONE = 0, + GCM = 1, + CUSTOM = 2, +}; + +/* 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). + /// + /// If set, it must be identical (byte-for-byte) across all participants + /// that are expected to decrypt each other’s media. + /// + /// If not set, keys must be provided out-of-band later (e.g. via KeyProvider + /// APIs). + std::optional> shared_key; + + /// Salt used when deriving ratcheted keys. + /// + /// If empty, the underlying implementation default is used. + std::vector ratchet_salt = std::vector( + kDefaultRatchetSalt, kDefaultRatchetSalt + std::char_traits::length( + kDefaultRatchetSalt)); + + /// Controls how many previous keys are retained during ratcheting. + int ratchet_window_size = kDefaultRatchetWindowSize; + + /// 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: + /** 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; + + /// Returns whether E2EE is currently enabled for this room at runtime. + bool enabled() const; + + /// 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); + + /// 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: + /// Internal constructor used by Room when E2EEOptions are provided. + explicit E2EEManager(std::uint64_t room_handle, const E2EEOptions &options); + friend class Room; + +private: + std::uint64_t room_handle_{0}; + bool enabled_{false}; + E2EEOptions options_; + KeyProvider key_provider_; +}; + +} // 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..a0d3d3d 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"). @@ -89,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. @@ -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..dbadb50 --- /dev/null +++ b/src/e2ee.cpp @@ -0,0 +1,190 @@ +/* + * 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" +#include "livekit/ffi_handle.h" + +namespace livekit { + +namespace { + +std::string bytesToString(const std::vector &v) { + return std::string(reinterpret_cast(v.data()), v.size()); +} + +std::vector stringToBytes(const std::string &s) { + return std::vector(s.begin(), s.end()); +} + +} // namespace + +// ============================================================================ +// KeyProvider +// ============================================================================ + +E2EEManager::KeyProvider::KeyProvider(std::uint64_t room_handle, + EncryptionKeyProviderOptions options) + : room_handle_(room_handle), options_(std::move(options)) {} + +const EncryptionKeyProviderOptions &E2EEManager::KeyProvider::options() const { + return options_; +} + +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); +} + +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()); +} + +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()); +} + +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); +} + +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()); +} + +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()); +} + +// ============================================================================ +// 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); +} + +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); +} + +// ============================================================================ +// 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 41304f2..6892fce 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); @@ -149,21 +158,45 @@ 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(); - 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); + // 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(); + } } // --- RTC configuration (optional) --- @@ -212,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/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..a14a4d9 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.encryption) { + std::cout << "creating E2eeManager " << std::endl; + e2ee_manager_ = std::unique_ptr( + new E2EEManager(room_handle_->get(), options.encryption.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