~/potatohd.ruEN
← Все статьи
13 августа 2026·10 мин

Umbra: мессенджер, устроенный так, чтобы сервер ничего не знал

E2EEGolibp2psystem design

WhatsApp сквозно шифрует сообщения — это правда и это много. Но шифрование содержимого решает ровно одну задачу. Сервер по-прежнему знает, какой номер кому пишет, в какое время, как часто и с каких устройств. Метаданные остаются, потому что вся архитектура держится на аккаунте, привязанном к телефону, и на почтовом ящике, который ведёт оператор.

Umbra — попытка ответить на другой вопрос: что должно быть в системе, чтобы посредник не мог узнать даже этого? Ниже — про архитектурные последствия такого требования. Они гораздо обширнее, чем «поменять библиотеку шифрования».

#Что видит посредник

В классической схеме сервер — это адресная книга и почтовое отделение одновременно. Он знает аккаунты, знает связи между ними и хранит очередь сообщений для тех, кто сейчас офлайн. Даже если содержимое конвертов ему недоступно, журнал доставки — сам по себе богатый источник данных.

В Umbra перевёрнута сама единица адресации: адресуется не аккаунт, а почтовый ящик, идентификатор которого выводится из корневого ключа аккаунта. Хранитель (custodian) принимает в него объекты — версионированные, адресуемые по содержимому непрозрачные блобы — и не знает ни отправителя, ни получателя в человеческом смысле.

Устройство отправителя шифрует объект и отдаёт его хранителю, который видит только непрозрачные байты; получатель забирает их из ящика по протоколу инбокса
Хранитель — камера хранения, а не почтальон: он не знает, что несёт и кому

Две детали делают это не декларацией, а свойством системы.

Во-первых, ящик не называется в запросе. Устройство приходит с доказательством авторизации, и хранитель сам выводит ящик из корневого ключа аккаунта в этом доказательстве, привязывая его к конкретному сетевому пиру на другом конце соединения. Спросить чужой ящик нельзя — не потому что запретят, а потому что такого поля в запросе нет. Подслушанное доказательство тоже не переиспользовать: оно привязано к соединению.

Во-вторых, управляющий контур не принимает содержимое. Приложению всё равно нужна небольшая серверная часть: точка входа в сеть, выдача коротких скоупных пропусков, подписанные карточки релеев на случай, если прямое соединение не собралось, и обычный «разбуди устройство» для APNs и FCM. У этого контура фиксированная схема запросов, ограниченные значения и закрытые списки — и ни одного эндпоинта, который принимал бы, хранил или возвращал текст сообщения. Пуш несёт только сигнал «проснись», без содержимого.

#Криптография: почему не просто X3DH

WhatsApp и Signal строят сессию на X3DH: несколько обменов Диффи–Хеллмана на эллиптических кривых, из которых получается общий секрет, а дальше — Double Ratchet, который меняет ключ на каждое сообщение.

Проблема известна и не в реализации: перехваченный сегодня трафик можно расшифровать завтра, когда появится достаточно большой квантовый компьютер. Для мессенджера, который хранит переписку годами, это реальный риск, а не мысленный эксперимент.

Поэтому в Umbra рукопожатие — PQXDH: те же обмены на X25519 плюс инкапсуляция ML-KEM-768. Секрет сессии собирается из обоих источников, поэтому взломать нужно оба: классическую часть и постквантовую.

Рукопожатие PQXDH из обменов X25519 и инкапсуляции ML-KEM даёт корневой секрет, из которого цепочка HMAC выдаёт ключ на каждое сообщение
Двойной корень сессии: сломать нужно и классическую часть, и постквантовую

Дальше — детали, которые обычно и решают, устоит ли схема на практике:

  • Заголовок сообщения и оба долговременных ключа идентичности идут в ассоциированные данные AEAD. То есть подписью защищено не только содержимое, но и «кто кому и каким по счёту»: релей не может подменить адресата или переставить сообщения местами.
  • Долговременный ключ устройства подписан ключом, который заверил корень аккаунта. Без этой привязки чужое устройство могло бы предъявить сертифицированную личность жертвы рядом со своим ключом.
  • Неудачная расшифровка не меняет состояние сессии, а поддельное рукопожатие не тратит одноразовый предключ. Иначе получаем бесплатный способ вывести переписку из строя снаружи.
  • Вся криптография на чистом Go без cgo — один и тот же формат обслуживает Android через GoMobile, десктоп, CLI и сборку в WebAssembly для браузера.

Отдельно: доверие решается не здесь. Функция расшифровки принимает предикат доверия, который приложение разрешает через сертификаты корня аккаунта. Криптографический слой не должен решать, кому мы верим, — он должен уметь проверить, что нам дали именно то, что подписано.

#Спам без телефонных номеров

Номер телефона в WhatsApp — не только идентификатор, но и главный барьер против спама: он дорогой и связан с реальным человеком. Убираем номер — теряем барьер. Заменить его нужно чем-то, что не требует знать, кто вы.

Ящик в Umbra по умолчанию закрыт: хранитель не примет объект, чей тикет не подписан тем, кого владелец ящика авторизовал. Дальше есть три способа стать досягаемым для незнакомца, и все три — про экономику, а не про личность:

  • Подписанная политика ящика — «принимаю: ничего / по приглашению / за марку / любое». У политики есть срок, и более старая версия никогда не вытеснит более новую.
  • Приглашения — ограниченные и отзываемые разрешения в контакт-инвайте или QR-коде. Каждое погашение считается и привязывается к устройству, поэтому подсмотренный по дороге инвайт не сработает у чужого, а утёкший ограничен числом погашений и сроком.
  • Почтовые марки — доказательство работы, привязанное к ящику, к дню и к конкретному объекту. Именно привязка к объекту делает массовую рассылку дорогой: работу нельзя размазать по тысяче получателей или переиспользовать для второго сообщения.

Ключевая деталь исполнения: проверка происходит на кадре предложения — до того, как передана полезная нагрузка. Отказанный отправитель не стоит получателю ни трафика, ни места. И политику проверяют независимо обе стороны — и хранитель, и само устройство-получатель.

#Что здесь честно не готово

Протокол доказан: браузер с браузером обмениваются сообщениями по-настоящему, через ту же сборку ядра в WebAssembly. Продукт — нет. Веб-клиент пока не может подтвердить получение из инбокса (нужный вызов не экспортирован в WASM-сборке), ответ требует, чтобы приглашениями обменялись обе стороны, а интерфейса для этого ещё нет. Хранитель в проде поднят руками, мимо платформы деплоя, и ждёт церемонии с корневым ключом владельца.

Это ровно тот класс задач, который на схеме не виден: архитектура держится, а продуктовый путь пользователя обрывается на третьем шаге.

#Что из этого стоит забрать

  • Шифрование содержимого — это первый шаг, а не результат. Пока сервер знает, кто кому пишет, он знает большую часть интересного.
  • Уберите поле из запроса, если хотите, чтобы им нельзя было злоупотребить. «Ящик не называется» надёжнее любой проверки прав.
  • Постквантовая часть добавляется поверх классической, а не вместо неё: комбинация не слабее худшего из двух, но требует сломать оба.
  • Убирая один защитный барьер (телефон), закладывайте другой заранее. Иначе открытая сеть станет спам-сетью в первый же день.

// свободен для заказов

Нужен сайт или сервис под ключ?