«Разблокируй телефон.» — Он разблокирует. Открывается мессенджер. Чаты с «Alex», «Mila», «Family». Всё выглядит нормально. Всё — подделка.

Привет, Хабр! Я разрабатываю open-source мессенджер Xipher (C++/Android), и одна из фич, которую пришлось проектировать особенно тщательно — Panic Mode. Это система правдоподобной отрицаемости (plausible deniability): при вводе специального PIN-кода мессенджер показывает полностью фейковую, но убедительную базу данных с поддельными чатами, а параллельно отправляет скрытый SOS-сигнал на сервер.

В статье разберу архитектуру целиком — от криптографического разделения баз до генерации правдоподобных фейков и маскировки panic-алерта под рутинный сетевой запрос. Весь код — из реального проекта.

Исходники открыты — ссылка на GitHub в конце статьи.

Содержание

  1. Зачем нужна правдоподобная отрицаемость

  2. Архитектура: две базы, один PIN-экран

  3. Криптографическая развилка: SQLCipher решает за вас

  4. Генератор фейков: как обмануть следователя

  5. Скрытый SOS: AES-GCM под видом metadata_sync

  6. Сетевая изоляция: ни один реальный бит не утечёт

  7. Полный flow: что происходит посекундно

  8. Модель угроз: от кого защищаем

  9. Сравнение с аналогами: VeraCrypt, Signal, Briar

  10. Честные ограничения

  11. Заключение и ссылки


1. Зачем нужна правдоподобная отрицаемость

E2EE решает проблему перехвата. Исчезающие сообщения решают проблему хранения. Но ни то, ни другое не решает проблему физического принуждения.

Сценарии:

  • Пограничный контроль: вас просят разблокировать телефон и показать мессенджер. В ряде стран отказ — уголовное правонарушение (UK — Regulation of Investigatory Powers Act, Part III: до 2 лет тюрьмы за отказ предоставить ключ шифрования)

  • Задержание: устройство изъято, пароль получен под давлением

  • Корпоративный шпионаж: сотрудника вынуждают показать рабочие чаты

  • Домашнее насилие: абьюзер требует доступ к переписке

Во всех этих случаях жертва не может солгать — пустой мессенджер или отсутствие приложения вызовут подозрения. А «Удалить чат» оставляет forensic-следы в SQLite (WAL-журнал, UNDELog, свободные страницы).

Правильный ответ — не скрывать, а подменять. Показать убедительную, нормально выглядящую переписку, которая не вызовет вопросов. Именно это делает Panic Mode.

Концепция не нова — VeraCrypt Hidden Volume использует тот же принцип для дисков. Я адаптировал его для мессенджера.


2. Архитектура: две базы, один PIN-экран

Система построена на двух полностью изолированных SQLCipher-базах данных:


+---------------------------------------+
|  Android Device                        |
|                                        |
|  /data/data/com.xipher/.../            |
|                                        |
|  +----------------+  +----------------+|
|  | real_user.db   |  | decoy_user.db  ||
|  | (SQLCipher)    |  | (SQLCipher)    ||
|  |                |  |                ||
|  | PIN: 1234      |  | PIN: 5678      ||
|  | Реальные чаты  |  | Фейковые чаты  ||
|  | 47 диалогов    |  | 47 диалогов    ||
|  | 12K сообщений  |  | ~180 сообщений ||
|  +----------------+  +----------------+|
|                                        |
|  +------------------------------------+|
|  | PinUnlockActivity                  ||
|  | [  _ _ _ _  ]  <-- один экран      ||
|  | Введите PIN                        ||
|  +------------------------------------+|
+---------------------------------------+

Ключевые принципы:

  • Один экран ввода — пользователь вводит PIN в одно и то же поле. Нет кнопки «Panic Mode», нет переключателя, нет визуальной разницы

  • Криптографическое разделение — какая база откроется, определяет PIN через SQLCipher, а не if/else в коде

  • Структурная мимикрия — количество чатов в decoy-базе совпадает с реальной, типы чатов (DM/группы/каналы) сохранены

  • Полная изоляция — в panic mode WebSocket отключён, push-токен не синхронизируется, реальные данные невидимы


3. Криптографическая развилка: SQLCipher решает за вас

Это самая элегантная часть. В коде нигде нет сравнения if (pin == panicPin). Вместо этого:


