Шифрование звонков
Аудио, видео и демонстрация экрана в звонках Floodilka шифруются сквозным шифрованием (E2EE): кадры шифруются в браузере отправителя и расшифровываются только в браузерах остальных участников. Медиасервер (LiveKit SFU) пересылает уже зашифрованные кадры и не может их прочитать.
Модель угроз
Заголовок раздела «Модель угроз»| Кто | Видит ли медиа |
|---|---|
| Пассивный наблюдатель в сети | Нет — поверх E2EE есть ещё DTLS-SRTP до SFU |
| LiveKit SFU (в т.ч. на стороннем хостинге) | Нет — получает только зашифрованные кадры |
| Скомпрометированный медиасервер | Нет — ключа у него нет, он никогда не попадает в LiveKit-токен |
| Бэкенд и gateway Floodilka | Да — они генерируют и раздают ключ комнаты |
| Участник звонка | Да — это и есть смысл общего ключа комнаты |
Практический выигрыш: медиасервер можно размещать где угодно (в том числе у стороннего провайдера, в другой юрисдикции, на арендованном железе), не расширяя круг тех, кто может слушать звонок.
Как это устроено
Заголовок раздела «Как это устроено»- Бэкенд генерирует для комнаты 32 случайных байта (
crypto.randomBytes) и кладёт их в Redis по ключуvoice:room:e2ee:guild:{guild_id}:{channel_id}либоvoice:room:e2ee:dm:{channel_id}. Создание атомарное (SET … NX GET), поэтому все участники одной комнаты гарантированно получают один и тот же ключ. - Ключ возвращается клиенту в ответе на выдачу голосового токена и не кладётся в LiveKit-токен — ни в grant, ни в metadata. Токен проверяет SFU, поэтому всё, что лежит внутри токена, для SFU читаемо; ключ туда попасть не должен.
- Gateway пересылает ключ в событии
VOICE_SERVER_UPDATE(поляe2ee_keyиe2ee_required) и сам его никак не интерпретирует. - Клиент декодирует base64 в сырые байты, отдаёт их
ExternalE2EEKeyProviderизlivekit-client(сырые байты означают HKDF; строка означала бы PBKDF2 по парольной фразе), включает шифрование и только после этого подключается к комнате. - Ключ живёт, пока живёт сама комната, и удаляется по вебхуку
room_finishedили при смене rtc-региона. TTL у ключа намеренно нет.
Пятый пункт стоит объяснить: ключ обязан переживать перезакрепление комнаты за сервером. Закрепление сбрасывается в том числе тогда, когда конкретному пользователю закреплённый сервер недоступен по персональным ограничениям, — и это ничего не говорит о том, занята комната или нет. Если бы ключ уходил вместе с закреплением, а комната затем вернулась на тот же сервер, уже подключённые участники остались бы со старым ключом, а следующий вошедший получил бы новый: все друг друга видят, но медиа не расшифровывается. По той же причине у ключа нет TTL — сутки непрерывного разговора без единого нового захода обнулили бы ключ под работающей комнатой.
Поля VOICE_SERVER_UPDATE
Заголовок раздела «Поля VOICE_SERVER_UPDATE»| Поле | Тип | Значение |
|---|---|---|
e2ee_key |
string | null |
Ключ комнаты в base64 (32 байта). null, если шифрование выключено на инстансе |
e2ee_required |
boolean |
Если true, клиент обязан подключаться только с включённым шифрованием |
Оба поля в событии присутствуют всегда: если бэкенд их не прислал, гейтвей подставляет null и false.
Проверки на null достаточно, отсутствующее поле обрабатывать не нужно.
При e2ee_required = true клиент, который не умеет шифровать (нет insertable streams), не подключается и
показывает ошибку. Тихого отката в незашифрованную сессию нет.
Если ключ пришёл, но воспользоваться им не удалось (браузер не поддерживает insertable streams, ключ не
декодируется, воркер не создался), а e2ee_required = false, клиент подключается незашифрованным — но
помечает соединение как проблемное и показывает «Ошибка ключа», а не «Транспортное». Остальные участники
комнаты в этот момент шифруют, то есть такой участник их не слышит, и вид здорового соединения тут вводил бы
в заблуждение.
Инстанс управляет этим через VOICE_E2EE_ENABLED и VOICE_E2EE_REQUIRED (обе по умолчанию true).
Порядок включения на живом инстансе
Заголовок раздела «Порядок включения на живом инстансе»Клиент, который про e2ee_key ничего не знает, зайдёт в комнату без шифрования: старые вкладки с
закешированным бандлом, не обновлённые сборки для компьютера, сторонние боты. В одной комнате с
шифрующимися участниками такой клиент не услышит их, а они — его, и на его стороне не будет никакой
диагностики, потому что он про шифрование не в курсе. Версионного гейта в схеме нет.
От смешанных комнат защищает только VOICE_E2EE_ENABLED=false: при нём ключ не выдаётся и не шифрует
никто. Порядок выката:
- Выкатить бэкенд и клиент с
VOICE_E2EE_ENABLED=false. - Дождаться оборота клиентов.
- Включить
VOICE_E2EE_ENABLED=true.
VOICE_E2EE_REQUIRED для этой задачи не годится и работает не так, как можно ожидать. Пока ключ выдаётся,
каждый способный клиент шифрует независимо от значения required — флаг управляет не шифрованием, а тем,
что делать с клиентом, который шифровать не может. При VOICE_E2EE_REQUIRED=false такой клиент заходит в
комнату незашифрованным: он не услышит остальных и остальные не услышат его, причём без единого
сообщения об ошибке. Поэтому значение по умолчанию true — оно превращает эту ситуацию в понятный отказ
подключиться. Ставить false осмысленно только там, где сознательно нужен деградированный режим.
Известные ограничения
Заголовок раздела «Известные ограничения»- Не zero-trust. См. врезку выше: раздача ключей идёт через наш контрол-плейн.
- Ключ живёт всю сессию звонка и не ротируется. В групповых ЛС состав участников может меняться прямо во время звонка, и удалённый из группы участник сохраняет ключ до конца звонка. Использовать его он сможет только пока у него есть доступ к шифротексту, то есть пока он остаётся подключённым к комнате SFU — принудительного отключения из активного звонка при удалении из группы платформа сегодня не делает независимо от шифрования. Ротация без переподключения требует отдельного gateway-события для рассылки нового ключа оставшимся и в текущей схеме не реализована.
- Метаданные не скрыты. Кто, когда, с кем и как долго говорит, кто говорит в данный момент, громкость, битрейт, размер кадров — всё это остаётся видно серверу. Шифруется содержимое медиа, а не факт звонка.
- Серверные функции, которым нужно читать медиа, работать не будут. Запись звонков и LiveKit egress получают только шифротекст.
- Только медиа. Текстовые сообщения, вложения и всё остальное содержимое E2EE не покрыто — они хранятся и обрабатываются сервером в открытом виде.
- Весь криптообмен комнаты идёт в одном воркере. На подключение создаётся один воркер insertable streams, и он же расшифровывает кадры всех удалённых участников. Для аудио это незаметно, но в канале с несколькими включёнными камерами упор будет в этот поток.
RED(аудиоизбыточность) отключается автоматически.livekit-clientвыключает RED при включённом E2EE; на нестабильной сети это может слегка ухудшить устойчивость аудио к потерям.
Требования к клиенту
Заголовок раздела «Требования к клиенту»Нужна поддержка insertable streams (Encoded Transform): актуальные Chrome/Edge, Safari 15.4+ и приложение
для компьютера. Firefox поддерживает их не во всех сборках — при e2ee_required = true там подключение
будет отклонено с понятной ошибкой.
