Руководство по интеграции с Chat API AeroNyx

12 мин чтения

Отправка и получение сквозным образом зашифрованных сообщений AeroNyx по HTTPS: подпись запросов, работающий быстрый старт, формат запечатанного конверта, коды ошибок и тестовые векторы.

Chat API AeroNyx позволяет серверу, боту или приложению отправлять и получать сквозным образом зашифрованные личные сообщения пользователям AeroNyx по обычному HTTPS. Он не использует Приватную сеть AeroNyx, VPN, луковую маршрутизацию или анонимный почтовый ящик.

Отправка происходит мгновенно: если App получателя подключено, сообщение доходит сразу. Получение через этот API работает опросом: GET /inbox/ возвращает то, что ожидает доставки, и никогда не держит запрос открытым. Чтобы получать сообщения в реальном времени, подключите ту же идентичность к WebSocket Chat Relay (wss://api.aeronyx.network/ws/relay/). См. раздел «Доставка в реальном времени» ниже.

Базовый URL: https://api.aeronyx.network/api/relay/developer/v1

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

  1. Создайте идентификатор. Ваш чат-идентификатор — пара ключей Ed25519. 32-байтовый открытый ключ, записанный как 64 строчных шестнадцатеричных символа, является вашим чат-адресом. Учётная запись AeroNyx не требуется.
  2. Подписывайте каждый запрос. Каждый вызов содержит заголовок Authorization, подписанный вашим ключом (Аутентификация).
  3. Запечатывайте каждое сообщение. Зашифруйте и подпишите сообщение на своей стороне и отправляйте только полученный конверт (Конверт сообщения).
  4. Отправьте его через POST /messages/.
  5. Получайте через GET /inbox/, расшифруйте и сохраните каждое сообщение, затем подтвердите его через POST /acks/.
Конечная точкаАутентификацияНазначение
GET /capabilities/не требуетсяОграничения сервиса и поддерживаемые функции
POST /messages/требуетсяОтправить запечатанное сообщение
GET /inbox/требуетсяПолучить неподтверждённые сообщения
POST /acks/требуетсяПодтвердить сообщения, которые вы сохранили

Быстрый старт

Этот скрипт отправляет сообщение от одной новой идентичности другой, читает его, расшифровывает и подтверждает. Сохраните приведённую ниже референсную реализацию как aeronyx_chat.py, установите зависимости командой pip install pynacl requests и запустите скрипт.

python
from __future__ import annotations

import hashlib, json, os, time, uuid

import requests
from nacl.signing import SigningKey

from aeronyx_chat import open_, seal

BASE = "https://api.aeronyx.network"
PREFIX = "/api/relay/developer/v1"


def call(key: SigningKey, method: str, path: str, body: dict | None = None):
    """Signs and sends one request. `path` is the full path after the host."""
    raw = b"" if body is None else json.dumps(body, separators=(",", ":")).encode()
    seconds = str(int(time.time()))
    nonce = os.urandom(16)
    digest = hashlib.sha256(
        b"AeroNyx/DeveloperChatRequestV1\0" + method.encode() + b"\0"
        + path.encode() + b"\0" + seconds.encode() + b"\0"
        + nonce + hashlib.sha256(raw).digest()
    ).digest()
    signature = key.sign(digest).signature.hex()
    pub = key.verify_key.encode().hex()
    headers = {
        "Authorization": f"AeroNyx-Chat-V1 {pub}:{seconds}:{nonce.hex()}:{signature}",
        "Content-Type": "application/json",
    }
    # Send exactly the bytes that were signed.
    return requests.request(method, BASE + path, data=raw or None,
                            headers=headers, timeout=20)


alice, bob = SigningKey.generate(), SigningKey.generate()
bob_address = bob.verify_key.encode().hex()

# 1. Alice seals and sends a message to Bob.
message_id = uuid.uuid4()
now = int(time.time())
sent = call(alice, "POST", PREFIX + "/messages/", {
    "receiver_pubkey": bob_address,
    "message_id": str(message_id),
    "discriminant": 11,
    "sent_at": now,
    "payload_b64": seal(alice, bob.verify_key.encode(), "Hello Bob", message_id, now),
})
print("send:", sent.status_code, sent.json())

# 2. Bob fetches his inbox, decrypts, and then acknowledges.
inbox = call(bob, "GET", PREFIX + "/inbox/?limit=50").json()
for item in inbox["messages"]:
    opened = open_(bob, item["payload_b64"])
    print("received:", opened["plaintext"], "from", opened["sender"][:8])
    # Store the message durably here, then acknowledge it.
    ack = call(bob, "POST", PREFIX + "/acks/", {"message_ids": [item["msg_id"]]})
    print("ack:", ack.status_code, ack.json())

Аутентификация

Каждая конечная точка, кроме GET /capabilities/, требует следующий заголовок:

text
Authorization: AeroNyx-Chat-V1 <public-key-hex>:<unix-seconds>:<nonce-hex>:<signature-hex>
  • public-key-hex — ваш 64-символьный чат-адрес.
  • unix-seconds должно отличаться от часов сервера не более чем на 60 секунд.
  • nonce-hex — 16 случайных байт, записанных как 32 строчных шестнадцатеричных символа. Каждый nonce можно использовать только один раз.
  • signature-hex — подпись Ed25519, записанная как 128 шестнадцатеричных символов, от следующего 32-байтового дайджеста:
text
SHA256(
  "AeroNyx/DeveloperChatRequestV1\0" ||
  UPPERCASE_HTTP_METHOD || "\0" ||
  PATH_WITH_QUERY || "\0" ||
  ASCII_UNIX_SECONDS || "\0" ||
  NONCE_BYTES ||
  SHA256(EXACT_HTTP_BODY_BYTES)
)

PATH_WITH_QUERY — это полный путь запроса в том виде, в котором он отправляется, включая префикс /api/relay/developer/v1 и любую строку запроса. Например:

ЗапросPATH_WITH_QUERY для подписи
POST https://api.aeronyx.network/api/relay/developer/v1/messages//api/relay/developer/v1/messages/
GET https://api.aeronyx.network/api/relay/developer/v1/inbox/?limit=50/api/relay/developer/v1/inbox/?limit=50

EXACT_HTTP_BODY_BYTES — это байты тела запроса в точности в том виде, в каком они отправляются. Поэтому пробелы в JSON и порядок ключей имеют значение: сериализуйте один раз, подпишите эти байты и отправьте те же самые байты. Для запроса без тела подписывается SHA-256 пустой строки.

Тестовый вектор аутентификации

Используйте его, чтобы проверить свой код подписи до обращения к API. Ключ предназначен только для тестирования; никогда не используйте его для реальных сообщений.

json
{
  "seed_hex": "0303030303030303030303030303030303030303030303030303030303030303",
  "public_key_hex": "ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1",
  "method": "POST",
  "path_with_query": "/api/relay/developer/v1/messages/",
  "unix_seconds": "1780000000",
  "nonce_hex": "000102030405060708090a0b0c0d0e0f",
  "body": "{\"receiver_pubkey\":\"8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394\",\"message_id\":\"018f5164-c1b2-7e2b-9d8a-2c60597f4c2f\",\"discriminant\":11,\"sent_at\":1780000000,\"payload_b64\":\"AAAA\"}",
  "body_sha256_hex": "1971a2ef86b2a085e033fd65313981c8668204d49cedbe530875036d1f45c9cc",
  "digest_hex": "6f4d0ca6dcd1221780b2dae45dd94e4bae7e79f266e8bb69f2d50c20de08b276",
  "signature_hex": "14ed2108e1ecf8ce5d04299e59af6d00dec218d62592de127e6a640f235ea4e9703004561fc08e929d0159f92fc51d35a881eec7d0eb16f19d94270c6262cb0c",
  "authorization": "AeroNyx-Chat-V1 ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1:1780000000:000102030405060708090a0b0c0d0e0f:14ed2108e1ecf8ce5d04299e59af6d00dec218d62592de127e6a640f235ea4e9703004561fc08e929d0159f92fc51d35a881eec7d0eb16f19d94270c6262cb0c"
}

Подписи Ed25519 детерминированы, поэтому одни и те же входные данные должны давать в точности такое значение signature_hex. Если ваше значение body_sha256_hex отличается, значит, отличаются байты вашего тела.

Устранение ошибки 401

API отвечает на любой сбой аутентификации одной и той же ошибкой 401, поэтому проверяйте пункты по порядку:

  1. Часы: unix-seconds отличается от реального времени не более чем на 60 секунд.
  2. Nonce новый для каждого запроса, длиной 16 байт, записан как 32 строчных шестнадцатеричных символа.
  3. Подписанный путь включает /api/relay/developer/v1 и строку запроса.
  4. Подписанный метод записан заглавными буквами и совпадает с методом запроса.
  5. Подписанные байты тела — это те байты, которые вы отправляете. Не сериализуйте JSON заново после подписания.
  6. Ваш дайджест и подпись совпадают с тестовым вектором выше.

Конечные точки

Информация о сервисе

GET /capabilities/ не требует аутентификации и возвращает текущие ограничения и поддерживаемые функции.

Отправка сообщения

POST /messages/

json
{
  "receiver_pubkey": "<64 lowercase hex characters>",
  "message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
  "discriminant": 11,
  "sent_at": 1780000000,
  "payload_b64": "<sealed envelope, Base64>"
}

Все пять полей обязательны, других полей быть не должно.

  • receiver_pubkey — чат-адрес получателя. Он должен отличаться от вашего.
  • message_id — UUID. Используйте новый случайный UUID для каждого сообщения и сохраняйте точное тело запроса для повторных попыток. Отправка того же message_id с другими полями возвращает 409 idempotency_conflict.
  • discriminant равен 11 для личного сообщения, которое должен увидеть пользователь AeroNyx App.
  • sent_at — время Unix в секундах; оно должно отличаться от времени сервера не более чем на 300 секунд.
  • payload_b64 — запечатанный конверт, описанный в разделе Конверт сообщения.

Реле проверяет только то, что payload_b64 является корректной строкой Base64 размером от 1 байта до 191 КиБ. Оно не может отличить конверт от других данных, поэтому полезная нагрузка, не являющаяся корректным конвертом, принимается здесь и без уведомления отбрасывается приложением получателя. Проверьте свой конверт по эталонному вектору.

Успешный запрос возвращает HTTP 202:

json
{
  "message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
  "accepted": true,
  "delivered_live": false
}

accepted означает, что ретранслятор надёжно сохранил шифротекст. delivered_live равно true, когда ретранслятор без ошибки передал сообщение в живую доставку. Оно равно true, даже если у получателя нет ни одного открытого соединения, поэтому не говорит, находится ли кто-то в сети. Ни одно из полей не означает, что получатель расшифровал или показал сообщение; это означает квитанция о доставке в вашем почтовом ящике (см. «События во входящих»).

Получение входящих

GET /inbox/?limit=50

Возвращает до 100 неподтверждённых событий. Одно и то же событие может появиться снова после тайм-аута или перезапуска.

json
{
  "messages": [{
    "sender_pubkey": "<hex>",
    "discriminant": 11,
    "payload_b64": "<ciphertext>",
    "timestamp": 1780000000,
    "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"
  }],
  "has_more": false
}

Если has_more равно true, подтвердите то, что вы сохранили, и запросите снова.

События во входящих

Во входящих хранится всё, что ретранслятор поставил в очередь для вашей идентичности, а не только новые сообщения. Элемент без control_type — это сообщение: откройте payload_b64, как описано в разделе «Конверт сообщения». Элемент с control_type — это событие, связанное с сообщением. Проверяйте control_type, прежде чем что-либо расшифровывать: у квитанций нет payload_b64.

control_typeЗначениеmsg_id
message_receiptApp получателя расшифровало и сохранило отправленное вами сообщение.message_id вашего сообщения
message_readПолучатель прочитал ваше сообщение. Отправляется, только если у него включены отчёты о прочтении.message_id вашего сообщения
message_editОтправитель изменил сообщение (target_msg_id — тот же ID).ID изменённого сообщения
message_revokeОтправитель отозвал сообщение.ID отозванного сообщения
message_reactionРеакция на target_msg_id добавлена или удалена.ID реакции

Изменения, отзывы и реакции подписаны (payload_sig); их форматы описаны в Интеграция клиента с AeroNyx Chat Relay. Подтверждение ID удаляет из очереди все элементы с этим ID, поэтому обработайте страницу целиком, прежде чем подтверждать. Подтверждайте и те типы событий, которые вы не обрабатываете, иначе они будут возвращаться снова.

Подтверждение сообщений

POST /acks/

json
{
  "message_ids": ["018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"]
}

Подтверждайте сообщение только после того, как вы проверили его, расшифровали и записали в надёжное хранилище. Неизвестные идентификаторы не попадают в список acknowledged в ответе.

Модель доставки

  • Доставка выполняется как минимум один раз. Устраняйте дубликаты по сочетанию control_type и msg_id: квитанция содержит ID сообщения, которое она подтверждает.
  • Недоставленный шифртекст хранится 72 часа. Не рассматривайте сервис как постоянный архив истории.
  • Повторяйте неудавшуюся отправку с точно таким же телом и новым nonce аутентификации.

Доставка в реальном времени

Отправка

POST /messages/ передаёт ваше сообщение во все открытые соединения получателя и одновременно хранит его в очереди, пока App получателя его не подтвердит. Получатель с подключённым App получает сообщение сразу; получатель не в сети получит его при следующем подключении App, в пределах 72 часов.

Сообщения, отправленные через этот API, не вызывают push-уведомление. Получатель с закрытым App не получает оповещения и увидит сообщение, когда в следующий раз откроет App.

Получение опросом

GET /inbox/ отвечает сразу, независимо от того, есть ли что-то в ожидании. Сообщение доходит до вас только при следующем вызове, поэтому видимая задержка равна интервалу опроса. Для переписки получайте сообщения через WebSocket.

Получение в реальном времени

Подключите ту же идентичность к WebSocket Chat Relay. Учётная запись и регистрация не нужны.

  1. Откройте wss://api.aeronyx.network/ws/relay/ и войдите с тем же ключом Ed25519, как описано в разделе о входе в Интеграция клиента с AeroNyx Chat Relay.
  2. После входа ретранслятор сначала повторно отправляет всё, что ещё стоит для вас в очереди (from_offline равно true), а затем отправляет каждый новый элемент сразу по прибытии. Сообщения приходят кадрами relay_envelope с теми же полями, что и сообщение во входящих; квитанции, изменения, отзывы и реакции приходят кадрами, у которых type равен control_type из таблицы выше.
  3. Сохранив элемент, подтвердите его:
json
{"type": "relay_offline_ack", "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"}

WebSocket и GET /inbox/ читают одну и ту же очередь: элемент, подтверждённый в одном канале, исчезает и из другого, а POST /acks/ работает для обоих. Можно продолжать отправлять через HTTPS и при этом получать через WebSocket.

Ограничения и ошибки

ОграничениеЗначение
Запечатанная полезная нагрузка191 КиБ (JSON-запрос с Base64 остаётся ниже ограничения на размер запроса)
Тело запроса256 КиБ
Страница входящих и пакет подтверждений100 элементов
Хранение для офлайн-доставки72 часа
СтатусЗначение
400 invalid_jsonТело не является JSON-объектом.
400 invalid_requestТело содержит не ровно пять полей, receiver_pubkey не состоит из 64 строчных шестнадцатеричных символов или совпадает с вашим адресом, discriminant выходит за пределы 1-255, message_id не является UUID или payload_b64 не является корректной строкой Base64.
400 invalid_payloadДекодированная полезная нагрузка пуста или превышает 191 КиБ.
400 message_expiredsent_at отличается от времени сервера более чем на 300 секунд.
401Аутентификация отсутствует, просрочена, недействительна или повторно использована. См. Устранение ошибки 401.
403Политика получателя отклонила этого отправителя.
409Конфликт идемпотентности либо идентичный запрос всё ещё выполняется.
429Достигнут лимит отправителя: 50 запросов в секунду и 100 000 в сутки по UTC для каждой идентичности. Подождите время, указанное в заголовке Retry-After.
503Надёжная доставка или аутентификация временно недоступны. Повторите точное тело с новым nonce.

Что не охватывает это руководство

Это руководство охватывает личные сообщения через HTTPS. Для всего остального подключите ту же идентичность к WebSocket ретранслятора чата и см. Интеграция клиента с AeroNyx Chat Relay:

  • Групповые сообщения, присутствие и индикаторы набора.
  • Зашифрованные вложения, включая голосовые сообщения и видеофайлы.
  • Голосовые и видеозвонки. Кадры сигнализации перечислены в этой статье, но полное руководство по интеграции звонков (медиатокен, подпись приглашения и шифрование звонка) пока не опубликовано.

Конфиденциальность и видимость

Содержимое сообщений защищено сквозным шифрованием: оно запечатывается на вашей стороне ключом, выведенным из идентичностей отправителя и получателя, которого AeroNyx никогда не имеет, а API хранит и пересылает только шифртекст. Для доставки сообщений сервис видит открытые ключи отправителя и получателя, время сообщения, размер, состояние доставки и IP-адрес каждого запроса. HTTPS не скрывает эти метаданные от сервиса.

Конверт сообщения

Сообщения, которые должен увидеть пользователь AeroNyx App, используют discriminant: 11 и сквозным образом запечатанный конверт в payload_b64. Реле никогда не видит ключ или открытый текст. Все целые числа хранятся в порядке little-endian.

1. Общий ключ

Обе стороны выводят один и тот же 32-байтовый ключ из своих чат-идентичностей Ed25519:

text
my_x25519_secret = clamp(SHA-512(my_ed25519_seed)[0..32])
peer_x25519      = Edwards-to-Montgomery(peer_ed25519_public_key)
raw              = X25519(my_x25519_secret, peer_x25519)
key              = HKDF-SHA256(salt = empty, ikm = raw, info = "AERONYX-P2P-KEY", length = 32)

С libsodium это crypto_sign_ed25519_sk_to_curve25519, crypto_sign_ed25519_pk_to_curve25519 и crypto_scalarmult, а затем HKDF. Отправители всегда используют ключ HKDF. Очень старые версии приложения отправляли сообщения напрямую с raw, поэтому получатель может попробовать raw, если ключ HKDF не расшифровывает сообщение.

2. Открытый текст

Текстовое сообщение — это его текст в UTF-8. Сообщения из приложения, содержащие вложения, ответы или пересылки, представляют собой JSON-объект с "type": "aeronyx_message" и полем text; если вы не поддерживаете остальное, отображайте text.

3. Шифрование

XChaCha20-Poly1305 со свежим случайным 24-байтовым nonce и без ассоциированных данных. ciphertext ниже — это выход шифра, включая его 16-байтовый тег.

4. Подпись

Отправитель подписывает эти 121 байт с помощью Ed25519:

text
sender_pubkey   32 bytes
message_id      16 bytes   (the raw bytes of the UUID sent as message_id)
receiver_pubkey 32 bytes
timestamp        8 bytes   (u64, Unix seconds; use the same value as sent_at)
content_type     1 byte    (0 = text)
SHA-256(ciphertext) 32 bytes

5. Конверт

text
message_id      16 bytes
sender_pubkey   32 bytes
receiver_pubkey 32 bytes
timestamp        8 bytes   (u64)
ciphertext_len   8 bytes   (u64)
ciphertext       ciphertext_len bytes
nonce           24 bytes
content_type     4 bytes   (u32, 0 = text)
signature       64 bytes

payload_b64 — это стандартный Base64 этих байтов. Обратите внимание, что content_type занимает 1 байт в подписываемых данных, но 4 байта в конверте.

Приём

Разберите конверт, проверьте, что receiver_pubkey — это ваша идентичность, проверьте подпись по sender_pubkey из конверта, затем расшифруйте ключом, выведенным для этого отправителя. Отбросьте сообщение, если любой шаг не удался. Приложение отбрасывает живые сообщения, у которых timestamp отличается от его часов более чем на 300 секунд.

Референсная реализация (Python)

python
"""AeroNyx 1:1 chat envelope (discriminant 11). Requires PyNaCl."""

from __future__ import annotations

import base64
import hashlib
import hmac
import os
import struct
import uuid

from nacl.bindings import (
    crypto_aead_xchacha20poly1305_ietf_decrypt,
    crypto_aead_xchacha20poly1305_ietf_encrypt,
    crypto_scalarmult,
    crypto_sign_ed25519_pk_to_curve25519,
    crypto_sign_ed25519_sk_to_curve25519,
)
from nacl.signing import SigningKey, VerifyKey

CONTENT_TEXT = 0
_HKDF_INFO = b"AERONYX-P2P-KEY"


def _hkdf_sha256(ikm: bytes, info: bytes, length: int = 32) -> bytes:
    # RFC 5869 with an empty salt (= 32 zero bytes).
    prk = hmac.new(b"\x00" * 32, ikm, hashlib.sha256).digest()
    okm, block, counter = b"", b"", 1
    while len(okm) < length:
        block = hmac.new(prk, block + info + bytes([counter]), hashlib.sha256).digest()
        okm += block
        counter += 1
    return okm[:length]


def chat_key(my_key: SigningKey, peer_public: bytes, hkdf: bool = True) -> bytes:
    """X25519 over the Ed25519 identities, then HKDF-SHA256."""
    my_secret = bytes(my_key) + my_key.verify_key.encode()  # libsodium 64-byte sk
    raw = crypto_scalarmult(
        crypto_sign_ed25519_sk_to_curve25519(my_secret),
        crypto_sign_ed25519_pk_to_curve25519(peer_public),
    )
    return _hkdf_sha256(raw, _HKDF_INFO) if hkdf else raw


def _sign_data(sender, message_id, receiver, timestamp, content_type, ciphertext):
    return (
        sender
        + message_id
        + receiver
        + struct.pack("<Q", timestamp)
        + bytes([content_type])  # one byte here ...
        + hashlib.sha256(ciphertext).digest()
    )


def seal(my_key: SigningKey, receiver: bytes, text: str, message_id: uuid.UUID,
         timestamp: int, nonce: bytes | None = None) -> str:
    """Returns payload_b64 for POST /messages/ (discriminant 11)."""
    nonce = nonce if nonce is not None else os.urandom(24)
    key = chat_key(my_key, receiver)
    ciphertext = crypto_aead_xchacha20poly1305_ietf_encrypt(
        text.encode("utf-8"), None, nonce, key)  # ciphertext || 16-byte tag, no AAD
    sender = my_key.verify_key.encode()
    mid = message_id.bytes
    signature = my_key.sign(
        _sign_data(sender, mid, receiver, timestamp, CONTENT_TEXT, ciphertext)
    ).signature
    envelope = (
        mid
        + sender
        + receiver
        + struct.pack("<Q", timestamp)
        + struct.pack("<Q", len(ciphertext))
        + ciphertext
        + nonce
        + struct.pack("<I", CONTENT_TEXT)  # ... four bytes here
        + signature
    )
    return base64.b64encode(envelope).decode("ascii")


def open_(my_key: SigningKey, payload_b64: str) -> dict:
    """Verifies and decrypts an inbox payload addressed to my_key."""
    env = base64.b64decode(payload_b64, validate=True)
    if len(env) < 16 + 32 + 32 + 8 + 8 + 16 + 24 + 4 + 64:
        raise ValueError("envelope too short")
    mid, sender, receiver = env[0:16], env[16:48], env[48:80]
    timestamp, ct_len = struct.unpack("<QQ", env[80:96])
    if len(env) != 96 + ct_len + 24 + 4 + 64:
        raise ValueError("envelope length mismatch")
    ciphertext = env[96:96 + ct_len]
    nonce = env[96 + ct_len:120 + ct_len]
    (content_type,) = struct.unpack("<I", env[120 + ct_len:124 + ct_len])
    signature = env[124 + ct_len:]
    if receiver != my_key.verify_key.encode():
        raise ValueError("envelope is addressed to another identity")
    VerifyKey(sender).verify(
        _sign_data(sender, mid, receiver, timestamp, content_type, ciphertext), signature)
    # Current App versions use the HKDF key; very old ones sent with the raw key.
    for hkdf in (True, False):
        try:
            plaintext = crypto_aead_xchacha20poly1305_ietf_decrypt(
                ciphertext, None, nonce, chat_key(my_key, sender, hkdf))
            break
        except Exception:  # noqa: BLE001 - try the legacy key next
            plaintext = None
    if plaintext is None:
        raise ValueError("decryption failed")
    return {
        "message_id": str(uuid.UUID(bytes=mid)),
        "sender": sender.hex(),
        "timestamp": timestamp,
        "content_type": content_type,
        "plaintext": plaintext.decode("utf-8"),
    }

Эталонный вектор

Фиксированные ключи, nonce, идентификатор и время — только для тестирования. Никогда не используйте эти ключи.

json
{
  "sender_seed_hex": "0101010101010101010101010101010101010101010101010101010101010101",
  "sender_pubkey": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",
  "receiver_seed_hex": "0202020202020202020202020202020202020202020202020202020202020202",
  "receiver_pubkey": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394",
  "message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
  "timestamp": 1780000000,
  "nonce_hex": "000102030405060708090a0b0c0d0e0f1011121314151617",
  "plaintext": "Hello from AeroNyx",
  "shared_key_hex": "dd08f8bb45a43c9bc186fdaf436d47e8e05b0aca44fa2bb9473769da0407652f",
  "payload_b64": "AY9RZMGyfiudiixgWX9ML4qI4910CfGV/VLbLTy6XXLKZwm/HZQSG/N0iAG0D29cgTl3Dqh9F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5QApRhqAAAAACIAAAAAAAAA1vVyzdTx1kAdPEI7AGO12ppwLwaME0K9PJKd+u7OgWq0rwABAgMEBQYHCAkKCwwNDg8QERITFBUWFwAAAACAlpInhB5A9cD4LqKAT0OE0OpbMqT5w/xB8IiHrzuyg9LPjF9gDHdOekPNoHNxDMeu3Ift0UB69OGSofdfwKIN"
}

seal(alice, bob_public, "Hello from AeroNyx", message_id, 1780000000, nonce=bytes(range(24))) должен выдавать в точности это значение payload_b64, а open_(bob, payload_b64) должен возвращать открытый текст. Этот вектор проверен на собственном согласовании ключей, подписи и шифре приложения AeroNyx App, а также на строгом декодере конвертов узла.

<!-- faq:start -->

Часто задаваемые вопросы

Что такое Chat API AeroNyx?

Это версионируемый HTTPS API для обмена сквозным образом зашифрованными личными сообщениями с пользователями AeroNyx с сервера, бота или приложения. У него четыре конечные точки: capabilities, отправка, входящие и подтверждение. Он передаёт только запечатанные конверты; содержимое шифруется на вашей стороне.

Нужна ли учётная запись AeroNyx, чтобы пользоваться API?

Нет. Идентичностью служит пара ключей Ed25519. Сгенерируйте её локально и подписывайте ею каждый запрос; открытый ключ — это ваш чат-адрес.

Что подписывать: /messages/ или полный путь?

Полный путь в том виде, в котором он отправляется, начиная с /api/relay/developer/v1, плюс строку запроса, если она есть. Подпись одного лишь /messages/ приводит к ошибке 401. Тестовый вектор аутентификации на этой странице показывает точные входные данные.

Почему я получаю 401?

Любой сбой аутентификации возвращает одну и ту же ошибку 401. Обычные причины: часы отличаются более чем на 60 секунд, повторно использованный nonce, подписанный путь или тело отличаются от отправленных либо метод HTTP в подписанных данных записан строчными буквами. Пройдите контрольный список в разделе «Устранение ошибки 401» и сравните свой дайджест с тестовым вектором.

Почему API возвращает 202, но сообщение так и не появляется в AeroNyx App?

Реле принимает любую корректную полезную нагрузку Base64, но приложение показывает только запечатанный конверт, который может проверить и расшифровать. Сверьте свой payload_b64 с эталонным вектором на этой странице, убедитесь, что receiver_pubkey в конверте — это получатель, и держите временную метку конверта в пределах 300 секунд от текущего времени. Простой текст, закодированный в Base64, принимается, но никогда не отображается.

Почему API возвращает invalid_request?

Наиболее частые причины: отправка на собственный открытый ключ, тело, содержащее не ровно пять описанных полей, message_id, не являющийся UUID, или discriminant вне диапазона 1-255. См. таблицу ошибок в разделе «Ограничения и ошибки».

Может ли AeroNyx читать сообщения, отправленные через этот API?

Нет. Конверты шифруются ключом, выведенным из идентичностей отправителя и получателя, которого AeroNyx никогда не имеет. Сервис видит метаданные доставки, такие как открытые ключи, время, размеры и IP-адреса.

Как долго хранятся недоставленные сообщения?

До 72 часов. Подтверждайте каждое сообщение только после того, как вы расшифровали и сохранили его; неподтверждённые сообщения могут быть доставлены повторно.

Какие языки программирования можно использовать?

Любой язык, поддерживающий Ed25519, X25519, HKDF-SHA256 и XChaCha20-Poly1305, что включает привязки libsodium для большинства языков. На этой странице приведены полная референсная реализация на Python и работающий быстрый старт.

Работает ли AeroNyx Chat API в реальном времени?

Отправка — да: получатель с подключённым App получает ваше сообщение сразу. Получение через HTTPS работает опросом, поэтому новые сообщения вы увидите при следующем вызове GET /inbox/. Чтобы получать в реальном времени, подключите ту же идентичность к WebSocket Chat Relay по адресу wss://api.aeronyx.network/ws/relay/; он читает ту же очередь.

Получит ли пользователь AeroNyx уведомление о моём сообщении?

Сейчас нет. Сообщения, отправленные через этот API, не вызывают push-уведомление, поэтому пользователь с закрытым App увидит ваше сообщение при следующем открытии. Пользователь с подключённым App видит его сразу.

<!-- faq:end -->