Паничный PIN, который открывал всё: где мы ошиблись и как переделали
Мы развиваем мессенджер RCQ (rcq.app, клиенты лежат на github.com/rcq-messenger) и давно держим в продукте функцию, которую разные приложения называют по‑своему: паничный PIN, duress PIN, "подставной код". Суть одинакова: когда вас вынуждают разблокировать телефон, вы вводите альтернативный PIN - и вместо настоящей переписки открывается "безопасная" версия приложения, где нет ничего чувствительного. Именно такие сценарии чаще всего и подталкивают компании искать мессенджер с шифрованием end-to-end для компании: не только ради криптографии на бумаге, но и ради поведения в стрессовых ситуациях.
В августе мы решили финально проверить, какие хвосты остались у этой фичи перед внешним аудитом. План был максимально будничный: пройтись по коду, сверить реализацию с моделью угроз, уточнить формулировки в описаниях. Но проверка неожиданно закончилась неприятным открытием: на Android подставной PIN оказался не "границей доступа", а фактически ключом ко всей переписке.
Как устроено шифрование локальной базы
И на Android, и на iOS мессенджер хранит локальную базу сообщений в зашифрованном виде. Ключ базы (условно назовём его dataKey) не лежит в открытом виде: он "упакован" в небольшой файл‑контейнер (slot), который расшифровывается ключом, выведенным из PIN‑кода пользователя. Внутри - привычная, понятная схема:
- PBKDF2‑HMAC‑SHA256
- 400 000 раундов
- соль хранится рядом с контейнером
- к соли добавляется pepper из защищённого хранилища платформы
Важно: сам PIN нигде не сохраняется. Проверка корректности - это попытка расшифровать слот. Расшифровалось - значит PIN правильный; нет - значит нет. Никаких сравнений хэшей "PIN с PIN" в явном виде.
Слотов несколько: под основной PIN, подставной PIN и стирающий (когда нужен отдельный сценарий реакции). Каждый слот несёт свою "полезную нагрузку". И именно в этой нагрузке на Android мы и допустили критическую ошибку.
Android: подставной PIN отдавал тот же ключ, что и основной
На Android при настройке подставного PIN в его слот попадал тот же самый dataKey, что и в основной. А "подставной режим" фактически строился как косметическая маскировка: приложение в памяти фильтровало списки и скрывало вторую "часть" данных, при этом файлы *.db оставались на диске. В результате пользователь вводил подставной код, видел "пустую/безопасную" картину - и был уверен, что сделал всё правильно. Но по факту он только что ввёл PIN, который выдавал ключ от настоящей базы.
Это хуже, чем отсутствие функции вообще. Без "паничного PIN" человек, попавший под давление, по крайней мере, может упереться в единственный ход: не разблокировать устройство. А тут получается ловушка: вы добровольно вводите код, который открывает всё, и уходите с ощущением, что защитились.
Отдельно отметим: локауты работали именно так, как были задуманы. Пять неверных попыток - 30 секунд ожидания, шестая - минута, седьмая - пять минут, восьмая - 15, дальше - час. Это именно задержка, а не стирание, потому что самый частый человек, ошибающийся пять раз подряд, - владелец телефона (например, банально замёрзли пальцы).
И нашли мы проблему не "магическим" сканером и не статическим анализом. Мы просто открыли файл и честно прочитали код сверху вниз, потому что перед аудитом нужно было, чтобы документация не расходилась с реальностью. Самое обидное: комментарий, где прямо объяснялось, что ключ общий, висел в коде несколько месяцев - и, похоже, после появления так ни разу и не стал предметом внимательного перечитывания.
Почему это вообще выглядело логичным
Изначально подставной режим задумывался как "второй настоящий аккаунт". Логика казалась стройной: если показывать не пустоту, а правдоподобную переписку - легенда становится убедительнее. А раз это "настоящий аккаунт" на том же устройстве, то он работает с той же базой, и ключ у базы один. Каждый шаг по отдельности выглядел разумно - и именно так иногда рождаются самые опасные инженерные решения.
Мы даже обсуждали, как усилить правдоподобие: отдельные контакты, собственные настройки, аватарку, и даже идеи с генерацией "живого шума" (чтобы у пользователей отличались списки диалогов). Но довольно быстро выяснилось, что проблема не упирается в криптографию.
Серверная сторона легко связывает аккаунты, живущие на одном устройстве: общий push‑эндпоинт, один IP, одинаковый device id, один и тот же "остров" инфраструктуры. Если у проверяющей стороны есть доступ к серверу (или сервер - это мы, и к нам пришли с запросом), связка "подставной и настоящий аккаунт на одном телефоне" вскрывается одним запросом. Легенда, которая живёт ровно до первой сверки на сервере, не выдерживает реального давления - а значит, "достраивать" ей контакты бессмысленно.
iOS: криптографически честно, но по легенде провал
На iOS с самого начала применялась другая модель: у подставного слота свой случайный dataKey. Настоящее хранилище остаётся закрытым, а ключа от него в подставной сессии просто не существует - то есть криптографически всё ровно так, как и должно быть.
Но там проявилась обратная крайность. Вход в подставной режим агрессивно чистил контакты и группы, и пользователь показывал пустой экран. Пустой мессенджер на телефоне человека, который "точно им пользуется", - это не защита, а очень заметная улика: она буквально сигнализирует, что здесь что‑то спрятано.
В итоге получилась неприятная асимметрия: одна платформа - "убедительная, но опасная", другая - "правильная, но бесполезная по сценарию".
Что переделали по итогу
Мы взяли iOS‑подход как базовый принцип: подставной режим не должен иметь технической возможности открыть настоящее хранилище. Это означает независимые ключи и отсутствие "общих" dataKey между режимами. А дальше уже достраивали поведение так, чтобы оно не выглядело подозрительно: подставной режим должен показывать что‑то правдоподобное, но при этом не быть дверью в реальную переписку.
И вот здесь важно проговорить практический вывод, который часто упускают, когда компании пытаются безопасный мессенджер купить "как коробку": duress‑сценарии нельзя оценивать только по маркетинговому описанию. Нужно смотреть, какие ключи существуют в системе, где они живут, как получаются, и что именно выдаётся на альтернативный PIN.
---
Дополнение: что это значит для бизнеса и внедрения (новые абзацы)
Для корпоративного использования подставной режим - не "экзотика для параноиков", а часть общей стратегии управления рисками. В реальности давление может возникать не только в криминальных сценариях: командировки, пересечение границ, временная передача телефона на проверку, инциденты с принуждением к разблокировке внутри организации. Поэтому, когда обсуждается корпоративный защищенный мессенджер для бизнеса цена, стоит отдельно закладывать расходы не только на лицензии, но и на регулярные проверки критичных функций - особенно тех, что обещают "аварийный" режим.
Есть и чисто операционный аспект. Если компания выбирает защищенный мессенджер для Android и iOS скачать "в пару кликов", это ещё не означает, что одинаковые функции одинаково реализованы на разных платформах. Как показал наш случай, формально одинаковая кнопка в интерфейсе может скрывать принципиально разные модели ключей и хранения. Для IT‑службы это означает простое правило: проверять кроссплатформенную симметрию не по UX, а по криптографической и системной архитектуре.
Отдельная тема - модель угроз "сервер видит всё". Даже при end‑to‑end шифровании метаданные, привязки устройств и пуш‑маршрутизация могут разрушать легенду "это другой аккаунт". Поэтому попытка сделать "вторую жизнь" внутри одного телефона часто обречена: если противник имеет административный доступ к серверной стороне, он собирает картину быстро. Подставной режим должен работать так, будто второго мира не существует, а не как "вторая витрина" того же магазина.
Наконец, для организаций, которым важна проверяемость, закономерно встаёт вопрос про open source защищенный мессенджер внедрение и поддержка. Открытые клиенты и воспроизводимые сборки упрощают аудит, но сами по себе не страхуют от ошибок в логике фич - особенно если команда долго живёт в предположении "мы уже это продумали". Здесь спасает дисциплина: периодические ручные ревизии критичного кода, тесты на негативные сценарии и практика "читать реализацию так, как будет читать аудитор".
Если вы строите защищённые коммуникации в компании, полезно формулировать требования не только "чтобы было E2E", но и "что произойдёт при принуждении", "какие ключи доступны в каждом режиме", "можно ли случайно открыть настоящее хранилище альтернативным PIN". Ровно эти вопросы и становятся отличием между красивой функцией и реальной защитой - особенно когда на кону не теория, а чужие руки на вашем телефоне и несколько секунд на решение. Подробнее о разборе этой истории и выводах мы рассказываем в материале паничный PIN и ошибки реализации.
