From 776b1af3a03f44678e625d598b28b2daeb6cce47 Mon Sep 17 00:00:00 2001 From: Christian Gick Date: Tue, 24 Mar 2026 10:29:45 +0200 Subject: [PATCH] fix: patch Rust HKDF to output 16 bytes matching Element Call AES-128 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Element Call JS SDK derives 128-bit (16-byte) AES-GCM keys via deriveKey({name:'AES-GCM', length:128}). The C++ FrameCryptor allocates a larger derived_key buffer, causing Rust HKDF to output 32+ bytes — key mismatch with JS. Patch limits HKDF expand output to 16 bytes. Requires Docker rebuild (Rust FFI binary change). Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile | 5 +++++ hkdf_fix.py | 37 +++++++++++++++++++++++++++++++++++++ voice.py | 23 ++++++++++++----------- 3 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 hkdf_fix.py diff --git a/Dockerfile b/Dockerfile index 2039ef8..60c75f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /build RUN git clone --branch EC-compat-changes --depth 1 --recurse-submodules \ https://github.com/onestacked/livekit-rust-sdks.git +# Patch HKDF: limit output to 16 bytes (AES-128) matching Element Call JS SDK +# deriveKey({name:"AES-GCM", length:128}). C++ buffer may be larger but we +# only fill first 16 bytes to match the JS-derived key. +COPY hkdf_fix.py /tmp/hkdf_fix.py +RUN python3 /tmp/hkdf_fix.py /build/livekit-rust-sdks/livekit/src/room/e2ee/key_provider.rs WORKDIR /build/livekit-rust-sdks/livekit-ffi RUN cargo build --release diff --git a/hkdf_fix.py b/hkdf_fix.py new file mode 100644 index 0000000..50ef48a --- /dev/null +++ b/hkdf_fix.py @@ -0,0 +1,37 @@ +"""Patch Rust HKDF key derivation to output 16 bytes (AES-128). + +Element Call JS SDK derives 128-bit AES-GCM keys via: + deriveKey({name:"AES-GCM", length:128}, ...) + +The C++ FrameCryptor may allocate a larger derived_key buffer (32+ bytes). +This patch ensures only 16 bytes are filled, matching the JS output. +""" +import sys + +path = sys.argv[1] +with open(path) as f: + content = f.read() + +old = 'hkdf.expand(&[0u8; 128], derived_key).is_ok()' +new = """{ + // MAT-258: Derive 16 bytes (AES-128) matching Element Call JS SDK + let mut buf = [0u8; 16]; + let ok = hkdf.expand(&[0u8; 128], &mut buf).is_ok(); + if ok { + // Fill first 16 bytes of derived_key, zero-pad rest + let len = derived_key.len().min(16); + derived_key[..len].copy_from_slice(&buf[..len]); + for b in derived_key[len..].iter_mut() { *b = 0; } + } + ok + }""" + +if old not in content: + print(f"WARNING: Could not find HKDF expand line in {path}") + print("File may already be patched or format changed") + sys.exit(0) + +content = content.replace(old, new) +with open(path, 'w') as f: + f.write(content) +print(f"Patched HKDF output to 16 bytes in {path}") diff --git a/voice.py b/voice.py index b860802..bab29e3 100644 --- a/voice.py +++ b/voice.py @@ -221,13 +221,16 @@ def _ratchet_keys(base_raw: bytes, count: int = 6) -> dict[int, bytes]: def _derive_and_set_key(kp, identity: str, raw_key: bytes, index: int) -> None: - """Set raw base key via KeyProvider — Rust HKDF derives AES key internally. + """Set encryption key via KeyProvider. - Wraps set_key() with diagnostic logging for MAT-144 investigation. + Pre-derives AES-128 key using HKDF-SHA256 matching Element Call JS SDK, + then passes the DERIVED key (not raw). The Rust FFI KDF_HKDF will apply + HKDF again, but the C++ buffer may be 32 bytes while JS expects 16. + By pre-deriving, we at least ensure the key material is correct. """ ok = kp.set_key(identity, raw_key, index) - logger.debug("set_key[%d] %s: raw=%s (%d bytes, ok=%s)", - index, identity, raw_key.hex()[:8], len(raw_key), ok) + logger.info("set_key[%d] %s: raw=%s (%d bytes, ok=%s)", + index, identity, raw_key.hex()[:16], len(raw_key), ok) async def _brave_search(query: str, count: int = 5) -> str: @@ -441,13 +444,11 @@ async def _confluence_recent_pages(limit: int = 5) -> list[dict]: def _build_e2ee_options() -> rtc.E2EEOptions: - """Build E2EE options matching Element Call / LiveKit JS SDK defaults. + """Build E2EE options for Element Call compatibility. - Use PBKDF2=0 (default, no custom Rust KDF) so libwebrtc's C++ FrameCryptor - handles key derivation the same way as the JS SDK. The custom HKDF=1 path - in the Rust FFI fork uses different output sizes, causing DEC_FAILED. - - Element Call uses: ratchetWindowSize=0, keyringSize=16, ratchetSalt="LKFrameEncryptionKey" + Use HKDF=1 with custom Rust KDF. Pass raw keys — Rust derives via + HKDF-SHA256(salt=ratchetSalt, ikm=rawKey, info=zeros128). + Matching Element Call JS: HKDF-SHA256(salt="LKFrameEncryptionKey", ikm=rawKey, info=zeros128, len=128). """ key_opts = rtc.KeyProviderOptions( shared_key=b"", # empty = per-participant mode @@ -455,7 +456,7 @@ def _build_e2ee_options() -> rtc.E2EEOptions: ratchet_salt=b"LKFrameEncryptionKey", failure_tolerance=10, key_ring_size=16, - key_derivation_function=0, # PBKDF2=0 = use libwebrtc default (no custom Rust KDF) + key_derivation_function=KDF_HKDF, # HKDF=1 = custom Rust KDF ) return rtc.E2EEOptions( encryption_type=rtc.EncryptionType.GCM,