Интеграция клиента с AeroNyx Chat Relay
Справочник по интеграции с AeroNyx Chat Relay: аутентификация WebSocket, запечатанные сообщения 1:1 и групповые сообщения, офлайн-доставка, уведомления о доставке и прочтении, реакции, статус присутствия, зашифрованные вложения, push-уведомления и ограничение частоты запросов.
Подготовить задание для ИИ
Выберите стек и задачу, затем скопируйте задание в помощник по программированию. Оно формируется локально, без отправки в сервис ИИ.
Готовое задание
Это справочник по интеграции с AeroNyx Chat Relay — сервисом WebSocket и HTTPS по адресу api.aeronyx.network, который передаёт между идентичностями AeroNyx сквозно зашифрованные личные сообщения 1:1, групповые сообщения, уведомления, реакции, статус присутствия и зашифрованные вложения.
Документ предназначен для инженеров, разрабатывающих клиенты, ботов и сервисы, совместимые с AeroNyx, а также для ИИ-агентов, реализующих их код. Все кадры, поля и ограничения, описанные на этой странице, соответствуют рабочему ретранслятору и AeroNyx App по состоянию на октябрь 2026 года.
Chat Relay — это централизованный путь доставки. Он сосуществует с децентрализованным путём через узлы (луковичная маршрутизация и анонимные почтовые ящики, см. Проверенная доставка через два узла); клиент может использовать оба пути. Интеграция по HTTPS в формате «запрос — ответ» без WebSocket описана в разделе Central Chat HTTPS API v1.
Модель доверия
Ретранслятор не видит содержимое. Клиенты шифруют и подписывают все данные до того, как они попадают на ретранслятор, а ретранслятор лишь маршрутизирует непрозрачный шифртекст, ставит его в очередь и применяет к нему ограничение частоты запросов.
Ретранслятор никогда не получает:
- текст сообщений, эмодзи реакций, правки или групповые полезные нагрузки в открытом виде
- ключи чатов, групповые ключи, ключи вложений или nonce
- содержимое вложений, имена файлов, миниатюры, звуковые волны или расшифровки
- закрытые ключи идентичности
При этом ретранслятор видит метаданные доставки, и интеграторам следует считать их доступными оператору:
- открытые ключи отправителя и получателя, идентификаторы групп и идентификаторы сообщений
- временные метки, размеры полезной нагрузки, а также состояние доставки, уведомлений и прочтения
- статус присутствия, состояние активности приложения и индикаторы набора текста (передаются открытыми кадрами)
- метаданные сигнализации звонков (имя комнаты, идентификатор звонка, признак видео)
- размер шифртекста вложений, заявленный тип медиа и срок действия
- флаг
contact_requestи метаданные соединения на уровне IP
Конфиденциальность содержимого обеспечивается сквозным шифрованием, а не контролем доступа на ретрансляторе. Проектируйте интеграцию с учётом этого: злоумышленник, получивший сохранённый шифртекст, всё равно не должен иметь возможности его прочитать.
Идентичности и ключи
Идентичность чата AeroNyx — это пара ключей Ed25519. Открытый ключ длиной 32 байта, записанный в виде 64 шестнадцатеричных символов в нижнем регистре, служит адресом. Генерируйте ключи на устройстве и никогда никуда не отправляйте закрытый ключ.
Из пары ключей идентичности выводятся два ключа:
- Ключ чата (1:1).
HKDF-SHA256(salt = empty, ikm = X25519(my_secret, peer_public), info = "AERONYX-P2P-KEY", length = 32), где оба ключа Ed25519 преобразуются в X25519 (SHA-512(seed)[0..32]с клампингом для закрытого ключа, преобразование из формы Эдвардса в форму Монтгомери для открытого ключа). Оба участника получают один и тот же ключ. - Подписи сообщений. Ed25519 с ключом идентичности над точными байтовыми строками, определёнными на этой странице.
Всегда используйте шестнадцатеричную запись в нижнем регистре для открытых ключей в кадрах. Ретранслятор не нормализует регистр во всех ключах очередей.
Аутентификация
Подпись RelayAuth
Вход по WebSocket и каждая аутентифицируемая конечная точка HTTPS используют одну и ту же подпись:
digest = SHA256("AeroNyx-RelayAuth-v1" || pubkey[32] || timestamp as u64 little-endian)
signature = Ed25519(identity_key, digest)
"AeroNyx-RelayAuth-v1" — это 20 байт ASCII без завершающего символа. timestamp задаётся в секундах Unix и должен отличаться от времени сервера не более чем на 300 секунд.
Подпись связывает только идентичность и время. Она не связывает метод, путь, тело запроса или соединение, и nonce не используется. Генерируйте новую временную метку для каждого запроса, передавайте подпись только по TLS и никогда не записывайте её в журналы.
Заголовок HTTPS
Authorization: Relay <pubkey-hex>:<timestamp>:<signature-hex>
При неудачной проверке возвращается HTTP 401 с {"success": false, "error": "<reason>"}. Возможные причины, в частности: missing_auth_header, malformed_auth_header, invalid_timestamp, timestamp_expired, invalid_pubkey и invalid_signature.
Соединение WebSocket
Конечная точка
wss://api.aeronyx.network/ws/relay/
Нативные клиенты подключаются без заголовка Origin. Браузеры должны подключаться с разрешённого источника; при любом другом источнике или неизвестном Host соединение закрывается с кодом 1008.
Вход
Сервер принимает сокет, а затем ожидает кадр auth в течение 30 секунд:
{
"type": "auth",
"pubkey": "<64 hex>",
"timestamp": 1780000000,
"signature": "<128 hex>"
}
При успехе:
{ "type": "auth_ack", "session_id": "<uuid>", "server_ts": 1780000001 }
Используйте server_ts для оценки расхождения часов; временные метки сообщений проверяются в том же окне ±300 секунд.
После auth_ack сервер запускает механизм пульса, подписывает соединение на свои каналы и сразу же воспроизводит офлайн-очередь (см. Офлайн-доставка).
При неудаче сервер отправляет {"type": "auth_error", "reason": "<reason>"} и закрывает сокет с кодом 4001. Причины: missing_fields, timestamp_expired, invalid_pubkey, invalid_signature_length, invalid_signature_encoding, invalid_signature, internal_error. При истечении времени ожидания входа отправляется причина timeout, и соединение закрывается с кодом 4002.
На любой другой кадр, полученный до входа, сервер отвечает auth_error с причиной authentication_required; сокет при этом остаётся открытым.
Пульс и состояние активности
| Направление | Кадр | Поведение |
|---|---|---|
| сервер → клиент | {"type":"ping"} каждые 30 с | Ответить {"type":"pong"}. |
| клиент → сервер | {"type":"ping"} | Сервер отвечает {"type":"pong"}. |
| клиент → сервер | {"type":"presence_state","foreground":true} | Помечает это соединение как активное. |
| клиент → сервер | {"type":"presence_state","foreground":false} | Помечает его как фоновое, что позволяет ретранслятору отправлять push-уведомления о новых сообщениях. |
Ретранслятор считает идентичность находящейся в сети только до тех пор, пока её соединение отправляет ping, pong или presence_state с foreground: true не реже одного раза в 90 секунд. AeroNyx App, находясь на переднем плане, отправляет ping каждые 15 секунд и считает соединение разорванным, если пропущено более трёх pong.
Долгоживущие соединения следует переустанавливать не реже одного раза в 24 часа. Сообщения не теряются, если соединение незаметно перестаёт получать кадры в реальном времени, поскольку каждое сообщение также ставится в очередь и воспроизводится при входе; однако доставка в реальном времени возобновляется только после переподключения.
Правила для кадров
- Только текстовые кадры, по одному объекту JSON на кадр. Двоичные кадры игнорируются.
- Максимальный размер кадра — 1 048 576 символов. Кадры большего размера отклоняются с
{"type":"error","reason":"message_too_large"}. Держитеpayload_b64в пределах примерно 800 КиБ, чтобы оставить место для остальной части кадра. - Для некорректных кадров возвращается
errorс причинойinvalid_json,invalid_json_typeилиunknown_type.
Общий кадр ошибки:
{ "type": "error", "reason": "<reason>", "retry_after": 0 }
Ошибки ограничения частоты запросов также содержат scope.
Коды закрытия
| Код | Значение |
|---|---|
1008 | Host или Origin не разрешены. |
4000 | Серверу не удалось передать сигнал пульса. |
4001 | Ошибка входа. |
4002 | Истекло время ожидания входа. |
Переподключайтесь с экспоненциальной задержкой и случайным разбросом. AeroNyx App ожидает 2^(attempt-1) секунд с ограничением диапазоном 1–60 секунд, умножая результат на случайный коэффициент от 0,8 до 1,2.
Отправка сообщения 1:1
1. Сборка запечатанного конверта
Полезная нагрузка сообщения 1:1 — это подписанный зашифрованный ChatEnvelope. Его точная структура, эталонная реализация на Python и эталонный тестовый вектор приведены в разделе Central Chat HTTPS API v1: формат запечатанного конверта. Оба API используют один и тот же конверт.
Вкратце: XChaCha20-Poly1305 с ключом чата, подпись Ed25519 над 121-байтовой стенограммой и фиксированная двоичная структура. content_type равен 0 для всех сообщений, включая сообщения с вложениями.
Открытый текст — это текст в UTF-8 для обычного сообщения или объект JSON для сообщений с вложениями, ответами, пересылками или предпросмотром ссылок:
{
"type": "aeronyx_message",
"text": "See the attached file",
"attachments": [ { "blob_id": "...", "file_key": "...", "media_type": "image/jpeg", "file_name": "photo.jpg", "file_size": 482113 } ],
"reply": { "msg_id": "...", "sender_pubkey": "...", "text": "..." },
"forwarded": true,
"forwarded_from_name": "...",
"forwarded_from_pubkey": "...",
"link_preview": { "url": "https://...", "title": "...", "description": "...", "site_name": "..." }
}
Все поля, кроме type и text, необязательны. Получатели должны отображать как обычный текст любой открытый текст, который не является объектом JSON с "type": "aeronyx_message". Объект вложения определён в разделе Зашифрованные вложения.
2. Подпись кадра
Каждый кадр сообщения содержит вторую подпись, payload_sig, которую ретранслятор проверяет перед приёмом:
payload_sig = Ed25519(identity_key,
SHA256(receiver_pubkey[32] || sender_pubkey[32] || discriminant as 1 byte
|| timestamp as u64 little-endian || envelope_bytes))
envelope_bytes — это декодированный payload_b64. Используйте дискриминант 11 для сообщений и правок и 12 для реакций. payload_sig кодируется в шестнадцатеричном виде.
3. Отправка
{
"type": "relay_send",
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"receiver_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>"
}
| Поле | Правила |
|---|---|
msg_id | 32 шестнадцатеричных символа в нижнем регистре: 16-байтовый message_id конверта. Приложение получателя сохраняет сообщение под этим идентификатором, поэтому он должен совпадать с конвертом. |
receiver_pubkey | Открытый ключ получателя. |
discriminant | 11. |
timestamp | Секунды Unix в пределах ±300 с от времени сервера. |
suppress_push | Необязательное. true отключает отправку push-уведомления. |
contact_request | Необязательное. Помечает первое сообщение пользователю, который требует проверки контактов (см. Проверка контактов). |
4. Подтверждение
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
durable(иqueued) принимает значениеtrue, как только сообщение сохранено в офлайн-очереди получателя. Считайтеdurable: trueпризнаком того, что сообщение «отправлено».deliveredозначает, что у получателя было активное соединение на переднем плане. Это не доказательство получения; для этого используйте уведомления о доставке.
Ожидайте подтверждения до 15 секунд, прежде чем считать попытку неудачной.
Отклонения и ошибки
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
| Ответ | Причина | Действие |
|---|---|---|
send_rejected | verification_required | Получатель принимает сообщения только от контактов. Не повторять. |
send_rejected | contact_request_rate_limited | Достигнут лимит запросов на добавление в контакты для этого получателя. Не повторять. |
error | missing_fields | Обязательное поле отсутствует или пусто. |
error | invalid_timestamp, timestamp_expired | Исправить часы и заново проставить временную метку. |
error | invalid_payload_sig | payload_sig не проходит проверку. |
error | rate_limited | Повторить через retry_after секунд. |
Повторные попытки
Повторяйте отправку неподтверждённого сообщения с тем же msg_id и теми же байтами конверта. Поскольку ретранслятор отклоняет кадры старше 300 секунд, для каждой повторной попытки вычисляйте новые timestamp кадра и payload_sig; конверт сохраняет исходную временную метку. Ретранслятор и получатели устраняют дубликаты по msg_id.
Резервный канал HTTPS
Если WebSocket недоступен, то же сообщение можно отправить по HTTPS:
POST /api/relay/push/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{
"receiver_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>"
}
При успехе возвращается HTTP 200 с {"success": true}. Ошибки проверки контактов возвращают 400 с verification_required или contact_request_rate_limited, а при ограничении частоты запросов возвращается 429 с Retry-After. Сообщения, отправленные таким способом, ставятся в очередь получателя, но не вызывают push-уведомления, поэтому после восстановления соединения WebSocket отправьте их повторно через него.
Получение сообщений
Входящие сообщения поступают в виде relay_envelope:
{
"type": "relay_envelope",
"sender_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"from_offline": false
}
Для discriminant: 11:
- Устраните дубликаты по
msg_id. Одно и то же сообщение может прийти в реальном времени и повторно из офлайн-очереди. - Разберите конверт и убедитесь, что его
receiver_pubkeyсовпадает с вашей идентичностью. - Для кадров в реальном времени (
from_offline: false) отбросьте сообщение, если временная метка конверта отличается от ваших часов более чем на 300 секунд. - Проверьте подпись конверта по
sender_pubkeyконверта. Аутентифицированным отправителем является именно этот ключ, а неsender_pubkeyкадра. - Расшифруйте сообщение ключом чата, выведенным для этого отправителя. Очень старые версии приложения шифровали непосредственно выходом X25519; попробуйте его, если ключ HKDF не подошёл.
- Надёжно сохраните сообщение, затем отправьте уведомление о доставке, а для
from_offline: true— также офлайн-подтверждение.
Конверты 1:1 доставляются без payload_sig; проверкой подлинности служит подпись конверта. Кадры также могут содержать contact_request: true.
Если сообщение адресовано не вам, не проходит проверку или не расшифровывается, отбросьте его, ничего не показывая пользователю.
Офлайн-доставка
Каждое сообщение, правка, отзыв, реакция, уведомление о доставке и уведомление о прочтении записывается в офлайн-очередь получателя до доставки в реальном времени. Очередь автоматически воспроизводится после каждого входа, а также по запросу:
{ "type": "relay_pull" }
Ретранслятор воспроизводит все элементы очереди в виде их обычных типов кадров с from_offline: true, упорядочивая их по временной метке, после чего отправляет:
{ "type": "relay_pull_done", "count": 12, "has_more": false }
Доставка выполняется по принципу «как минимум один раз». Элементы остаются в очереди до подтверждения:
{ "type": "relay_offline_ack", "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834" }
Реакции подтверждайте с помощью "reaction_id" вместо "msg_id". Подтверждайте элемент только после его надёжного сохранения на устройстве. Ретранслятор отвечает {"type":"relay_offline_ack","msg_id":"...","success":true}.
Ограничения очереди:
| Ограничение | Значение |
|---|---|
| Элементов на получателя | 1 000. При заполнении очереди новые элементы отклоняются, а отправитель получает durable: false. |
| Срок хранения | 72 часа после добавления самого последнего элемента. |
| Размер полезной нагрузки | 1 МиБ после декодирования, в пределах лимита кадра в 1 МиБ. |
Ретранслятор — это буфер доставки, а не история сообщений. Храните историю на устройстве.
Уведомления о доставке и прочтении
Уведомление о доставке
Отправляется после сохранения сообщения:
{ "type": "message_receipt", "receiver_pubkey": "<original sender>", "msg_id": "...", "timestamp": 1780000010 }
Ретранслятор отвечает message_receipt_ack и доставляет исходному отправителю {"type":"message_receipt","sender_pubkey":"...","msg_id":"...","timestamp":...}.
Уведомление о прочтении
{ "type": "message_read", "receiver_pubkey": "<original sender>", "msg_id": "<latest read message>", "timestamp": 1780000200, "enabled": true }
Уведомления о прочтении работают как отметки уровня: приложение помечает прочитанными указанное сообщение и все более ранние исходящие сообщения в беседе. Отправляйте уведомление только в том случае, если у пользователя включены уведомления о прочтении.
Ретранслятор применяет правило взаимности при отправке, при доставке в реальном времени и при воспроизведении очереди. Уведомление о прочтении доставляется только в том случае, если оба пользователя являются взаимными контактами и у обоих включено read_receipts_enabled. В противном случае отправитель получает:
{ "type": "message_read_ack", "msg_id": "...", "delivered": false, "suppressed": true, "reason": "receiver_read_receipts_disabled" }
| Причина | Значение |
|---|---|
client_disabled | Кадр содержал enabled: false или read_receipts_enabled: false. |
not_mutual_contact | Пользователи не являются взаимными контактами. |
reader_read_receipts_disabled | У прочитавшего пользователя отключены уведомления о прочтении. |
receiver_read_receipts_disabled | У исходного отправителя отключены уведомления о прочтении. |
invalid_pubkey | Открытый ключ имеет некорректный формат. |
Доставленное уведомление о прочтении подтверждается кадром {"type":"message_read_ack","msg_id":"...","delivered":true}.
Правки и отзывы
Правка
Правка — это новый запечатанный конверт (со своим случайным message_id), содержащий полное заменяющее содержимое и отправляемый с привязкой к идентификатору исходного сообщения:
{
"type": "message_edit",
"receiver_pubkey": "<64 hex>",
"msg_id": "<original msg_id>",
"target_msg_id": "<original msg_id>",
"payload_b64": "<base64 envelope>",
"payload_sig": "<128 hex>",
"timestamp": 1780000400
}
payload_sig вычисляется по формуле из раздела Подпись кадра с дискриминантом 11. Правка, удаляющая все вложения, устанавливает "attachments_edited": true в своём открытом JSON. Ретранслятор отвечает message_edit_ack. Получатели проверяют и расшифровывают конверт так же, как сообщение, и должны применять правку только в том случае, если её отправитель является автором исходного сообщения.
Отзыв
{
"type": "message_revoke",
"receiver_pubkey": "<64 hex>",
"sender_pubkey": "<64 hex>",
"msg_id": "<original msg_id>",
"timestamp": 1780000500,
"payload_sig": "<128 hex>"
}
Подпись отзыва вычисляется над необработанными байтами, без хеширования:
payload_sig = Ed25519(identity_key,
"aeronyx-message-revoke-v1" || sender_pubkey[32] || receiver_pubkey[32]
|| UTF-8(msg_id) || timestamp as u64 little-endian)
Ретранслятор отвечает message_revoke_ack. Ретранслятор не проверяет авторство: получатели должны проверить подпись и применять отзыв только в том случае, если его отправитель является автором исходного сообщения. Чтобы удалить вложения отозванного сообщения, вызовите POST /api/relay/blob/{blob_id}/delete/.
Реакции
Реакция 1:1 — это запечатанный конверт, у которого message_id является идентификатором реакции, с content_type 2 и открытым текстом {"emoji": "❤️", "op": "add"} (или "remove"):
{
"type": "message_reaction",
"receiver_pubkey": "<64 hex>",
"msg_id": "<message being reacted to>",
"reaction_id": "<32 hex>",
"payload_b64": "<base64 envelope>",
"payload_sig": "<128 hex>",
"timestamp": 1780000300
}
payload_sigиспользует дискриминант12.reaction_idслужит ключом идемпотентности. Повторныйreaction_idв течение 72 часов подтверждается с"duplicate": trueи повторно не доставляется.- Ретранслятор отвечает
{"type":"message_reaction_ack","msg_id":"...","reaction_id":"...","delivered":...,"queued":...}. - Количество реакций ограничено 20 на отправителя и беседу в пределах окна ограничения.
Групповые реакции описаны в разделе Группы.
Присутствие и набор текста
Кадры присутствия и набора текста представляют собой открытые метаданные и видны ретранслятору.
Присутствие
Подписка на контакты:
{ "type": "presence_subscribe", "pubkeys": ["<64 hex>", "<64 hex>"] }
В одном кадре учитывается до 200 ключей; некорректные и повторяющиеся ключи игнорируются. Подписки накапливаются в течение всего времени жизни соединения. Подписывайтесь только на собственные контакты.
{
"type": "presence_subscribe_ack",
"count": 2,
"updates": [
{ "pubkey": "<a>", "visible": true, "presence_visible": true, "last_seen_visible": true, "online": false, "last_seen_ts": 1780000000, "reason": "allowed" },
{ "pubkey": "<b>", "visible": false, "presence_visible": false, "last_seen_visible": false, "reason": "not_mutual_contact" }
],
"server_ts": 1780000001
}
Статус присутствия виден только между взаимными контактами и только если у целевого пользователя включено presence_enabled. Скрытые записи (reason not_mutual_contact или presence_hidden) не содержат online и last_seen_ts. last_seen_ts присутствует только тогда, когда last_seen_visible равен true; в противном случае показывайте обобщённое состояние, например «был(а) недавно».
Изменения в реальном времени поступают в виде:
{ "type": "presence_update", "pubkey": "<a>", "online": true, "presence_visible": true, "last_seen_visible": true, "last_seen_ts": 1780000100 }
Набор текста
{ "type": "typing", "receiver_pubkey": "<64 hex>", "is_typing": true }
{ "type": "group_typing", "group_id": "<uuid>", "is_typing": true }
Индикатор набора текста пересылается только в том случае, если статус присутствия отправителя был бы виден получателю (взаимные контакты и presence_enabled), либо другим участникам группы, в которой состоит отправитель. Он никогда не сохраняется, не ставится в очередь и не передаётся через push-уведомления.
Профиль и настройки конфиденциальности
GET /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
{
"success": true,
"profile": {
"pubkey_hex": "<64 hex>",
"display_name": "AeroNyx User",
"bio": "",
"handle": "",
"avatar_url": "",
"status_text": "",
"privacy": { "presence_enabled": true, "last_seen_enabled": true, "read_receipts_enabled": true },
"updated_at": "2026-10-09T10:00:00Z"
}
}
Идентичность без профиля получает пустые поля и все три флага конфиденциальности со значением true.
PATCH /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{ "display_name": "Alice", "privacy": { "presence_enabled": true, "last_seen_enabled": false, "read_receipts_enabled": false } }
| Поле | Правила |
|---|---|
display_name | До 50 символов. |
bio | До 200 символов. |
avatar_url | URL https:// или пустое значение. |
handle | a-z и 0-9, от 5 до 24 символов. На имена пользователей распространяются правила членства и периоды ожидания между изменениями. |
privacy.* | Логические значения. Три флага также можно передавать на верхнем уровне. |
Возможные ошибки, в частности: no_valid_fields, <flag>_invalid_boolean, handle_taken и handle_change_cooldown:<date>.
Поддерживайте поведение клиента в соответствии с этими настройками: не отправляйте уведомления о прочтении, когда они отключены, и не отображайте состояние прочтения собеседника, пока ваши собственные уведомления о прочтении отключены.
Проверка контактов
Пользователь может потребовать, чтобы незнакомцы проходили проверку, прежде чем писать ему. Если получатель включил эту функцию и не добавил отправителя в контакты, relay_send отклоняется с verification_required.
Чтобы начать беседу, отправьте одно сообщение с "contact_request": true. Запросы на добавление в контакты обходят эту проверку и ограничены 3 на пару «отправитель — получатель» в скользящем 24-часовом окне; последующие запросы отклоняются с contact_request_rate_limited. Флаг contact_request доставляется получателю, чтобы приложение могло представить сообщение как запрос.
Проверка контактов применяется к relay_send и к резервному каналу HTTPS.
Группы
Групповые сообщения
Групповое содержимое шифруется общим 32-байтовым групповым ключом с использованием AES-256-GCM:
payload_b64 = base64(nonce[12] || AES-256-GCM(group_key, plaintext_json) || tag[16])
Открытый JSON содержит text, type (text, media, system или reaction), sender_pubkey, created_at, а также необязательные attachments, mentions, reply, forwarded, forwarded_from_name, forwarded_from_pubkey и link_preview.
{
"type": "group_send",
"msg_id": "<32 hex>",
"group_id": "<uuid>",
"payload_b64": "<base64>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>",
"key_version": 3
}
payload_sig = Ed25519(identity_key,
SHA256(UTF-8(group_id) || sender_pubkey[32] || 0x19 || timestamp as u64 little-endian
|| decoded payload))
Ретранслятор проверяет подпись и то, что отправитель является активным участником, сохраняет сообщение для каждого из остальных активных участников, а затем доставляет его в реальном времени. Групповые полезные нагрузки не используют конверт 1:1, а групповые конверты доставляются вместе с group_id, key_version и payload_sig, чтобы получатели могли проверить отправителя до расшифровки.
{
"type": "group_delivered",
"msg_id": "...",
"group_id": "...",
"accepted": true,
"accepted_count": 5,
"delivered_count": 2,
"queued_count": 5,
"failed_count": 0,
"member_count": 6
}
Отправитель, не являющийся участником группы, получает error с причиной not_a_member.
Групповые правки, отзывы и реакции
| Кадр | Подпись |
|---|---|
group_message_edit (group_id, msg_id, target_msg_id, payload_b64, payload_sig, key_version, timestamp) | Групповая формула выше. |
group_message_reaction (group_id, msg_id, reaction_id, payload_b64, payload_sig, key_version, timestamp) | Групповая формула выше. Полезная нагрузка — групповая полезная нагрузка с type: "reaction". |
group_message_revoke (group_id, sender_pubkey, msg_id, timestamp, payload_sig) | Ed25519 над необработанными байтами `"aeronyx-group-message-revoke-v1" |
Каждый из этих кадров подтверждается соответствующим кадром _ack со счётчиками доставки. Получатели применяют правки и отзывы только от исходного автора.
Групповые ключи
Групповые ключи распространяются владельцем группы в виде пакетов ключей для каждого участника: base64(nonce[12] || AES-256-GCM(chat_key(owner, member), group_key) || tag[16]). Конечные точки REST в /api/relay/groups/ позволяют создавать группы, управлять участниками и приглашениями, загружать пакеты ключей, выполнять ротацию ключей (keys/rotate/) и получать текущий пакет вызывающей стороны (keys/me/). Отправители шифруют последней имеющейся у них версией ключа; получатели, у которых отсутствует нужная версия ключа, должны запросить keys/me/ и удерживать сообщение до получения ключа.
Зашифрованные вложения
Вложения шифруются на устройстве, загружаются в виде непрозрачного шифртекста, а ссылки на них передаются внутри зашифрованного сообщения.
Шифрование файла
Для каждого файла сгенерируйте случайный 32-байтовый ключ и случайный 12-байтовый nonce:
blob = nonce[12] || AES-256-GCM(file_key, file_bytes) || tag[16]
Загрузите blob. Помещайте file_key и все описательные поля внутрь зашифрованного сообщения и никогда — в запрос на загрузку.
Объект вложения
| Ключ | Обязательный | Значение |
|---|---|---|
blob_id | да | Идентификатор, возвращённый при загрузке. |
file_key | да | 32-байтовый ключ файла в кодировке Base64. |
media_type | да | MIME-тип открытого файла. |
file_name | да | Отображаемое имя. |
file_size | да | Размер открытого файла в байтах. |
thumb_b64 | нет | Миниатюра JPEG в кодировке Base64, до 64 КиБ. |
duration_ms | нет | Длительность аудио или видео. |
waveform | нет | До 96 чисел в диапазоне [0, 1] для голосовых сообщений. |
sticker, sticker_pack, sticker_pose | нет | Идентификация стикера. |
live | нет | Анимированная часть Live Photo: {blob_id, file_key, media_type, file_size, duration_ms?, width?, height?}. |
Получатели игнорируют вложения без blob_id или file_key. Голосовые сообщения из приложения кодируются в AAC-LC в контейнере MP4 (audio/mp4).
Загрузка: одиночный запрос (до 10 МиБ)
POST /api/relay/blob/presign/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{ "file_size": 482141, "media_type": "image/jpeg", "media_kind": "image", "ttl_days": 7 }
file_size — это размер шифртекста. media_kind принимает одно из значений voice, image, video, file, avatar, other. ttl_days ограничивается диапазоном 1–30 (по умолчанию 7).
{
"blob_id": "<uuid>",
"upload_url": "https://...",
"upload_method": "PUT",
"storage": "r2",
"expires_at": "2026-10-16T10:00:00Z",
"media_type": "image/jpeg",
"media_kind": "image",
"access_mode": "capability",
"ttl_days": 7,
"max_bytes": 10485760
}
Затем:
- Отправьте шифртекст методом
PUTнаupload_urlв течение 15 минут, указав только заголовокContent-Type. Не передавайте заголовокAuthorizationв хранилище. - Вызовите
POST /api/relay/blob/{blob_id}/complete/с RelayAuth. Ретранслятор подтверждает объект и возвращает{blob_id, file_size, expires_at, storage}.
Загрузка: multipart (до 100 МиБ)
POST /api/relay/blob/multipart/create/
{ "total_size": 52428800, "media_type": "video/mp4", "media_kind": "video", "ttl_days": 7 }
{
"blob_id": "<uuid>",
"upload_id": "...",
"part_size": 8388608,
"total_parts": 7,
"part_urls": ["https://...", "..."],
"storage": "r2",
"expires_at": "...",
"max_bytes": 104857600
}
part_size по умолчанию равен 8 МиБ; можно запросить значение от 5 до 16 МиБ. URL частей действительны в течение 60 минут. Отправьте каждую часть методом PUT на её URL и сохраните заголовок ответа ETag, затем:
POST /api/relay/blob/multipart/complete/
{ "blob_id": "<uuid>", "upload_id": "...", "parts": [ { "part_number": 1, "etag": "\"...\"" } ] }
AeroNyx App использует загрузку одиночным запросом для шифртекста размером до 8 МиБ и multipart-загрузку для файлов большего размера.
Скачивание
GET /api/relay/blob/{blob_id}/
Ретранслятор отвечает 302 с заголовком Location, указывающим на сеть доставки контента, а также с заголовками X-AeroNyx-Blob-Size, X-AeroNyx-Blob-Media-Type и X-AeroNyx-Blob-Storage. Выполните переход по перенаправлению самостоятельно, не передавая заголовок Authorization, затем проверьте и расшифруйте blob с помощью его file_key. Ограничьте объём скачивания ожидаемым размером.
blob_id — это мандат на предъявителя: любой, кто им владеет, может получить шифртекст, поэтому ключ передаётся только внутри зашифрованного сообщения. access_mode и срок действия применяются при перенаправлении на ретрансляторе. Не полагайтесь на них для обеспечения конфиденциальности.
Удаление вложений
POST /api/relay/blob/{blob_id}/delete/
Удалить blob может только тот, кто его загрузил. Ответ: {"blob_id": "...", "deleted": true}.
Ошибки вложений
Ошибки возвращаются в формате {"success": false, "error": "<text>", "error_code": "<code>"}. Ориентируйтесь на error_code.
| HTTP | error_code | Значение |
|---|---|---|
| 400 | blob_id_invalid | Некорректный идентификатор blob. |
| 400 | blob_missing_file_size, blob_missing_total_size, blob_missing_parts, blob_bad_parts, blob_bad_part_count | Недопустимый запрос на загрузку. |
| 400 | blob_not_r2, blob_multipart_complete_failed | Не удалось завершить загрузку; начните новую. |
| 401 | auth_required | Для blob требуется RelayAuth. |
| 403 | blob_not_uploader, download_forbidden | Вызывающей стороне доступ запрещён. |
| 404 | blob_not_found, blob_not_uploaded | Неизвестный blob или попытка завершения до окончания загрузки. |
| 410 | blob_expired | Срок действия blob истёк. Попросите отправителя отправить файл повторно. |
| 413 | blob_too_large | Превышен лимит; ответ содержит max_bytes и chunked_max_bytes. |
| 503 | blob_r2_unavailable | Хранилище временно недоступно; повторите попытку с задержкой. |
Устаревшие конечные точки загрузки
POST /api/relay/blob/ (multipart-форма, до 10 МиБ) и API возобновляемых сессий в /api/relay/blob/session/ (до 100 МиБ, фрагменты от 64 КиБ до 4 МиБ, сессии действительны 24 часа) остаются доступными в качестве резервного варианта. Новые клиенты должны использовать описанные выше конечные точки.
Push-уведомления
Ретранслятор отправляет уведомления через Apple Push Notification service (APNs) для iOS и macOS. Клиенты Android получают сообщения только через WebSocket.
Push-уведомление отправляется для relay_send и group_send, если у получателя нет активного соединения на переднем плане, отправитель не установил suppress_push, а получатель не отключил уведомления для беседы. Правки, отзывы, реакции и уведомления о доставке и прочтении push-уведомлений не вызывают. Полезные нагрузки push-уведомлений не содержат шифртекста:
{
"aps": { "alert": { "title": "AeroNyx", "body": "New encrypted message" }, "sound": "default", "badge": 1, "content-available": 1 },
"kind": "p2p_message",
"sender_pubkey": "<64 hex>",
"message_id": "..."
}
kind принимает значение p2p_message (вместе с sender_pubkey) или group_message (вместе с group_id). Уведомления о звонках используют missed_call и специальные полезные нагрузки для звонков. Получив push-уведомление, подключитесь и выполните relay_pull.
| Конечная точка | Тело запроса |
|---|---|
POST /api/relay/push/register/ | token (64 hex), platform (ios или macos), bundle_id, environment (production или sandbox), необязательные token_type (alert или voip) и provider (apns). |
POST /api/relay/push/unregister/ | token, необязательные platform, token_type, provider. |
POST /api/relay/push/mute/ | kind (p2p или group), target (открытый ключ или идентификатор группы), muted (логическое значение). |
Все три требуют RelayAuth. При регистрации токен закрепляется за вызывающей идентичностью.
Звонки
Сигнализация голосовых и видеозвонков передаётся через тот же WebSocket в виде открытых метаданных; медиапотоки передаются отдельно. Для звонков 1:1 используются кадры call_invite, call_answer, call_reject, call_hangup и call_busy, для групп — group_call_invite, group_call_invite_broadcast, group_call_answer и group_call_hangup_broadcast, а для встреч с организатором — кадры допуска участников.
Ретранслятор проверяет room_name по участникам: p2p_, за которым следуют первые 16 шестнадцатеричных символов SHA256(lower_key + ":" + higher_key), для звонков 1:1 и grp_<first 8 characters of group_id>_<first 8 hex of SHA256(group_id)> для групп. Сигналы звонков для получателя, находящегося не в сети, хранятся 120 секунд.
Ограничение частоты запросов
| Область | Лимит | Ответ |
|---|---|---|
relay_send, group_send и push по HTTPS, на идентичность | 50 за короткое окно; 100 000 в сутки | WebSocket: error rate_limited с retry_after. HTTPS: 429 с Retry-After. |
group_send на отправителя и группу | 10 за короткое окно | error rate_limited, scope: "sender". |
group_send на группу | 50 за короткое окно | error rate_limited, scope: "group". |
| Реакции на отправителя и беседу | 20 за короткое окно | error rate_limited, scope: "reaction". |
| Запросы на добавление в контакты на пару «отправитель — получатель» | 3 за 24 часа | send_rejected contact_request_rate_limited. |
Выжидайте не менее retry_after секунд. Не повторяйте отправку в плотном цикле: счётчики продолжают расти во время повторных попыток.
Контрольный список реализации
- Сгенерируйте и сохраните идентичность Ed25519; везде используйте ключи в шестнадцатеричной записи в нижнем регистре.
- Реализуйте RelayAuth, а также вход по WebSocket, пульс и
presence_state. - Реализуйте запечатанный конверт и проверьте его по эталонному вектору из Central Chat HTTPS API v1.
- Отправляйте через
relay_send, считайтеdurable: trueпризнаком отправки, при повторных попытках заново вычисляйтеtimestampиpayload_sig. - Принимайте
relay_envelope: устраняйте дубликаты, проверяйте, расшифровывайте, сохраняйте, затем отправляйтеmessage_receiptиrelay_offline_ack. - Выполняйте
relay_pullпосле входа и при пробуждении по push-уведомлению; подтверждайте только после надёжного сохранения. - Считывайте флаги конфиденциальности профиля и соблюдайте их для статуса присутствия и уведомлений о прочтении.
- Шифруйте вложения на устройстве и загружайте их через
presignили multipart; хранитеfile_keyвнутри зашифрованного сообщения. - Проверяйте авторство перед применением правок и отзывов.
- Храните поиск и историю сообщений на устройстве. Ретранслятор не поддерживает поиск по содержимому и не является архивом.
Часто задаваемые вопросы
Может ли ретранслятор чата AeroNyx читать мои сообщения?
Нет. Сообщения, правки, реакции, групповые сообщения и вложения шифруются и подписываются на устройстве отправителя до того, как попадают на ретранслятор, а ключи никогда не покидают устройства участников разговора. Ретранслятор хранит и пересылает только шифртекст.
Что видит ретранслятор чата AeroNyx?
Ретранслятор видит метаданные доставки: открытые ключи отправителя и получателя, идентификаторы групп и сообщений, метки времени, размеры полезной нагрузки, статус доставки и прочтения, сигналы присутствия и набора текста, метаданные сигнализации звонков, размеры вложений и IP-адреса подключений. Он не видит ни содержимого сообщений, ни содержимого вложений, ни ключей. Полный список приведён в модели доверия в начале этой страницы.
Какое шифрование использует чат AeroNyx?
Каждая идентичность — это пара ключей Ed25519. Два собеседника выводят общий ключ чата с помощью X25519 и HKDF-SHA256. Сообщения 1:1 шифруются с помощью XChaCha20-Poly1305 и подписываются с помощью Ed25519. Групповые сообщения и вложения шифруются с помощью AES-256-GCM. Точные байтовые форматы и тестовый вектор опубликованы в документации HTTPS API центрального чата v1.
Как защищены фото, видео и файлы?
Перед загрузкой каждый файл шифруется на устройстве собственным случайным ключом AES-256-GCM. Хранилище получает только шифртекст. Ключ файла передаётся внутри сквозно зашифрованного сообщения, поэтому расшифровать файл могут только получатели.
Что происходит, если получатель не в сети?
Ретранслятор хранит зашифрованные сообщения в офлайн-очереди получателя до 72 часов и доставляет их, когда получатель снова подключается. На iOS и macOS получатель также получает push-уведомление, не содержащее текста сообщения.
Хранит ли AeroNyx историю моих чатов?
Нет. Ретранслятор — это буфер доставки: элементы удаляются после того, как устройство получателя подтвердит их сохранение. История чатов и поиск находятся на ваших устройствах.
Могу ли я создать собственный клиент или бот для AeroNyx?
Да. Любое программное обеспечение, которое владеет идентичностью Ed25519 и реализует форматы, описанные на этой странице, может обмениваться сообщениями с пользователями AeroNyx App. Для более простой интеграции по схеме «запрос — ответ» без WebSocket используйте HTTPS API центрального чата v1.
Является ли ретранслятор чата AeroNyx децентрализованным?
Ретранслятор чата — это централизованный сервис доставки AeroNyx. Кроме того, AeroNyx управляет сетью узлов с открытым исходным кодом (AGPL-3.0), которая может передавать шифртекст чата по двухскачковому маршруту через разные сети. Оба пути сосуществуют: ретранслятор обеспечивает быструю и надёжную доставку и офлайн-очереди, а путь через узлы — это необязательный маршрут, который сокращает объём данных, доступных для наблюдения любому отдельному оператору.
Почему моё сообщение отклоняется с ошибкой verification_required?
Получатель принимает сообщения только от контактов. Отправьте одно первое сообщение с contact_request: true — получатель увидит его как запрос на добавление в контакты. Допускается не более трёх запросов на добавление в контакты на одного получателя в любом 24-часовом окне.
Проверенная доставка через два узла
Для допустимого аутентифицированного трафика ChatRelay источник может выбрать двухузловой путь с сетевым разнесением и засчитывать доставку только после проверки подписанного подтверждения от ожидаемого конечного узла. Узлы-ретрансляторы маршрутизируют шифртекст и не разбирают сквозно зашифрованную полезную нагрузку. Полная модель доказательств описана в разделе Обнаружение узлов и проверенная зашифрованная доставка через ретрансляторы.
<!-- verified-two-hop-delivery-v1:end -->