diff --git a/CMakeLists.txt b/CMakeLists.txt index 45cce83..062542a 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 @@ -186,6 +187,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/local_audio_track.cpp 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..cbd3bb0 100644 --- a/examples/simple_data_stream/main.cpp +++ b/examples/simple_data_stream/main.cpp @@ -46,10 +46,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 +209,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 +222,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 +235,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 +245,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 +258,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 +274,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..3299dfb 100644 --- a/examples/simple_room/main.cpp +++ b/examples/simple_room/main.cpp @@ -43,19 +43,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 +76,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 +92,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 +102,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 +137,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 +235,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 +270,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 +304,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 +335,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 +363,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 +390,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..1190001 100644 --- a/examples/simple_rpc/main.cpp +++ b/examples/simple_rpc/main.cpp @@ -67,12 +67,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 +82,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 +96,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 +232,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 +308,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 +326,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 +345,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 +353,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 +373,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 +401,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 +409,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 +452,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 +465,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 +474,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 +518,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 +538,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 318baaf..d08dae0 100644 --- a/include/livekit/room.h +++ b/include/livekit/room.h @@ -18,6 +18,7 @@ #define LIVEKIT_ROOM_H #include "livekit/data_stream.h" +#include "livekit/e2ee.h" #include "livekit/ffi_client.h" #include "livekit/ffi_handle.h" #include "livekit/room_event_types.h" @@ -32,27 +33,10 @@ namespace proto { class FfiEvent; } +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..395e2ef --- /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 "livekit/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 ee7bba7..4c90441 100644 --- a/src/ffi_client.cpp +++ b/src/ffi_client.cpp @@ -30,6 +30,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 +168,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 6a7f8d0..de2fcad 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 e3d93b2..4fa89d8 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/ffi_client.h" #include "livekit/local_participant.h" #include "livekit/local_track_publication.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 746e4ef..5abfdcd 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