«Мы знаем, что вы вчера в 23:47 переписывались с Алексеем 14 минут. О содержании разговора нам неизвестно.» — Так выглядит мир, где сообщения зашифрованы, а метаданные — нет.
Привет, Хабр! Я занимаюсь разработкой open-source мессенджера (проект Xipher, C++/Android), и один из компонентов, который пришлось проектировать с нуля — защита метаданных. Не содержимого сообщений (E2EE сейчас есть у всех), а информации о самом факте общения: кто с кем, когда, сколько раз.
В этой статье я подробно разберу инженерные решения, к которым пришёл, — от криптографических примитивов до С++ кода и SQL-схемы. Все примеры — из реального работающего кода. В конце честно расскажу, где подход имеет ограничения и чем отличается от того, что делают Signal и Tor.
��сходники проекта открыты — ссылка на GitHub в конце статьи, если захотите покопаться или раскритиковать.
Содержание
Timing Obfuscation: случайные задержки против корреляционного анализа
Полная картина: что видит атакующий с root-доступом к серверу
1. Зачем вообще шифровать метаданные?
Большинство «защищённых» мессенджеров гордо заявляют: «Мы используем E2EE!». И это правда — AES-256-GCM + X25519 + HKDF делают содержимое сообщений нечитаемым для сервера. Вот только сервер всё равно знает:
Метаданные | Что из них можно извлечь |
|---|---|
Кто с кем общ��ется (sender_id → receiver_id) | Социальный граф пользователя |
Когда (timestamp отправки/доставки) | Режим дня, часовой пояс, срочность |
Как часто (кол-во сообщений за период) | Интенсивность отношений |
Статус «онлайн» | Привычки, реальное местоположение |
Typing indicators | Что человек собирался написать, но передумал |
Read receipts | Скорость реакции, приоритетность собеседника |
Push-уведомления (payload в FCM/RuStore) | Google/VK знают receiver_id + preview |
User-Agent | Модель устройства, версия ОС, fingerprint |
Исследование «Metadata: Piecing Together a Privacy Solution» от EFF наглядно показало: метаданные телефонных звонков позволяют восстановить круг общения, рабочий график и политические предпочтения человека — без единого слова из содержания разговоров.
Бывший директор АНБ Майкл Хейден прямо сказал: «We kill people based on metadata.»
Вывод: E2EE без защиты метаданных — это бронированная дверь с прозрачными стенами.
2. Модель угроз: что знает сервер
Прежде чем писать код, стоит определить модель угроз по методологии STRIDE. Adversary — кто-то с полным root-доступом к серверу: может читать БД, логи, память процесса и перехватывать сетевой трафик на уровне ОС.
Это не параноидальный сценарий. Это реальность для:
Администратора хостинга (Hetzner, VK Cloud, AWS — у них есть физический доступ к железу)
Государственных органов с ордером на выемку сервера
Атакующего, эксплуатирующего RCE-уязвимость в серверном коде
Вопрос: что он должен НЕ мочь узнать?
Цель: минимизировать знание сервера о паттернах коммуникации (traffic analysis resistance).
Задача разбивается на слои — от «скрыть превью в push» до «сервер вообще не знает, кто с кем общается». В итоге получилось 4 уровня защиты.
3. Архитектура: четыре уровня скрытности
Level 0 — Off Обычный режим, вся метадата хранится нормально Level 1 — Basic Анонимизация push, отключение activity/typing/read/online Level 2 — Enhanced + редакция WS-логов, удаление User-Agent из сессий Level 3 — Maximum + анонимные пары (HMAC-SHA256), timing jitter, cover traffic
Каждый уровень включает все предыдущие. Вот как это выглядит в C++:
enum class StealthLevel { Off = 0, Basic = 1, Enhanced = 2, Maximum = 3 }; struct StealthSettings { StealthLevel level = StealthLevel::Off; bool hide_push_content = false; // Не отправлять preview в push bool hide_push_sender = false; // Не отправлять имя отправителя bool anonymize_activity = false; // Не обновлять last_activity bool disable_typing_events = false; // Подавить "печатает..." bool disable_read_receipts = false; // Подавить "прочитано" bool disable_online_status = false; // Не транслировать онлайн/оффлайн bool strip_user_agent = false; // Не сохранять User-Agent bool anonymize_ws_logs = false; // Редактировать WS-логи bool use_anonymous_pairs = false; // Хранить сообщения под хеш-парой bool dummy_traffic = false; // Генерировать фиктивный трафик };
Преобразование уровня в набор флагов — тривиальная функция:
StealthSettings MetadataGuard::levelToSettings(StealthLevel level) { StealthSettings s; s.level = level; switch (level) { case StealthLevel::Basic: s.hide_push_content = s.hide_push_sender = true; s.anonymize_activity = s.disable_typing_events = true; s.disable_read_receipts = s.disable_online_status = true; break; case StealthLevel::Enhanced: // ... всё от Basic + s.strip_user_agent = s.anonymize_ws_logs = true; break; case StealthLevel::Maximum: // ... всё от Enhanced + s.use_anonymous_pairs = s.dummy_traffic = true; break; } return s; }
Почему не один bool stealth_enabled?
Потому что гранулярность критична. Защита метаданных — это всегда компромисс с UX. Пользователь на Level 1 жертвует удобством (нет typing, нет read receipts), но сохраняет push-уведомления. Level 3 жертвует всем (включая push-доставку через FCM), но получает максимальную устойчивость к traffic analysis.
Этот паттерн — graduated privacy — хорошо известен в академической литературе. Например, в работе Tyagi et al. Stadium: A Distributed Metadata-Private Messaging System (SOSP '17) проанализированы компромиссы между уровнем приватности метаданных и пропускной способностью системы.
4. HMAC-SHA256 Anonymous Pairs — ядро системы
Это самая интересная часть. На уровне Maximum вместо хранения (sender_id, receiver_id) в базе данных мы заменяем идентификаторы пользователей криптографическими хешами.
4.1. Проблема
В обычном мессенджере таблица сообщений выглядит так:
-- Обычный мессенджер INSERT INTO messages (sender_id, receiver_id, encrypted_content, ...) VALUES ('alice-uuid', 'bob-uuid', 'encrypted-blob', ...);
Даже если encrypted_content невозможно прочитать, поля sender_id и receiver_id — это прямой социальный граф в открытом виде. Атакующий с доступом к БД мгновенно восстановит, кто с кем общается.
4.2. Решение: HMAC-SHA256 с глобальной солью
Мы заменяем пару (alice, bob) на детерминированный, но необратимый хеш:
pair_hash = HMAC-SHA256(global_salt, min(alice, bob) || "||" || max(alice, bob)) sender_tag = HMAC-SHA256(pair_hash, sender_id)[0:16]
Вот как это выглядит в C++:
std::string MetadataGuard::computePairHash( const std::string& user_a, const std::string& user_b ) { ensureSalt(); // Детерминированный порядок: всегда min||max std::string ordered; if (user_a < user_b) { ordered = user_a + "||" + user_b; } else { ordered = user_b + "||" + user_a; } return hmacSha256(pair_hmac_salt_, ordered); } std::string MetadataGuard::computeSenderTag( const std::string& sender_id, const std::string& pair_hash ) { std::string full_tag = hmacSha256(pair_hash, sender_id); // Первые 16 hex-символов (8 байт) — короткий тег return full_tag.substr(0, 16); }
Критические свойства:
Свойство | Гарантия |
|---|---|
Детерминированность |
|
Необратимость | Без |
Уникальность тегов |
|
Collision resistance | HMAC-SHA256 даёт 256 бит — вероятность коллизии порядка 2^(-128) |
4.3. Глобальная соль: линия жизни системы
global_salt — это 32-байтный случайный ключ, без которого вся схема разваливается. Если атакующий получит соль, он сможет перебрать все пары пользователей и раскрыть граф.
void MetadataGuard::ensureSalt() { std::lock_guard lock(salt_mutex_); if (!pair_hmac_salt_.empty()) return; // 1. Попробовать загрузить из БД if (db_ && initialized_) { std::string stored = db_->getAdminSetting("metadata_guard_salt"); if (!stored.empty()) { pair_hmac_salt_ = stored; return; } } // 2. Сгенерировать новую (CSPRNG) unsigned char salt_bytes[32]; if (RAND_bytes(salt_bytes, 32) != 1) { // Fallback: time-based seed (только если OpenSSL CSPRNG недоступен) std::random_device rd; std::mt19937_64 gen(rd()); for (int i = 0; i < 32; ++i) salt_bytes[i] = static_cast<unsigned char>(gen() & 0xFF); } // 3. Сохранить в admin_settings pair_hmac_salt_ = toHex(salt_bytes, 32); db_->upsertAdminSetting("metadata_guard_salt", pair_hmac_salt_); }
Критическое замечание: хранение соли в той же БД, что и хеши — осознанный компромисс. Если атакующий имеет доступ к anonymous_messages, он, вероятно, имеет доступ и к admin_settings. В production правильнее хранить соль в HSM (HashiCorp Vault, AWS KMS, YC KMS) — так, чтобы компрометация БД не давала возможности раскрутить хеши. Это known limitation текущей реализации (подробнее в разделе «Ограничения»).
4.4. Как выглядит БД с анонимными парами
Вместо обычной таблицы messages создаётся отдельная anonymous_messages:
CREATE TABLE anonymous_messages ( id BIGSERIAL PRIMARY KEY, pair_hash VARCHAR(64) NOT NULL, -- HMAC-SHA256 хеш пары sender_tag VARCHAR(16) NOT NULL, -- Кто из пары отправил (8 байт hex) encrypted_content TEXT NOT NULL, -- E2EE зашифрованный контент nonce VARCHAR(32) NOT NULL, -- Nonce для AES-256-GCM ephemeral_key VARCHAR(64), -- Эфемерный ключ (если нужен) content_type VARCHAR(20) DEFAULT 'text', -- Тип контента created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_anon_msg_pair ON anonymous_messages(pair_hash, created_at DESC);
Запрос на вставку:
INSERT INTO anonymous_messages (pair_hash, sender_tag, encrypted_content, nonce, ephemeral_key, content_type) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, created_at;
Что видит атакующий с root-доступом:
pair_hash: "a7f3b2c1e4d6..." -- Какая-то пара общается sender_tag: "3e8f1a2b" -- Один из двоих что-то написал content: "U2FsdGVkX1..." -- Зашифрованный блоб
Он не может определить: это Alice->Bob или Carol->Dave? Сколько вообще уникальных людей стоит за этими хешами? Без global_salt — невозможно.
4.5. Получение сообщений: серверная сторона
Получатель запрашивает сообщения, предоставляя свой токен и peer_id. Сервер вычисляет pair_hash заново (потому что знает оба ID в момент запроса) и фильтрует по нему:
std::string RequestHandler::handleStealthGetMessages(const std::string& body) { // ... парсинг token, peer_id ... auto& guard = MetadataGuard::getInstance(); std::string pair_hash = guard.computePairHash(user_id, peer_id); std::string my_sender_tag = guard.computeSenderTag(user_id, pair_hash); const char* params[2] = {pair_hash.c_str(), limit_s.c_str()}; std::string sql = "SELECT id, sender_tag, encrypted_content, nonce, ... " "FROM anonymous_messages " "WHERE pair_hash = $1 " "ORDER BY created_at DESC LIMIT $2::int"; // Для каждого сообщения: is_mine = (sender_tag == my_sender_tag) }
Обратите внимание: сервер вычисляет pair_hash только на лету, в момент конкретного запроса. В базе хранятся только хеши — mapping user_id -> pair_hash нигде не персистируется.
5. Отравление собственных логов
На уровне Enhanced (Level 2+) WebSocket-сообщения в серверных логах редактируются перед записью:
std::string MetadataGuard::sanitizeLogMessage( const std::string& raw_message, const StealthSettings& settings ) { if (!settings.anonymize_ws_logs) { return raw_message; // Level 0-1: логируем как есть } try { auto data = JsonParser::parse(raw_message); std::string type = data.count("type") ? data["type"] : "unknown"; // Оставляем только тип и размер std::ostringstream oss; oss << "{\"type\":\"" << type << "\",\"size\":" << raw_message.size() << ",\"redacted\":true}"; return oss.str(); } catch (...) { // Если парсинг упал — всё равно редактируем return "{\"redacted\":true,\"size\":" + std::to_string(raw_message.size()) + "}"; } }
Было:
{"type":"message","content":"Привет!","sender_id":"abc-123","receiver_id":"def-456"}
Стало:
{"type":"message","size":82,"redacted":true}
Зачем тип? Для диагностики. Если сервер падает, нам нужно знать какой тип сообщения вызвал проблему (message, typing, ack и т.д.), но не содержание.
6. Push-уведомления как канал утечки
Э��о неочевидный, но критический вектор. Когда сервер отправляет push через Firebase Cloud Messaging (Google) или RuStore Push, payload проходит через серверы третьей стороны. Если push содержит sender_id и preview — Google/VK знают, кто кому пишет.
6.1. Санитизация payload (Level 1+)
std::map MetadataGuard::sanitizePushPayload( const std::map& original_payload, const StealthSettings& settings ) { if (settings.level == StealthLevel::Off) return original_payload; std::map<std::string, std::string> sanitized; // Всегда оставляем type для маршрутизации на клиенте sanitized["type"] = original_payload.at("type"); if (settings.hide_push_sender) { sanitized["sender_id"] = "anonymous"; // Google видит "anonymous" sanitized["chat_title"] = ""; sanitized["title"] = ""; } if (settings.hide_push_content) { sanitized["body"] = ""; // Preview стёрт sanitized["message_type"] = "hidden"; } return sanitized; }
Поле | Обычный режим | hide_push_sender | hide_push_content |
|---|---|---|---|
|
|
| — |
|
|
| — |
|
| — |
|
6.2. Level 3: полный отказ от push через FCM/RuStore
На максимальном уровне push-уведомления полностью отключены:
// request_handler_stealth.cpp — handleStealthSendAnonymous() // Push notifications intentionally omitted for anonymous messages. // Sending through FCM/RuStore would leak receiver_id to Google/VK, // defeating the purpose of metadata-hidden communication.
Получатель узнаёт о новых сообщениях через WebSocket (если подключён) или polling. Да, это жертва UX ради приватности. На Level 3 пользователь осознанно принимает это решение.
7. Timing Obfuscation: случайные задержки
Даже если атакующий не знает sender_id и receiver_id, он может коррелировать по времени: «пользователь A отправил что-то в 14:32:05.123, пользователь B получил что-то в 14:32:05.456 — вероятно, это одна пара».
На уровне Maximum мы добавляем случайную задержку 0–2000 мс:
int MetadataGuard::getTimingJitter(const std::string& user_id) { if (getStealthLevel(user_id) < StealthLevel::Maximum) { return 0; } // Thread-local MT19937 с аппаратным seed static thread_local std::mt19937 rng(std::random_device{}()); std::uniform_int_distribution<int> dist(0, 2000); return dist(rng); }
Перед доставкой сообщения сервер ждёт getTimingJitter() миллисекунд. Пара (14:32:05.123 отправил) -> (14:32:06.847 получил) уже не поддаётся тривиальной корреляции.
Почему 0–2000 мс?
Меньше — недостаточно шума для маскировки
Больше — заметная задержка в чате, ухудшение UX
Равномерное распределение — каждая задержка равновероятна
Дополнительно на Level 3 включается Cover Traffic (Dummy Traffic) — генерация фиктивных пакетов, чтобы наблюдатель не мог отличить реальное сообщение от шума по объёму трафика.
8. Мердж настроек: дипломатия приватности
Вот элегантная проблема: Alice на Level 2, Bob на Level 0. Какой уровень защиты применять к их диалогу?
Ответ: максимальный из двух. Каждый флаг мержится через OR:
StealthSettings MetadataGuard::getConversationStealthSettings( const std::string& user_a, const std::string& user_b ) { auto sa = getStealthSettings(user_a); auto sb = getStealthSettings(user_b); StealthSettings merged; merged.level = (sa.level > sb.level) ? sa.level : sb.level; merged.hide_push_content = sa.hide_push_content || sb.hide_push_content; merged.hide_push_sender = sa.hide_push_sender || sb.hide_push_sender; merged.anonymize_activity = sa.anonymize_activity || sb.anonymize_activity; merged.disable_typing_events = sa.disable_typing_events || sb.disable_typing_events; merged.disable_read_receipts = sa.disable_read_receipts || sb.disable_read_receipts; merged.disable_online_status = sa.disable_online_status || sb.disable_online_status; // ... и так для всех флагов return merged; }
Философия: если хотя бы один участник хочет приватности — приватность применяется ко всему диалогу. Bob не может «заставить» Alice раскрыть метаданные.
Это аналогично Strict-Transport-Security в HTTPS — если сервер запросил HSTS, клиент не может откатиться на HTTP.
9. Хранение и production-реалии
9.1. Кеширование настроек
Запрос к PostgreSQL на каждое сообщение — накладно. Поэтому MetadataGuard кеширует настройки в unordered_map с мьютексом:
class MetadataGuard { std::unordered_map settings_cache_; mutable std::mutex cache_mutex_; StealthSettings getStealthSettings(const std::string& user_id) { { std::lock_guard<std::mutex> lock(cache_mutex_); auto it = settings_cache_.find(user_id); if (it != settings_cache_.end()) return it->second; } StealthSettings settings = loadFromDb(user_id); { std::lock_guard<std::mutex> lock(cache_mutex_); settings_cache_[user_id] = settings; } return settings; } };
9.2. Singleton и потокобезопасность
MetadataGuard — singleton (Meyer's singleton через static local). Все мутабельные поля защищены мьютексами. getTimingJitter использует thread_local RNG — без блокировок на горячем пути.
9.3. Подавление побочных эффектов
Каждый модуль сервера перед выполнением действия проверяет stealth-настройки:
// Перед обновлением активности: if (!MetadataGuard::getInstance().shouldSkipActivityUpdate(user_id)) { db_manager_.updateLastActivity(user_id); } // Перед отправкой typing indicator: if (!MetadataGuard::getInstance().shouldSuppressTyping(user_id)) { ws_sender_(peer_id, typing_payload); } // Перед записью User-Agent в сессию: if (!MetadataGuard::getInstance().shouldStripUserAgent(user_id)) { session.user_agent = request.headers["User-Agent"]; }
Это инверсия обычного подхода: вместо «включить защиту» мы «выключаем утечки» — каждая потенциальная точка утечки явно гейтится.
10. Полная картина: что видит атакующий с root-доступом
Уровень | Что знает атакующий | Что НЕ знает |
|---|---|---|
Level 0 | Всё: кто, с кем, когда, метаданные | Содержание (E2EE) |
Level 1 | Кто с кем (из БД), когда (из БД) | Preview в push, typing, read receipts, online |
Level 2 | Кто с кем (из БД), когда (из БД) | + содержание WS-логов, User-Agent |
Level 3 | Хеши пар (без salt — нераскрываемые), приблизительное время | Кто с кем, точное время, все индикаторы, push-получатель |
На Level 3 атакующий видит:
anonymous_messages: pair_hash = "9a2f...c4d1" -- Какая-то пара (которая из 10000?) sender_tag = "e7b3a1f2" -- Один из двоих encrypted_content = "..." -- Нечитаемо (E2EE) created_at = 14:32:06.847 -- Время +/- jitter stealth_settings: user_id = -- Этот user на Level 3 Нет в anonymous_messages: sender_id, receiver_id, username Нет в WS-логах: содержание, идентификаторы Нет в push: ничего (push отключён на L3)
11. Сравнение с аналогами: Signal, Tor, Matrix
Было бы нечестно рассказывать про свой подход, не сравнив его с тем, что делают другие. Вот объективное сравнение:
Signal — Sealed Sender
Signal решает проблему защиты отправителя с помощью Sealed Sender: сообщение помещается во «внешний конверт», зашифрованный ключом сервера, но без поля sender_id. Сервер знает только получателя, не отправителя.
Преимущества Signal по сравнению с Metadata Guard:
Sealed Sender прошёл формальный аудит безопасности
Защита отправителя работает без потери UX (push-уведомления сохраняются)
Реализация значительно проще и элегантнее
Преимущества Metadata Guard:
На Level 3 скрыт не только отправитель, но и получатель (pair_hash скрывает обоих)
Graduated privacy: пользователь выбирает точку компромисса, а не бинарный вкл/выкл
Защита не только «кто отправил», но и timing, activity, typing, push payload
Tor / Mixnet-подход
Tor и mix-сети (например, Nym) решают задачу на сетевом уровне: сообщения проходят через цепочку прокси-серверов, каждый из которых снимает один слой шифрования. Наблюдатель не может связать входящий и исходящий трафик.
Преимущества Tor/Mixnet:
Математически доказуемая анонимность (при достаточном числе узлов)
Защита от глобального наблюдателя (network-level adversary)
Не требует доверия к серверу приложения
Почему я не использовал этот подход:
Высокая задержка (Tor: 1-5 сек, mixnet: до 30 сек) — неприемлемо для мессенджера
Сложность реализации для мобильных клиентов
Зависимость от инфраструктуры третьей стороны
Matrix / Element
Matrix Protocol использует E2EE (Megolm), но метаданные (sender, receiver, room_id, timestamp) хранятся в открытом виде на homeserver. Проект Matrix Sliding Sync улучшает UX, но не метаданные.
Итоговая таблица
Критерий | Signal (Sealed Sender) | Tor/Mixnet | Metadata Guard |
|---|---|---|---|
Скрыт отправитель | Да | Да | Да (Level 3) |
Скрыт получатель | Нет | Да | Да (Level 3, pair_hash) |
Защита тайминга | Нет | Да (onion routing) | Частичная (jitter 0-2с) |
Push-уведомления | Сохранены | N/A | Отключены на L3 |
Задержка | ~0 мс | 1000-5000 мс | 0-2000 мс |
Формальный аудит | Да | Да | Нет |
Гранулярность | Бинарная | Бинарная | 4 уровня |
Честный вывод: Signal Sealed Sender — более зрелое решение с формальным аудитом. Tor-подход — теоретически сильнее. Metadata Guard пытается занять нишу «сильнее Signal по метаданным, практичнее Tor по задержке», но пока не прошла независимый аудит.
12. Честные ограничения и что не работает
Ни одна система безопасности не идеальна. Вот чего Metadata Guard не решает и где есть известные слабости:
12.1. Соль в одной БД с хешами
Как упоминалось выше, global_salt хранится в admin_settings — в той же PostgreSQL, что и anonymous_messages. Атакующий с полным дампом БД получит и соль, и хеши, и сможет перебрать все пары пользователей (если количество пользователей невелико).
Mitigation: перенос соли в HSM/Vault. В идеале — использовать разные мастер-ключи для разных компонентов с ротацией.
12.2. Размер множества анонимности
При 100 пользователях и Level 3 атакующий знает, что pair_hash — это одна из C(100, 2) = 4950 возможных пар. При 1000 пользователях — одна из примерно 500 000. Для по-настоящему сильной анонимности нужна большая пользовательская база.
Это фундаментальное ограничение любой системы, основанной на k-anonymity: чем меньше пользователей, тем слабее анонимность.
12.3. Сервер знает оба ID в момент API-запроса
Когда Alice вызывает GET /api/stealth/get-messages?peer_id=bob, сервер в runtime знает оба ID (из токена и из запроса). pair_hash защищает данные at rest (в БД, бэкапах, логах), но не in transit (в памяти процесса).
Для защиты in-transit нужен подход Signal (Sealed Sender) или Tor. Это принципиальное архитектурное ограничение клиент-серверной модели.
12.4. Timing jitter — не панацея
Равномерный jitter 0-2000 мс ломает тривиальную корреляцию, но не защищает от статистического анализа на больших объёмах. Если атакующий наблюдает 10 000 сообщений между двумя парами, он может восстановить паттерн даже с jitter.
Для серьёзной timing protection нужен constant-rate padding (как в Vuvuzela) — но это кратно увеличивает нагрузку на сервер.
12.5. Cover Traffic масштабируется плохо
Dummy traffic на Level 3 увеличивает нагрузку на WebSocket-соединения и сетевой трафик пользователя. На мобильных устройствах с ограниченным трафиком это может быть проблемой.
12.6. Нет независимого аудита
Код открыт, но формального аудита от независимой security-компании (Cure53, Trail of Bits, NCC Group) пока не проводилось. Для любого security-sensitive проекта это критический пробел.
Как это вписывается в общую архитектуру шифрования
Для контекста: Metadata Guard — один из трёх криптографических слоёв мессенджера:
+----------------------------------------------------------+ | Слой 1: E2EE (содержание) | | X25519 key exchange -> HKDF-SHA256 -> AES-256-GCM | | + Message Ratchet (forward secrecy) | +----------------------------------------------------------+ | Слой 2: Metadata Guard (метаданные) <-- эта статья| | HMAC-SHA256 anonymous pairs + push sanitization | | + timing obfuscation + WS log redaction | +----------------------------------------------------------+ | Слой 3: Provable Chat (доказуемость) | | Ed25519 подписи + SHA-256 hash chain | | Неотрекаемость: блокчейн для переписки | +----------------------------------------------------------+
E2EE и Metadata Guard ортогональны: E2EE скрывает что сказано, Metadata Guard скрывает кто, когда, с кем и сколько. Provable Chat, наоборот, делает переписку доказуемой — каждое сообщение криптографически подписано и связано в цепочку. Если интересно, могу написать отдельный разбор каждого слоя — дайте знать в комментариях.
13. Заключение
Защита метаданных — это не бинарный переключатель, а спектр компромиссов между удобством и приватностью. Подход с 4 уровнями позволяет пользователю самому выбрать точку на этом спектре:
Level 0: «Мне нечего скрывать» — полный функционал
Level 1: «Скройте preview в push» — минимальная жертва UX
Level 2: «Почистите логи» — серьёзнее относящиеся к приватности
Level 3: «Сервер не должен знать, с кем я говорю» — максимальная защита, осознанная жертва UX
Что можно забрать в свой проект, даже если вы не пишете мессенджер:
HMAC-SHA256 для деперсонализации связей — подходит для любой системы, где нужно хранить связи между сущностями, не раскрывая их (медицинские данные, HR-системы, аналитика)
Мердж настроек по OR — паттерн «приватность одного защищает обоих» применим к любым двусторонним взаимодействиям
Послойная санитизация — архитектурный принцип: каждая точка выхода данных (push, лог, API-ответ) гейтится через единый policy-объект
Graduated privacy — вместо «вкл/выкл» дайте пользователю шкалу
Источники и дальнейшее чтение
Академические работы:
Danezis G., Diaz C. — A Survey of Anonymous Communication Channels (Microsoft Research, 2008)
Van den Hooff J. et al. — Vuvuzela: Scalable Private Messaging Resistant to Traffic Analysis (OSDI '15)
Tyagi N. et al. — Stadium: A Distributed Metadata-Private Messaging System (SOSP '17)
Angel S. et al. — Unobservable Communication over Fully Untrusted Infrastructure (OSDI '16)
Индустрия:
Стандарты:
Исходный код проекта: github.com/w78flezeex/Xipher.pro (copyleft-лицензия)
Буду рад конструктивной критике в комментариях — особенно от тех, кто работал с traffic analysis или проводил аудиты мессенджеров. Если есть вопросы по конкретным решениям или хотите увидеть разбор E2EE-слоя — пишите, сделаю отдельную статью.