Skip to content

fix(e2ee): fix SET_MEDIA_ENCRYPTION_KEY pipeline and add participantId support#17351

Open
encedo wants to merge 10 commits intojitsi:masterfrom
encedo:master
Open

fix(e2ee): fix SET_MEDIA_ENCRYPTION_KEY pipeline and add participantId support#17351
encedo wants to merge 10 commits intojitsi:masterfrom
encedo:master

Conversation

@encedo
Copy link
Copy Markdown

@encedo encedo commented Apr 27, 2026

Companion to jitsi/lib-jitsi-meet#3028.

Issue 1 — incorrect key type in middleware

middleware.ts imported raw key bytes as an AES-GCM CryptoKey before
forwarding them to conference.setMediaEncryptionKey(). However,
ExternallyManagedKeyHandler ultimately passes the value to the E2EE worker's
Context.setKey(key: Uint8Array | ArrayBuffer), which calls:

const material = await importKey(keyBuffer);   // HKDF
const newKey   = await deriveKeys(material);   // HKDF-SHA-256 → AES-GCM-128

An already-derived AES-GCM key cannot serve as HKDF input. The fix removes
the intermediate importKey call; raw Uint8Array bytes are passed directly
so the worker can perform the derivation itself.

Issue 2 — participantId not propagated

The SET_MEDIA_ENCRYPTION_KEY Redux action and the External API
setMediaEncryptionKey() method did not read or serialise a participantId
field. This made per-sender key selection (enabled by
e2ee.externallyManagedSharedKey: false in lib-jitsi-meet) unreachable through
the public API. participantId is now an optional field throughout the chain;
omitting it preserves the existing behaviour.

Testing

Unit tests are in the companion lib-jitsi-meet PR. Manual verification:

  1. Deploy Jitsi with e2ee: { externallyManagedKey: true, externallyManagedSharedKey: false }
  2. From an embedding page call api.setMediaEncryptionKey({ key, index, participantId })
  3. Confirm in the E2EE worker console that setKey is called with the correct
    participantId and non-empty key bytes.

…d support

Two issues in the externally-managed key path:

1. middleware.ts imported the raw key bytes as an AES-GCM CryptoKey before
   passing them to the conference.  The E2EE worker's Context.setKey() expects
   raw bytes so it can run its own importKey() + deriveKeys() (HKDF-SHA-256 →
   AES-GCM-128) derivation.  Importing as AES-GCM bypassed this step and
   produced a key the worker could not use.  The importKey call is removed;
   raw Uint8Array bytes are forwarded directly.

2. Neither middleware.ts nor the External API method propagated a participantId
   field.  Without it, ExternallyManagedKeyHandler cannot select the correct
   per-sender E2EEContext, and the worker receives an undefined participantId
   which it rejects.  participantId is now extracted from action.keyInfo and
   forwarded to conference.setMediaEncryptionKey(), and the External API
   setMediaEncryptionKey() method accepts and serialises it alongside the key.

External API callers that want per-sender key asymmetry (one unique key per
sender, as opposed to a shared group key) should pass participantId in the
keyInfo object.  The field is optional; omitting it keeps the existing
behaviour where the local participant's ID is used by the handler.
@jitsi-jenkins
Copy link
Copy Markdown

Hi, thanks for your contribution!
If you haven't already done so, could you please make sure you sign our CLA (https://jitsi.org/icla for individuals and https://jitsi.org/ccla for corporations)? We would unfortunately be unable to merge your patch unless we have that piece :(.

@encedo
Copy link
Copy Markdown
Author

encedo commented Apr 27, 2026

Hi, thanks for your contribution! If you haven't already done so, could you please make sure you sign our CLA (https://jitsi.org/icla for individuals and https://jitsi.org/ccla for corporations)? We would unfortunately be unable to merge your patch unless we have that piece :(.

Done, signed.

Krzysztof Rutecki added 8 commits April 28, 2026 13:37
…nal API event

Exposes the OLM custom-message channel (added in lib-jitsi-meet companion
commit) through the JitsiMeetExternalAPI so that the host-app can use OLM
as a secure E2EE transport for ML-KEM key exchange and HSM attestation.

API.js:
- 'send-olm-message' command: forwards (participantId, type, JSON payload)
  to JitsiConference.sendOlmMessage(), which encrypts the message over the
  established OLM session.  Empty participantId broadcasts to all sessions.
- notifyOlmMessageReceived(from, type, payload): fires the external event
  when an OLM custom message is received from a remote participant.

external_api.js:
- Register 'olm-message-received' → 'olmMessageReceived' in the public
  event map so that embedding pages can subscribe with addListener().

middleware.ts:
- Listen to JitsiConferenceEvents.OLM_MESSAGE_RECEIVED (emitted by
  ExternallyManagedKeyHandler when OLM decrypts a custom message) and
  forward it to APP.API.notifyOlmMessageReceived.
…dKey is set

When e2ee.externallyManagedKey is true in the Jitsi config, dispatch
toggleE2EE(true) immediately on CONFERENCE_JOINED so that media is
encrypted from the first frame without requiring a manual UI toggle or
an External API command from the host-app.
- app.js: import @matrix-org/olm with window.Olm assignment; load olm-options.js first
- olm-options.js: set OLM_OPTIONS.locateFile so Emscripten finds olm.wasm under libs/
- index.html: add <script src="libs/olm.js"> for production HTML
- external_api.js: add sendOlmMessage to External API commands map
- API.js: fix notifyOlmMessageReceived — spread fields at top level instead of nesting
  under data: (external_api.js destructures {name, ...rest}, so nested fields were
  invisible to the outer-page listener, causing silent handshake failure)
- webpack.config.js: alias lib-jitsi-meet to ../lib-jitsi-meet; ignore node_modules
  in webpack-dev-server watch to prevent reload storms
- Makefile: point LIBJITSIMEET_DIR at ../lib-jitsi-meet
Show a visible warning banner at the top of the chat panel when E2EE is
active, so users are not misled into thinking chat messages are protected
by the same end-to-end encryption as media.
…ly in Jitsi UI

When E2EE is enabled, SEND_MESSAGE dispatches via conference.sendOlmMessage
instead of sendTextMessage, and own message is added to Redux store directly
(OLM messages don't echo back). Incoming encedo:chat OLM messages are
injected into the native chat UI via _handleReceivedMessage.
Falls back to plain XMPP when E2EE is off — fully backwards-compatible.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants