Commit 4294af4
End-to-end encryption for call media (#1801)
* feat(core): end-to-end encryption for call media
Adds framed AES-GCM E2EE, following the shape the JS and iOS SDKs use so the
same integration works across platforms.
An E2EEManager is attached to a Call before join. The publisher installs an
encryptor on each outgoing sender after addTransceiver, and the subscriber
installs a decryptor on each incoming receiver once it knows which user the
track belongs to. The join request carries an e2ee flag that the coordinator
validates against the call's encryption settings.
Key generation and distribution stay out of the SDK, per spec. Integrators
either drive StreamEncryptionManager's key APIs or supply their own
E2EEManager, which detaches Stream from the encryption entirely.
Notable decisions:
- Key management lives on E2EEKeyProvider, separate from E2EEManager. The
spec's manager contract is only encrypt/decrypt, and a custom manager backed
by MLS or a hardware keystore has no key setters to offer.
- Call.setE2EESharedKey and friends lazy-create the default manager, so setting
a key is all it takes to enable encryption. A manager the SDK created is
disposed on cleanup; one handed to us by the app is not, since it usually
outlives the call.
- If the encryptor cannot be attached, the publisher drops the transceiver
instead of caching it. Publishing there would send plaintext on a call the
app believes is encrypted.
StreamEncryptionManager reaches org.webrtc.EncryptionManager through
reflection, because no published WebRTC artifact carries GetStream/webrtc#110
yet: 146.7.0 (May) and 148.0.1-SNAPSHOT (Aug 12) both predate it. Compiling
against the class directly would break every module. The binding resolves
methods by name and arity, isSupported() reports whether the class exists, and
the cost is nil since encrypt/decrypt run once per track attach rather than
per frame. Replace it with direct calls when the AAR ships.
Still open: the SFU's JoinResponse.e2ee_enabled from protocol#1892 is not in
our vendored proto, so CallState.e2eeEnabled reflects the attached manager
rather than the server's view.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(core): own E2EE keys on StreamEncryptionManager
Drop Call-level key helpers and the JNI wrapper so the app holds the
manager, sets keys before attach, and disposes it. Point WebRTC at the
snapshot that ships EncryptionManager.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(core): send e2ee on first join and keep the lobby manager alive
The coordinator rejects a join whose flag disagrees with the call.
Rejoin and migrate omit the param and reuse the attached manager.
The lobby no longer disposes that manager when Join clears the task.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(core): dump public API for E2EE types
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(e2ee): address media encryption review
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(e2ee): return results from manager setup
Co-authored-by: Cursor <cursoragent@cursor.com>
* Remove comment
* fix(core): reattach decryptors after track removal and document E2EE events
Join coordinator tests now match the e2ee joinRequest argument. Removed tracks drop decryptor tracking so a re-added receiver can be attached again, and the demo plus KDoc cover runtime manager events.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(core): trace E2EE setup and native encryption errors
Encryption problems were visible only in local logcat, so a call that
joined encrypted and then went undecodable left nothing behind in call
stats to diagnose it.
Traces four things through the existing tracer pipeline:
- whether the app attached a manager, recorded at session creation since
setE2EEManager has to run before join, when no tracer exists yet
- setE2EEManager rejected because the call already joined, which silently
leaves the call unencrypted
- native encryption events, throttled per event kind and track because
decryption can fail per frame while the buffer drains on the stats
interval; suppressed repeats are counted, not dropped
- encryptor and decryptor attach failures, which withhold a track without
ever reaching the SFU
WebRTC exposes a single observer slot that setEventListener used to claim,
so the SDK could not observe events without displacing the app. The
manager now owns the slot and fans out to both listeners, isolating a
throwing one from the other. Sessions register through an internal
listener that clears only if still current, so a rejoin does not lose it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(core): trace only E2EE setup, not native events
Narrows the previous commit to the encryption setup. Native encryption
events fire per frame on every client, which is more volume than call
stats should carry, and apps already observe them through
StreamEncryptionManager.setEventListener.
Removes the native event trace and its throttle, and the encryptor and
decryptor attach-failure traces, which keep their existing logs. The
observer fan-out goes with them: it existed so SDK tracing could share
WebRTC's single observer slot with the app, and with no SDK listener left
setEventListener owns the slot directly again.
What remains is one trace per session recording whether a manager was
attached and which algorithm it uses, plus the setE2EEManager call that
was rejected for arriving after join.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(core): raise the unit test heap so Robolectric can load
The unit test job failed with OutOfMemoryError loading
android-all-instrumented-13.jar, in whichever Robolectric class ran
first: IncomingCallPresenterTest, ServiceLauncherTest,
TelecomPermissionsTest, ForegroundServicePermissionManagerTest.
Gradle forks one test worker with a 512 MB heap by default, and unlike
isolatedTest the main task does not set forkEvery, so the whole suite
shares that worker. Robolectric's android-all jar never comfortably fit,
and it tipped over as the suite grew. Reruns sometimes passed, which is
what a marginal heap looks like rather than a flake.
Raising the worker to 4g fixes it, matching what
stream-video-android-ui-compose already does for Paparazzi. Verified by
reproducing the exact failure locally at 512m and confirming all 1117
tests pass at 4g.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>1 parent 083e929 commit 4294af4
34 files changed
Lines changed: 2463 additions & 51 deletions
File tree
- demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby
- gradle
- stream-video-android-core
- api
- src
- main/kotlin/io/getstream
- android/video/generated
- infrastructure
- models
- video/android/core
- call
- components
- connection
- e2ee
- socket/common/parser2
- trace
- test/kotlin/io/getstream/video/android/core
- call/components
- e2ee
- rtc
- trace
Lines changed: 188 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
Lines changed: 20 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
167 | | - | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
168 | 174 | | |
169 | 175 | | |
170 | 176 | | |
| |||
176 | 182 | | |
177 | 183 | | |
178 | 184 | | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
179 | 188 | | |
180 | 189 | | |
181 | 190 | | |
| |||
207 | 216 | | |
208 | 217 | | |
209 | 218 | | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
210 | 226 | | |
211 | 227 | | |
212 | 228 | | |
| |||
542 | 558 | | |
543 | 559 | | |
544 | 560 | | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
545 | 564 | | |
546 | 565 | | |
547 | 566 | | |
| |||
Lines changed: 63 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
| 30 | + | |
29 | 31 | | |
30 | 32 | | |
31 | 33 | | |
| 34 | + | |
32 | 35 | | |
33 | 36 | | |
34 | 37 | | |
| |||
44 | 47 | | |
45 | 48 | | |
46 | 49 | | |
| 50 | + | |
47 | 51 | | |
48 | 52 | | |
49 | 53 | | |
| |||
54 | 58 | | |
55 | 59 | | |
56 | 60 | | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
57 | 67 | | |
58 | 68 | | |
59 | 69 | | |
| |||
66 | 76 | | |
67 | 77 | | |
68 | 78 | | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
69 | 84 | | |
70 | 85 | | |
71 | 86 | | |
| |||
192 | 207 | | |
193 | 208 | | |
194 | 209 | | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
195 | 258 | | |
196 | 259 | | |
197 | 260 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
| 52 | + | |
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| |||
0 commit comments