Криптографически доказуемые чаты в Xipher: как я собрал Ed25519, хеш-цепочку и sequence в одну систему
Мне 18 лет, и последние несколько месяцев я почти безостановочно пишу Xipher - свой мессенджер с нуля: бэкенд на C++, клиент на Kotlin под Android. В какой‑то момент стало мало просто "безопасного мессенджера", захотелось функции, которой я не нашёл ни в одном массовом продукте: режима, где историю чата нельзя подделать вообще никому - ни участникам, ни администратору сервера - и это можно проверить независимым способом, имея лишь один файл с перепиской.
Так родился режим Xipher Provable Chat - криптографически доказуемые чаты. Ниже разбираю, как это устроено внутри, какие криптографические идеи я использовал и с какими нюансами столкнулся при реализации.
---
Зачем вообще нужна доказуемость переписки
Обычное сквозное шифрование (E2E) решает задачу приватности: посторонний не может прочитать сообщения. Но E2E никак не доказывает, что конкретная переписка не была изменена задним числом. Удалить сообщение, подправить текст, "задвинуть" невыгодную реплику - всё это в классических мессенджерах зачастую никак не видно.
Доказуемость - это про другое:
- Защита от подделки истории: нельзя тихо удалить или вставить сообщение в середину диалога.
- Фиксация авторства: видно, какой пользователь что именно подписал своим ключом.
- Жёсткая привязка к времени: сервер маркирует каждое сообщение меткой времени, и изменить эту дату клиент не может.
Типичные сценарии, где такая функция полезна:
- деловые переписки, в которых важно подтвердить договорённости;
- спорные ситуации, когда слово "чей‑то" против "чьего‑то" и нужно объективное доказательство;
- случаи, когда вам важно иметь историю, которую можно предъявить как цифровой аналог "подписанного документа".
Цель: сделать так, чтобы любой третий человек, не имея доступа к моему серверу, мог взять один JSON‑файл с историей чата и сам проверить, что:
1. сообщения не менялись;
2. ничего не потеряно и не добавлено;
3. подписи корректны и принадлежат тем пользователям, за которых они себя выдают.
---
Базовая идея: применить принципы блокчейна к чату
Я не изобретал новую криптографию. Задача "сделать последовательность записей неизменяемой" давно решена в блокчейнах. Я взял те же принципы - цифровые подписи, хеш-цепочку, монотонный порядковый номер - и адаптировал их под чат:
- никаких токенов,
- никакого майнинга,
- никакой децентрализации.
Просто линейная, криптографически защищённая цепочка сообщений внутри конкретного диалога.
---
Три криптографических столпа
Система опирается сразу на три вещи:
1. Цифровые подписи Ed25519
2. Хеш-цепочка сообщений
3. Монотонный sequence (порядковый номер)
Вместе они дают свойство: историю либо видно как целую и непротиворечивую, либо верификатор сразу находит место, где что‑то не сходится.
1. Ed25519: подпись каждого сообщения
Когда пользователь включает режим доказуемого чата, его клиент генерирует пару ключей Ed25519.
- На Android это делается на Kotlin.
- На сервере реализована поддержка через C++ с использованием EVP API OpenSSL (для верификации и работы с публичными ключами).
Важно: приватный ключ никогда не покидает устройство. На сервер отправляется только публичная часть, которая хранится в специальной таблице.
Почему именно Ed25519, а не, скажем, RSA или ECDSA:
- очень компактные ключи (32 байта) и подписи;
- высокая скорость генерации подписи и проверки;
- алгоритм *детерминированный*: одни и те же данные → всегда одна и та же подпись.
Детерминизм тут критичен: это упростило проверку и повторную генерацию подписей при тестах, а также исключило влияние случайности на результат.
Каждое отправляемое сообщение в доказуемом режиме:
1. канонически сериализуется;
2. хешируется;
3. этот хеш подписывается приватным ключом автора.
2. Хеш-цепочка: каждое сообщение "сцеплено" с предыдущим
Чтобы нельзя было незаметно удалить или вставить запись в середину, каждое новое сообщение включает в свои данные хеш предыдущего сообщения.
Упрощённый канонический формат данных для хеширования можно описать так:
- `sequence` (порядковый номер),
- `timestamp`,
- `sender_id`,
- `sender_public_key`,
- `previous_hash`,
- `message_body`.
Всё это объединяется в одну строку в строго заданном порядке, с фиксированным разделителем (`n`) и кодировкой UTF‑8. Полученную строку хешируем, и так мы получаем `current_hash`, на который уже и ставится цифровая подпись.
Ключевое слово - детерминированность:
- реализация на Kotlin и реализация на C++ обязаны выдавать одинаковый хеш для одинакового набора полей;
- любое отличие в порядке полей, лишний пробел или другая кодировка сразу ломают верифицируемость.
Чтобы исключить такие баги, я сделал сквозной тест: одни и те же тестовые данные последовательно прогоняются через Kotlin‑модуль и C++‑движок - и сравниваются хеши.
3. Монотонный sequence
Каждое криптографически доказуемое сообщение получает строго возрастающий порядковый номер `sequence` внутри чата.
Это даёт сразу несколько свойств:
- если кто‑то попытается переставить сообщения, порядок номеров сразу покажет несоответствие;
- вставка сообщения "задним числом" невозможна без нарушения линейной последовательности;
- удаление записи приведёт к "дырке" в последовательности, которую можно детектировать при проверке.
При верификации proof‑документа цепочка считается корректной только если номера идут подряд: 1, 2, 3, 4... без пропусков и повторов.
---
Серверная часть: API и хранение в PostgreSQL
Для поддержки доказуемых чатов на сервере Xipher я добавил восемь специализированных API‑эндпоинтов.
Эндпоинты Xipher Provable Chat
- `POST /api/provable/register-key`
Сохраняет публичный ключ Ed25519 для конкретного пользователя.
- `POST /api/provable/enable`
Активирует доказуемый режим для заданного чата.
- `POST /api/provable/status`
Возвращает текущее состояние цепочки: последний `sequence`, `last_hash` и служебные данные.
- `POST /api/provable/send-message`
Принимает очередное сообщение от клиента, вместе с подписью и метаданными.
- `POST /api/provable/messages`
Отдаёт фрагмент цепочки с пагинацией - для подгрузки истории.
- `POST /api/provable/export`
Формирует proof‑документ (целостный JSON с куском или всей цепочкой).
- `POST /api/provable/verify`
Проверяет переданный proof‑документ на целостность и корректность подписей.
Отдельно реализована инфраструктура для генерации и хранения ключей, логики включения режима и поддержки монотонного sequence.
Хранение в PostgreSQL
В базе данных используются три основные таблицы, каждая отвечает за свой слой:
1. Таблица публичных ключей
Хранит сопоставление `user_id → публичный ключ Ed25519`, а также метаданные (дата регистрации ключа и т.п.).
2. Таблица чатов с провинцией режима
Фиксирует, для каких чатов включён доказуемый режим и в каком состоянии находится цепочка.
3. Таблица записей цепочки
Наиболее важная: хранит для каждого сообщения:
- `sequence`;
- `timestamp` (задаётся только сервером);
- `sender_id`;
- `sender_public_key` (на момент сообщения);
- `previous_hash`;
- `current_hash`;
- сам текст сообщения и подпись.
Отдельно стоит подчеркнуть: `sender_public_key` хранится прямо в записи цепочки, а не только в таблице ключей. Это сделано для того, чтобы при ротации ключей старые proof‑документы оставались верифицируемыми: каждое сообщение содержит именно тот ключ, который был актуален в момент его подписания.
---
Как устроен поток отправки сообщения
Сценарий отправки в доказуемом режиме выглядит так:
1. Клиент делает запрос `POST /api/provable/status`, чтобы получить:
- последний `sequence`;
- последний `hash`;
- текущее состояние режима.
2. На основе этих данных клиент формирует новое сообщение:
- выставляет `sequence = last_sequence + 1`;
- указывает `previous_hash = last_hash`;
- добавляет тело сообщения и метаданные.
3. Всё это сериализуется в каноническом формате, хешируется и подписывается приватным ключом Ed25519.
4. Клиент отправляет структуру на сервер через `POST /api/provable/send-message`.
5. Сервер:
- проверяет подпись;
- сверяет `sequence` и `previous_hash` с данными своей цепочки;
- фиксирует свой `timestamp` (чтобы клиент не мог подделать дату);
- записывает всё в базу и обновляет состояние.
Если хоть один из шагов верификации ломается (неверная подпись, неверный previous_hash, пропуск sequence), сообщение не попадает в цепочку.
---
Клиентская часть: Kotlin как зеркало C++‑движка
На стороне Android‑клиента есть модуль `ProvableCrypto.kt`. По сути, это почти зеркальная реализация того, что находится в `provable_chain.cpp` на сервере.
Требования к этому модулю очень строгие:
- одинаковая схема сериализации;
- одинаковое задание кодировки (UTF‑8);
- те же правила по разделителям и порядку полей.
Именно поэтому я создал единый набор тестовых векторов, которые регулярно гоняю через обе реализации. Любое расхождение в хеше или подписи сразу ловится на тестах, а не в продакшене.
---
Proof‑документ: один JSON для полной проверки
Любой фрагмент переписки в доказуемом чате Xipher можно экспортировать в самодостаточный JSON‑документ.
В него входят:
- последовательность сообщений (цепочка);
- все необходимые публичные ключи;
- хеши, подписи и служебные поля;
- данные о чате и участниках, достаточные для проверки.
Этот JSON не требует подключения к серверу: его можно хранить локально, передавать по почте, сохранять как архив переписки.
Как проверить такой документ
Верификация возможна тремя основными способами:
1. Через сервер Xipher
Отправить proof‑документ на эндпоинт `POST /api/provable/verify` и получить результат проверки.
2. Офлайн на устройстве
Вызвать на клиенте функцию `ProvableCrypto.verifyChain()` - она перехеширует всю цепочку, проверит подписи и корректность sequence.
3. На любом компьютере вручную
Используя стороннюю библиотеку или простую реализацию SHA‑256 и Ed25519‑verify. Минимальная версия такой проверки укладывается примерно в пару десятков строк на обычном скриптовом языке.
---
Что оказалось сложнее, чем казалось
1. Детерминизм хешей
Поначалу я столкнулся с неприятной проблемой: клиент и сервер выдавали разные хеши для, казалось бы, одинаковых входных данных.
Виновником оказался один тестовый UUID, содержащий пробел, который по‑разному сериализовали Kotlin и C++. В результате канонический формат нарушался, и хеши расходились.
Решение:
- задать жёсткие правила сериализации для каждого поля;
- добавить фиксированные тестовые векторы, прогоняющиеся через оба движка автоматически;
- считать любые различия в результатах блокером для релиза.
2. Окно между `status` и `send-message`
Между запросом `POST /api/provable/status` и отправкой `POST /api/provable/send-message` может вклиниться другое сообщение от другого участника.
В итоге:
- `last_hash` и `last_sequence`, на которые ориентировался клиент, уже устарели;
- сервер отвергает запрос, потому что цепочка за это время продвинулась.
Это - нормальная ситуация, логичное следствие линейной хеш-цепочки. Но на стороне клиента пришлось заложить:
- retry‑логику при ошибке "устаревшего состояния";
- переобновление статуса и повторную подпись с новым `previous_hash` и `sequence`.
3. Потеря ключа при сбросе устройства
На Android приватный ключ хранится в `EncryptedSharedPreferences`, который завязан на Android Keystore. Если пользователь полностью сбрасывает устройство:
- Keystore стирается;
- приватный ключ теряется безвозвратно;
- старые сообщения остаются верифицируемыми (в них уже есть подписи и публичный ключ);
- но пользователь больше не может подписывать новые сообщения для этого режима.
Это честное ограничение безопасности: приватный ключ по дизайну не восстанавливается. Единственный выход - завести новый ключ и, при необходимости, начать новую доказуемую цепочку. Я явно доношу это до пользователя в интерфейсе при включении режима.
---
Дополнительные аспекты и практические нюансы
UX и объяснение пользователю
Криптография сама по себе пугает многих пользователей. Пришлось подумать о формулировках:
- режим описывается простыми словами: "переписка, которую нельзя незаметно подредактировать";
- при включении объясняется риск потери ключа и что произойдёт, если устройство будет сброшено;
- экспорт осуществляется в один читаемый JSON‑файл, который пользователь может хранить где угодно.
Цель - чтобы люди понимали не внутренние детали Ed25519, а реальные последствия: можно ли доверять истории и при каких условиях.
Ограничения модели доверия
Важно честно проговорить пределы системы:
- она не мешает участникам скриншотить или пересказывать сообщения;
- она не защищает от ситуации, когда один из собеседников добровольно публикует экспорт и выдает его за переписку с другим - но тут на помощь приходят подписи и идентификаторы пользователей;
- она не скрывает факт переписки, а фиксирует её содержание и порядок.
Моя задача была не сделать анонимный чат, а обеспечить неизменяемость истории, если стороны хотят иметь такое свойство.
Отношение к правовым аспектам
Доказуемая переписка по сути превращает чат в некий аналог цифрового документа:
- виден автор каждого сообщения;
- нельзя "задним числом" изменить фразу или дату;
- верификация не зависит от сервера и приложения.
С точки зрения потенциальных юридических сценариев это может оказаться полезным, но я изначально строил систему как технический инструмент, а не как юридическую платформу. Тем не менее архитектура уже сейчас позволяет использовать proof‑документы как дополнительный источник доказательств.
Сравнение с классическими мессенджерами
Большинство популярных мессенджеров реализуют:
- шифрование трафика;
- E2E‑шифрование в отдельных режимах;
- локальные бэкапы и экспорт истории.
Но почти нигде нет:
- формальной хеш-цепочки сообщений с проверяемыми последовательностями;
- криптографически доказуемой невозможности незаметно удалить или изменить сообщение;
- независимой от сервера верификации истории.
Xipher Provable Chat восполняет именно этот пробел, не пытаясь конкурировать с гигантами по всем остальным параметрам.
Что можно улучшить в будущем
Есть несколько направлений, куда можно развивать систему:
- поддержка ротации ключей с более гибкой политикой и явным описанием в цепочке;
- дополнительные типы записей (например, "системное событие" или "изменение настроек чата"), которые тоже включаются в хеш‑цепочку;
- инструменты для более удобной офлайн‑проверки на настольных системах;
- визуализация "целостности" и "доказуемости" прямо в UI, чтобы пользователю было видно, что цепочка не нарушена.
---
Итоги
Я взял несколько известных криптографических идей - Ed25519, хеш-цепочки и монотонные sequence - и применил их к чату, чтобы получить режим, в котором:
- ни пользователь, ни сервер не могут тихо переписать историю;
- любой человек, получив JSON‑файл, может убедиться в целостности переписки;
- утрата приватного ключа не ломает уже существующие доказательства, а лишь блокирует выпуск новых.
Реализация потребовала аккуратной работы с детерминизмом сериализации, строгой синхронизации логики между C++ и Kotlin, продуманного API и честного описания ограничений для пользователей. Зато результатом стал мессенджер, в котором переписка в доказуемом режиме превращается не просто в набор сообщений, а в криптографически защищённую историю, которой действительно можно доверять.



