Криптографически доказуемые чаты в xipher: ed25519, хеш-цепочка и sequence

Криптографически доказуемые чаты в 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 и честного описания ограничений для пользователей. Зато результатом стал мессенджер, в котором переписка в доказуемом режиме превращается не просто в набор сообщений, а в криптографически защищённую историю, которой действительно можно доверять.

Прокрутить вверх