← Все статьи

30 марта 2026 · 6 мин чтения Безопасность

E2EE-шифрование: компромиссы о которых никто не говорит

"End-to-end encrypted" — модный лейбл в маркетинговых материалах. Но реализовать его без потери удобства — задача из адских, рассказываем что у нас работает, а что — нет.

Что мы изначально хотели

Чтобы данные пользователя были криптографически недоступны для нас (сервера). Чтобы при компроментации DB или сервера задачи и заметки оставались зашифрованным мусором.

Звучит просто. На практике — куча подводных камней:

Камень 1: Поиск

Если данные зашифрованы — как сервер ищет по ним? Классический ответ: никак, ищем на клиенте после расшифровки. Проблема — для команды из 30 человек с 10K задач это значит синкать ВСЁ на каждое устройство.

Что сделали:

Это компромисс. Чистый E2EE-пуристы скажут что Blind Index — это утечка metadata. Технически правы. Но без неё фича "найти задачу по слову на сервере" невозможна.

Камень 2: Совместный доступ

В команде из 10 человек все должны иметь доступ к общим задачам. Каждая задача шифруется ключом, ключ зашифрован публичными ключами участников. При добавлении нового члена — нужно пере-зашифровать ключи всех задач его публичным ключом.

На 10K задач это 10K крипто-операций. Делать на клиенте при каждом добавлении — нон-старт. Решение:

// Group-key model
const groupKey = generateAESKey();         // shared symmetric key
const wrappedForUser = [];
for (const member of team) {
    wrappedForUser.push({
        userId: member.id,
        wrappedKey: encrypt(member.publicKey, groupKey)
    });
}
// Server stores wrappedForUser. Знает только публичные ключи.

Когда новый юзер добавлен — старая group key остаётся (он не получит доступ к старым задачам), генерируется новая для будущих. Юзер видит "вы добавлены — но только будущие задачи".

Если нужен доступ к старым — админ выгружает старые задачи, расшифровывает, перешифровывает с новой group key. Лагает. Юзер ждёт. Менее идеально.

Камень 3: Восстановление пароля

Что если пользователь забыл пароль? У нас нет мастер-ключа на сервере. Просто "забыл пароль" = потерял доступ ко всему навсегда.

Юзеры это не понимают. В Slack ты восстанавливаешь пароль и логинишься. В Signal — теряешь чаты. FocusWork по дефолту был как Signal. Юзеры возмущались.

Решение: recovery key. При активации E2EE юзер скачивает 24-словную recovery-фразу (типа crypto-wallet seed). Если потерял пароль — вводит фразу, расшифровываются данные, ставится новый пароль.

"Recovery key звучал страшно, пока мы не поняли что это buy-in для юзера. Если он скачал и сохранил seed — он понимает risks и серьёзно относится к шифрованию. Если не скачал — fallback на non-E2EE workspace, никаких сюрпризов."

Камень 4: Push-уведомления

Apple/Google push содержат body (текст). У нас body — шифрованный мусор. Юзер видит "У вас новая задача" вместо названия.

Решение: push содержит только task_id и notification_type. Клиент при получении push'а сам подтягивает шифрованную задачу и расшифровывает локально. Дополнительный round-trip — но превью полноценное.

Что мы НЕ делаем (и почему)

HSM / Hardware Security Module

Не используем. Для нашего масштаба overkill. Ключи в Keychain (iOS/Mac) / Keystore (Android) / IndexedDB-encrypted (web). Достаточно для нашей threat model.

Full forward secrecy

Не реализовали. Forward secrecy требует частой ротации session keys и пере-шифрования. Performance hit + complexity для use case "task manager" не оправданы.

Анонимные аккаунты

Аккаунт по email-у. Без верификации не реальной идентичности, но и не Tor-уровня анонимности. Это compromise для UX.

Уроки

  1. "E2EE" — не бинарный yes/no. Чёткие границы threat model важнее marketing-лейбла.
  2. Дать юзеру выбор уровня шифрования (E2EE workspace vs обычный) — повышает adoption обоих.
  3. UX компромиссы неизбежны. Признавать честно, документировать.
  4. Готовый recovery flow — most underrated часть шифрования.

— Команда безопасности FocusWork