private void initInternal(Context context, String password, boolean allowDecoyCreate)        throws InvalidPasswordException {    char[] passphrase = password != null ? password.toCharArray() : new char[0];    try {        try {            // ▶ Шаг 1: пробуем открыть РЕАЛЬНУЮ базу с введённым PIN            DbHolder real = openDatabase(context, REAL_DB_NAME, passphrase, true);            setActive(context, real, false);  // isPanic = false            return;        } catch (Exception ignore) {            // SQLCipher не смог расшифровать → PIN не от реальной базы        }
    // ▶ Шаг 2: реальная не открылась → пробуем decoy    if (!PanicModePrefs.isEnabled(context) && !allowDecoyCreate) {        throw new IllegalStateException("Panic mode disabled");    }    DbHolder decoy = openDatabase(context, DECOY_DB_NAME, passphrase, allowDecoyCreate);    if (allowDecoyCreate && decoy.created) {        seedDecoy(decoy.db);    }    setActive(context, decoy, true);  // isPanic = true
} catch (Exception e) {    throw new InvalidPasswordException("Invalid password", e);
} finally {    wipeChars(passphrase);  // Затираем PIN из памяти
}

}

Как это работает:

  1. Пользователь вводит PIN

  2. DatabaseManager пробует расшифровать real_user.db этим PIN через SQLCipher

  3. Если расшифровка удалась — это реальный PIN. Открывается настоящая база

  4. Если real_user.db не расшифровался — пробуем decoy_user.db

  5. Если decoy расшифровался — это panic PIN. Включается panic mode

  6. Если ни одна база не открылась — «неверный пароль»

Почему это надёжно:

Подход

Уязвимость

if (pin == PANIC_PIN)

Panic PIN хранится в открытом виде или как хеш. Forensic-анализ извлечёт его из APK/SharedPreferences

if (hash(pin) == storedHash)

Наличие двух хешей — прямое доказательство существования panic mode

SQLCipher try/catch (наш подход)

Нет сравнения PIN. Нет хранения PIN. SQLCipher — чёрный ящик: либо расшифровал, либо нет. Forensic-аналитик видит два зашифрованных файла и не может доказать, какой «настоящий»

В setActive() сразу срабатывает цепная реакция:


private void setActive(Context context, DbHolder holder, boolean isPanic) {    synchronized (lock) {        clearSensitiveStateLocked();  // Затираем предыдущую базу        activeDb = holder.db;        activePassphrase = holder.passphrase;        activeDbName = holder.dbName;        panicMode = isPanic;    }    SecurityContext.get().setPanicMode(isPanic);    // ▶ Если panic — немедленно ставим в очередь скрытый SOS    if (isPanic) {        PanicModeWorker.enqueue(context.getApplicationContext());    }
}

Обратите внимание: clearSensitiveStateLocked() затирает байты предыдущего пароля из памяти через Arrays.fill(activePassphrase, (byte) 0). Это не идеальная защита от cold boot атак, но лучше, чем оставлять пароль в куче.


4. Генератор фейков: как обмануть следователя

Фейковая база должна быть убедительной. Пустая база или два чата «Welcome» и «Support» мгновенно выдадут обман. Поэтому DecoyDataGenerator анализирует структуру реальной базы и генерирует правдоподобную копию.

4.1. Зеркалирование структуры

Генератор итерирует по каждому реальному чату и создаёт его «двойника»:


static void generate(Context context, AppDatabase source, AppDatabase target,                     String selfId, String selfName) {    List sourceChats = source != null ? source.chatDao().getAll() : null;    if (sourceChats == null || sourceChats.isEmpty()) {        seedFallback(target, System.currentTimeMillis());  // Минимальный fallback        return;    }
SecureRandom random = new SecureRandom();
Set<String> usedNames = new HashSet<>();
int index = 0;
for (ChatEntity src : sourceChats) {    ChatEntity chat = new ChatEntity();    chat.id = "decoy_" + index + "_" + UUID.randomUUID().toString();    // ▶ Сохраняем СТРУКТУРУ: тип чата идентичен реальному    chat.isGroup = src.isGroup;    chat.isChannel = src.isChannel;    chat.isSaved = src.isSaved;    chat.isPrivate = src.isPrivate;    // ▶ Меняем СОДЕРЖАНИЕ: имя, сообщения, даты — случайные    if (src.isGroup) {        chat.title = pickUnique(GROUP_NAMES, usedNames, random);    } else if (src.isChannel) {        chat.title = pickUnique(CHANNEL_NAMES, usedNames, random);    } else {        chat.title = pickUnique(DIRECT_NAMES, usedNames, random);    }    // ...
}

}

Результат: если у вас 15 личных чатов, 8 групп и 3 канала — в decoy-базе будет ровно 15 личных, 8 групп и 3 канала. Но с другими именами и сообщениями.

4.2. Логарифмическое масштабирование сообщений

Наивный подход — «столько же сообщений, сколько в реальном чате» — слишком дорогой и подозрительный (зачем 10 000 скучных сообщений?). Вместо этого:


private static int estimateMessageCount(AppDatabase source, ChatEntity chat,                                         SecureRandom random) {    int realCount = source.messageDao().countByChat(chat.id);    int count = 2 + (int) Math.round(Math.log(realCount + 1) * 2.0d);    if (count < 2) count = 2;    if (count > 12) count = 12;    return count;
}

Логика: 2 + ln(realCount + 1) * 2, ограничено диапазоном [2, 12].

Реальных сообщений

Фейковых сообщений

0

2

10

7

100

11

1 000

12 (cap)

10 000

12 (cap)

Почему логарифм? Активный чат (1000 сообщений) должен выглядеть «живым» в preview-списке, но генерировать 1000 фейковых сообщений — и долго, и подозрительно, и бессмысленно: следователь не будет скроллить 1000 сообщений «Got it, thanks!». Достаточно 10-12 сообщений, чтобы чат выглядел естественно при беглом просмотре.

4.3. Пулы нейтрального контента

Фейковые сообщения выбираются из специально составленных пулов — однозначно безобидных и невызывающих подозрений:


private static final String[] DIRECT_MESSAGES = {    "Hey, are we still on for today?",    "Got it, thanks!",    "On my way.",    "Can you send the file?",    "Let's do it tomorrow.",    "Sounds good.",    "I'll check and get back to you.",    "See you at 6.",    "All set here.",    "Thanks, appreciate it."
};
private static final String[] GROUP_MESSAGES = {
"Reminder: meeting at 4pm.",
"Anyone free this weekend?",
"I'll bring snacks.",
"Updated the doc.",
"Please review the notes.",
"Let's sync after lunch.",
"Sharing the draft now.",
"Heads-up: schedule changed."
};

Контент намеренно скучный. Никаких тем, которые могли бы заинтересовать проверяющего. Только бытовая координация — встречи, файлы, обеды.

4.4. Реалистичная хронология

Фейковые сообщения получают правдоподобные временные метки:


long jitter = random.nextInt(60 * 1000);  // Случайный сдвиг до 60 секунд
long createdAt = baseTime - ((count - 1 - i) * 2L * 60L * 1000L) + jitter;

Интервал между сообщениями — ~2 минуты с jitter до минуты. Это имитирует естественный ритм переписки: не слишком быстро (бот), не слишком медленно (заброшенный чат).

4.5. Уникальность имён и SecureRandom


private static String pickUnique(String[] pool, Set used, SecureRandom random) {    for (int i = 0; i < pool.length * 2; i++) {        String candidate = pool[random.nextInt(pool.length)];        if (used.add(candidate)) {            return candidate;        }    }    // Fallback: имя + число, если пул исчерпан    String fallback = pool[random.nextInt(pool.length)] + " " + (used.size() + 1);    used.add(fallback);    return fallback;
}

Два момента:

  • SecureRandom, а не java.util.Random — криптографически стойкий ГПСЧ. Атакующий не может предсказать, какие имена будут выбраны

  • Дедупликация через HashSet — никогда не будет двух чатов с «Alex». Если пул имён исчерпан (20 имён, а чатов больше), добавляется порядковый номер: «Alex 21»

4.6. Атомарная запись


target.runInTransaction(() -> {    target.messageDao().clearAll();    target.chatDao().clear();    target.chatDao().upsertAll(chats);    target.messageDao().upsertAll(messages);
});

Вся decoy-база заполняется в одной транзакции. Если процесс упадёт посреди генерации — никакого «корраптнутого» промежуточного состояния.


5. Скрытый SOS: AES-GCM под видом metadata_sync

При активации panic mode на сервер должен уйти сигнал «убить все мои реальные сессии». Но этот сигнал сам по себе — улика. Если наблюдатель видит POST /api/panic — игра окончена.

Решение: замаскировать panic-алерт под рутинный сетевой запрос.

5.1. Внешний слой: безобидный JSON


private Request buildRequest() throws Exception {    String token = SecurityContext.get().getCurrentSessionToken();    String deviceId = Settings.Secure.getString(        getApplicationContext().getContentResolver(),        Settings.Secure.ANDROID_ID);
String encrypted = encryptPayload(token, deviceId);
// ▶ Внешний JSON — выглядит как рутинная синхронизация
JsonObject body = new JsonObject();
body.addProperty("type", "metadata_sync");
body.addProperty("ts", System.currentTimeMillis());
body.addProperty("device_id", deviceId != null ? deviceId : "unknown");
body.addProperty("payload", encrypted);  // Настоящий сигнал — внутри
RequestBody rb = RequestBody.create(body.toString(), JSON);
return new Request.Builder()    .url(BuildConfig.API_BASE + "/api/v1/auth/panic_alert")    .post(rb)    .header("X-Auth-Token", token)    .build();

}

Наблюдатель, перехватывающий HTTPS-трафик (даже через корпоративный MITM-прокси с установленным корневым сертификатом), видит:


{  "type": "metadata_sync",  "ts": 1708701234567,  "device_id": "a1b2c3d4e5f6",  "payload": "dGhpcyBpcyBub3QgdGhlIHJlYWwgcGF5bG9hZA..."
}

Поле type: "metadata_sync" — стандартный тип запроса, который приложение отправляет регулярно. Один лишний запрос неотличим от фонового трафика.

5.2. Внутренний слой: AES-128-GCM


private String encryptPayload(String token, String deviceId) throws Exception {    // ▶ Деривация ключа: SHA-256(token + "|" + deviceId), обрезка до 16 байт    String keyMaterial = (token != null ? token : "") + "|"                        + (deviceId != null ? deviceId : "");    byte[] keyBytes = MessageDigest.getInstance("SHA-256")        .digest(keyMaterial.getBytes(StandardCharsets.UTF_8));    byte[] aesKey = Arrays.copyOf(keyBytes, 16);
// ▶ AES-128-GCM с случайным 12-байт IV
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE,    new SecretKeySpec(aesKey, "AES"),    new GCMParameterSpec(128, iv));
// ▶ Настоящий payload — скрыт внутри
JsonObject payload = new JsonObject();
payload.addProperty("event", "metadata_sync");  // Камуфляж даже внутри
payload.addProperty("panic", true);              // Единственный маркер
payload.addProperty("action", "kill_sessions");  // Что сделать
payload.addProperty("ts", System.currentTimeMillis());
byte[] ciphertext = cipher.doFinal(    payload.toString().getBytes(StandardCharsets.UTF_8));
// IV || ciphertext → Base64
byte[] combined = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);
return Base64.encodeToString(combined, Base64.NO_WRAP);

}

Почему именно так:

Решение

Зачем

AES-128-GCM

Шифрование + аутентификация. Нельзя подменить payload

Ключ = SHA-256(token + deviceId)

Только сервер (знающий оба значения) может расшифровать. Не нужен отдельный shared secret

Случайный IV

Каждый запрос уникален. Нельзя определить panic по повторяющемуся blob

"event": "metadata_sync" даже внутри

Если AES вскрыт, содержимое выглядит рутинным. "panic": true — единственный маркер

5.3. Гарантия доставки: WorkManager


public static void enqueue(Context context) {    Constraints constraints = new Constraints.Builder()        .setRequiredNetworkType(NetworkType.CONNECTED)        .build();    OneTimeWorkRequest req = new OneTimeWorkRequest.Builder(PanicModeWorker.class)        .setConstraints(constraints)        .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)        .build();    WorkManager.getInstance(context.getApplicationContext())        .enqueueUniqueWork(UNIQUE_NAME, ExistingWorkPolicy.REPLACE, req);
}

Почему WorkManager, а не просто OkHttp?

  • Переживает убийство процесса — если Android убьёт приложение после ввода panic PIN, WorkManager возобновит задачу при следующем запуске

  • Ожидание сети — если телефон был в авиарежиме, panic-алерт уйдёт сразу при появлении сети

  • Exponential backoff — при ошибке сети: 10с → 20с → 40с → ... Не бросает попытки

  • REPLACE policy — повторный вход в panic mode не создаёт дубликаты

Что делает сервер: получив panic-алерт, сервер расшифровывает payload, проверяет "panic": true, и убивает все активные сессии пользователя. Это значит: никто не сможет подключиться к аккаунту с другого устройства, используя украденный токен.


6. Сетевая изоляция: ни один реальный бит не утечёт

Panic mode бесполезен, если сквозь фейковую базу просвечивают реальные данные. Поэтому в panic mode вся реальная сетевая активность отключена.

6.1. WebSocket заблокирован


// SocketManager.java
public void connect() {    if (SecurityContext.get().isPanicMode()) {        Log.w(TAG, "panic mode active; websocket blocked");        Listener l = listener;        if (l != null) l.onClosed();        return;    }    // ... нормальное подключение
}

В panic mode WebSocket не открывается. Приложение не получает реальных входящих сообщений и не отправляет исходящих. Для UI это выглядит как «нет интернета» — вполне правдоподобно.

6.2. Push-токен не синхронизируется


// PinUnlockActivity.java
private void openMain() {    if (!SecurityContext.get().isPanicMode()) {        PushTokenManager.syncIfPossible(this);  // Только для реального режима    }    startActivity(new Intent(this, ChatActivity.class));    finish();
}

Push-токен FCM/RuStore не отправляется на сервер. Это значит: сервер не будет слать push-уведомления на это устройство, и новые сообщения за реальных собеседников не просочатся через notification bar.

6.3. Визуальная идентичность

ChatActivity открывается одинаково независимо от режима. Тот же список чатов, тот же UI, те же иконки. Наблюдатель, смотрящий через плечо, не заметит разницы.


7. Полный flow: что происходит посекундно


t=0.000  Пользователь вводит panic PIN [5678] в PinUnlockActivity
t=0.001  DatabaseManager.init("5678") вызван
t=0.050  SQLCipher пробует: real_user.db + "5678" → FAIL (неверный ключ)
t=0.100  SQLCipher пробует: decoy_user.db + "5678" → SUCCESS
t=0.110  setActive(decoy, isPanic=true)
├── clearSensitiveStateLocked()  // Затирает bytе[] старого пароля
├── SecurityContext.setPanicMode(true)  // Глобальный volatile флаг
└── PanicModeWorker.enqueue()  // SOS в очередь WorkManager
t=0.120  PinUnlockActivity.openMain()
├── PushTokenManager.syncIfPossible → SKIP (panic mode)
└── startActivity(ChatActivity)
t=0.200  ChatActivity отображает decoy-чаты:
Alex, Mila, Family, Weekend Plan, City News ...
t=0.300  SocketManager.connect() → BLOCKED (panic mode)
→ UI показывает "connecting..." (выглядит как слабый интернет)
t=0.500  PanicModeWorker.startWork()
├── buildRequest(): JSON { type: "metadata_sync", payload: AES(...) }
└── OkHttp POST /api/v1/auth/panic_alert
t=1.200  Сервер получает запрос
├── Расшифровывает payload: { panic: true, action: "kill_sessions" }
└── Инвалидирует все сессии пользователя
Итого: 1.2 секунды от ввода PIN до убийства всех сессий.

8. Модель угроз: от кого защищаем

Adversary

Возможности

Panic Mode защищает?

Пограничный контроль

Визуальный осмотр телефона, 5-10 минут

Да — увидят нормально выглядящий мессенджер с безобидными чатами

Разъярённый партнёр

Требует разблокировать, проверяет чаты

Да — увидят «Alex», «Mila», «Got it, thanks!»

Forensic-аналитик

Полный дамп файловой системы, инструменты типа Cellebrite

Частично — увидят два зашифрованных файла, но SQLCipher с достаточно длинным PIN устойчив

Forensic + знание о Xipher

Знает что Xipher имеет panic mode, ищет decoy_user.db

Слабо — наличие двух файлов БД может вызвать подозрения (см. раздел «Ограничения»)

Cold boot / RAM dump

Извлечение ключей из оперативной памяти

НетwipeChars()/wipeBytes() помогают, но JVM GC может оставить копии


9. Сравнение с аналогами

VeraCrypt Hidden Volume

VeraCrypt Hidden Volume — прямой идеологический предшественник. Два пароля → два раздела диска. Внешний выглядит нормально, внутренний — настоящий.

Чем Panic Mode отличается:

  • Адаптирован для мессенджера, а не файловой системы — генерируются чаты, а не файлы

  • Автоматическая генерация контента (VeraCrypt требует вручную заполнять outer volume)

  • Серверный компонент (kill sessions) — VeraCrypt чисто локальный

Signal — нет аналога

Signal не предлагает правдоподобной отрицаемости. «Disappearing messages» удаляют контент, но не скрывают факт использования мессенджера. Пустой Signal с нулём чатов — сам по себе подозрителен.

Briar — нет аналога

Briar (Tor-based мессенджер для активистов) шифрует все данные локально, но тоже не имеет decoy-режима. При принуждении — либо отдаёшь PIN, либо отказываешься.

Итоговая таблица

Критерий

VeraCrypt

Signal

Briar

Xipher Panic Mode

Два пароля → два набора данных

Да

Нет

Нет

Да

Автогенерация фейков

Нет (вручную)

Да (DecoyDataGenerator)

Структурная мимикрия

Зависит от пользователя

Да (зеркалирует реальные чаты)

Скрытый серверный алерт

Нет

Нет

Нет

Да (AES-GCM metadata_sync)

Kill other sessions

N/A

Нет

Нет

Да (panic → kill_sessions)

Устойчив к forensic

Высоко

Низко

Средне

Средне (см. ограничения)


10. Честные ограничения

10.1. Два файла базы данных — улика

Forensic-аналитик, знающий о Xipher, обнаружит два файла в /data/data/com.xipher/databases/: real_user.db и decoy_user.db. Само наличие двух баз — косвенное доказательство panic mode.

Mitigation (будущее): использовать единый файл с двумя SQLCipher-«страницами» (аналог VeraCrypt, где outer и hidden volume в одном контейнере). Это сложнее реализовать с Room/SQLCipher, но теоретически возможно.

10.2. Имена файлов говорящие

real_user.db и decoy_user.db — очевидные имена. В production стоит использовать нейтральные имена (user_v1.db, user_v2.db), чтобы не было ясно, какой файл «настоящий».

10.3. JVM не гарантирует затирание памяти

wipeChars(passphrase) вызывает Arrays.fill(passphrase, '\0'), но JIT-компилятор может оптимизировать эту запись (dead store elimination). Garbage Collector может создать копии массива при сборке. Для Java/Android это нерешаемая проблема — единственное полное решение — NDK + mlock + explicit_bzero.

10.4. Английский контент фейков

Текущие пулы сообщений — на английском. Для русскоговорящего пользователя англоязычные чаты подозрительны. Необходима локализация пулов под язык устройства.

10.5. Пустая история после входа

В panic mode WebSocket отключён → новые сообщения не приходят. Если следователь наблюдает за телефоном длительное время, отсутствие входящих вызовет вопросы. Частичное решение — генерировать фейковый «incoming message» по таймеру, но это значительно усложняет систему.

10.6. Rubber-hose cryptanalysis

Никакая техническая мера не защищает от достаточно мотивированного физического насилия. Panic Mode снижает порог «знаю, что скрываешь» до «может быть, скрываешь», но не устраняет его полностью. Это инструмент для покупки времени, а не абсолютная защита.


11. Заключение

Panic Mode — это реализация принципа «не скрывай — подменяй» для мессенджера:

  1. Две SQLCipher базы — криптографическая развилка без if/else с PIN

  2. DecoyDataGenerator — зеркалирует структуру реальных чатов, логарифмически масштабирует сообщения, использует SecureRandom

  3. PanicModeWorker — AES-128-GCM panic-алерт, замаскированный под metadata_sync, с гарантией доставки через WorkManager

  4. Сетевая изоляция — WebSocket, push-токены и real-time данные полностью отключены в panic mode

Это не серебряная пуля — у подхода есть конкретные ограничения. Но для сценария «показать телефон за 5 минут» — это разница между «здесь скрытые чаты» и «обычный мессенджер, вот Alex пишет See you at 6».

Что можно забрать в свой проект:

  • Криптографический fork вместо условного — пусть шифрование решает, какой путь выбрать, а не ваш код

  • Структурная мимикрия — фейковые данные должны повторять форму реальных

  • Камуфляж сетевых запросов — критические сигналы должны быть неотличимы от рутинного трафика

  • Логарифмическое масштабирование — универсальный приём для «достаточно, чтобы выглядело реально, не слишком, чтобы было дорого»


Источники и дальнейшее чтение

Plausible deniability:

SQLCipher:

Mobile forensics:

Исходный код проекта: github.com/w78flezeex/Xipher.pro (copyleft-лицензия)


Это вторая статья из серии о криптографической архитектуре Xipher. Первая была про защиту метаданных (Metadata Guard). Если интересен разбор E2EE-слоя (X25519 + Double Ratchet) — пишите в комментариях.