Integración de clientes con AeroNyx Chat Relay

AeroNyx25 min de lectura

Referencia de integración de AeroNyx Chat Relay: autenticación WebSocket, mensajes sellados 1:1 y de grupo, entrega sin conexión, confirmaciones, reacciones, estado de conexión, archivos adjuntos cifrados, notificaciones push y límites de frecuencia.

Preparar una instrucción de integración

Elige la tecnología y la tarea, y copia la instrucción a tu asistente de programación. Se genera localmente; no se envía a un servicio de IA.

Instrucción generada

Esta es la referencia de integración de AeroNyx Chat Relay: el servicio WebSocket y HTTPS en api.aeronyx.network que transporta mensajes 1:1 cifrados de extremo a extremo, mensajes de grupo, confirmaciones, reacciones, estado de conexión y archivos adjuntos cifrados entre identidades de AeroNyx.

Está dirigida a ingenieros que desarrollan clientes, bots y servicios compatibles con AeroNyx, y a agentes de programación con IA que los implementan. Cada trama, campo y límite de esta página refleja el relé de producción y la AeroNyx App a octubre de 2026.

Chat Relay es la ruta de entrega centralizada. Coexiste con la ruta descentralizada de nodos (enrutamiento cebolla y buzones anónimos; consulte Entrega verificada en dos saltos); un cliente puede usar ambas. Para una integración HTTPS de solicitud/respuesta sin WebSocket, consulte Central Chat HTTPS API v1.

Modelo de confianza

El relé no tiene visibilidad del contenido. Los clientes cifran y firman todo antes de que llegue al relé, y el relé enruta, encola y aplica límites de frecuencia a texto cifrado opaco.

El relé nunca recibe:

  • texto de mensajes, emojis de reacciones, ediciones ni cargas útiles de grupo en texto claro
  • claves de chat, claves de grupo, claves de archivos adjuntos ni nonces
  • contenido de archivos adjuntos, nombres de archivo, miniaturas, formas de onda ni transcripciones
  • claves privadas de identidad

El relé sí observa metadatos de entrega, y los integradores deben considerarlos visibles para el operador:

  • claves públicas del remitente y del destinatario, ID de grupo e ID de mensaje
  • marcas de tiempo, tamaños de carga útil y estado de entrega, confirmación y lectura
  • estado de conexión, estado en primer plano e indicadores de escritura (enviados como tramas en texto claro)
  • metadatos de señalización de llamadas (nombre de sala, ID de llamada, indicador de vídeo)
  • tamaño del texto cifrado de los adjuntos, tipo de contenido declarado y caducidad
  • el indicador contact_request y los metadatos de conexión a nivel de IP

La confidencialidad del contenido proviene del cifrado de extremo a extremo, no del control de acceso en el relé. Diseñe en consecuencia: un atacante que obtenga un texto cifrado almacenado debe seguir sin poder leerlo.

Identidades y claves

Una identidad de chat de AeroNyx es un par de claves Ed25519. La clave pública de 32 bytes, escrita como 64 caracteres hexadecimales en minúscula, es la dirección. Genere las claves en el dispositivo y nunca envíe la clave privada a ningún lugar.

A partir de un par de identidad se derivan dos claves:

  • Clave de chat (1:1). HKDF-SHA256(salt = empty, ikm = X25519(my_secret, peer_public), info = "AERONYX-P2P-KEY", length = 32), donde ambas claves Ed25519 se convierten a X25519 (SHA-512(seed)[0..32] con clamping para la clave secreta, conversión de Edwards a Montgomery para la clave pública). Ambos pares derivan la misma clave.
  • Firmas de mensajes. Ed25519 con la clave de identidad, sobre las secuencias exactas de bytes definidas en esta página.

Use siempre hexadecimal en minúscula para las claves públicas en las tramas. El relé no normaliza las mayúsculas y minúsculas en todas las claves de cola.

Autenticación

Firma RelayAuth

El inicio de sesión por WebSocket y todos los endpoints HTTPS autenticados usan la misma firma:

text
digest    = SHA256("AeroNyx-RelayAuth-v1" || pubkey[32] || timestamp as u64 little-endian)
signature = Ed25519(identity_key, digest)

"AeroNyx-RelayAuth-v1" son los 20 bytes ASCII sin terminador. timestamp está en segundos Unix y debe estar a menos de 300 segundos de la hora del servidor.

La firma vincula únicamente la identidad y la hora. No vincula el método, la ruta, el cuerpo ni la conexión, y no hay nonce. Genere una marca de tiempo nueva para cada solicitud, envíela solo a través de TLS y nunca la registre en logs.

Encabezado HTTPS

http
Authorization: Relay <pubkey-hex>:<timestamp>:<signature-hex>

Una verificación fallida devuelve HTTP 401 con {"success": false, "error": "<reason>"}. Los motivos incluyen missing_auth_header, malformed_auth_header, invalid_timestamp, timestamp_expired, invalid_pubkey e invalid_signature.

Conexión WebSocket

Endpoint

text
wss://api.aeronyx.network/ws/relay/

Los clientes nativos se conectan sin encabezado Origin. Los navegadores deben conectarse desde un origen permitido; cualquier otro origen, o un Host desconocido, se cierra con el código 1008.

Inicio de sesión

El servidor acepta el socket y, a continuación, espera una trama auth en un plazo de 30 segundos:

json
{
  "type": "auth",
  "pubkey": "<64 hex>",
  "timestamp": 1780000000,
  "signature": "<128 hex>"
}

Éxito:

json
{ "type": "auth_ack", "session_id": "<uuid>", "server_ts": 1780000001 }

Use server_ts para estimar el desfase del reloj; las marcas de tiempo de los mensajes se comprueban con la misma ventana de ±300 segundos.

Tras auth_ack, el servidor inicia su latido, suscribe la conexión a sus canales y reproduce de inmediato la cola sin conexión (consulte Entrega sin conexión).

Un fallo envía {"type": "auth_error", "reason": "<reason>"} y cierra el socket con el código 4001. Motivos: missing_fields, timestamp_expired, invalid_pubkey, invalid_signature_length, invalid_signature_encoding, invalid_signature, internal_error. Si se agota el tiempo de inicio de sesión, se envía el motivo timeout y se cierra con 4002.

Cualquier otra trama anterior al inicio de sesión se responde con auth_error con el motivo authentication_required; el socket permanece abierto.

Latido y estado en primer plano

DirecciónTramaComportamiento
servidor → cliente{"type":"ping"} cada 30 sResponda {"type":"pong"}.
cliente → servidor{"type":"ping"}El servidor responde {"type":"pong"}.
cliente → servidor{"type":"presence_state","foreground":true}Marca esta conexión como activa.
cliente → servidor{"type":"presence_state","foreground":false}La marca como en segundo plano, de modo que el relé puede enviar una notificación push para los mensajes nuevos.

El relé considera que una identidad está en línea solo mientras su conexión envía ping, pong o presence_state con foreground: true al menos cada 90 segundos. La AeroNyx App envía un ping cada 15 segundos mientras está en primer plano y considera la conexión inactiva cuando se pierden más de tres pongs.

Las conexiones de larga duración deben reconectarse al menos una vez cada 24 horas. Los mensajes nunca se pierden cuando una conexión deja de recibir tramas en vivo sin aviso, porque cada mensaje también se encola y se reproduce al iniciar sesión, pero la entrega en vivo solo se reanuda tras la reconexión.

Reglas de las tramas

  • Solo tramas de texto, un objeto JSON por trama. Las tramas binarias se ignoran.
  • El tamaño máximo de trama es de 1.048.576 caracteres. Las tramas más grandes se rechazan con {"type":"error","reason":"message_too_large"}. Mantenga payload_b64 por debajo de unos 800 KiB para dejar espacio al resto de la trama.
  • Las tramas mal formadas devuelven error con el motivo invalid_json, invalid_json_type o unknown_type.

Trama de error genérica:

json
{ "type": "error", "reason": "<reason>", "retry_after": 0 }

Los errores por límite de frecuencia también incluyen scope.

Códigos de cierre

CódigoSignificado
1008Host u Origin no permitido.
4000El servidor no pudo enviar su latido.
4001El inicio de sesión falló.
4002Se agotó el tiempo de inicio de sesión.

Reconecte con retroceso exponencial y variación aleatoria (jitter). La AeroNyx App espera 2^(attempt-1) segundos, limitados al rango de 1 a 60 segundos, multiplicados por un factor aleatorio entre 0,8 y 1,2.

Envío de un mensaje 1:1

1. Construir el sobre sellado

La carga útil de un mensaje 1:1 es un ChatEnvelope firmado y cifrado. Su construcción exacta, una implementación de referencia en Python y un vector de prueba de referencia se encuentran en Central Chat HTTPS API v1: formato del sobre sellado. Ambas API usan el mismo sobre.

En resumen: XChaCha20-Poly1305 con la clave de chat, una firma Ed25519 sobre una transcripción de 121 bytes y un formato binario fijo. content_type es 0 para todos los mensajes, incluidos los que tienen archivos adjuntos.

El texto claro es el texto UTF-8 en un mensaje simple, o un objeto JSON en los mensajes con archivos adjuntos, respuestas, reenvíos o vistas previas de enlaces:

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": "..." }
}

Todos los campos salvo type y text son opcionales. Los receptores deben mostrar como texto simple cualquier texto claro que no sea un objeto JSON con "type": "aeronyx_message". El objeto de archivo adjunto se define en Archivos adjuntos cifrados.

2. Firmar la trama

Cada trama de mensaje lleva una segunda firma, payload_sig, que el relé verifica antes de aceptarla:

text
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 es el payload_b64 decodificado. Use el discriminante 11 para mensajes y ediciones, y 12 para reacciones. payload_sig se codifica en hexadecimal.

3. Enviar

json
{
  "type": "relay_send",
  "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
  "receiver_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>"
}
CampoReglas
msg_id32 caracteres hexadecimales en minúscula: el message_id de 16 bytes del sobre. La App receptora almacena el mensaje con este ID, por lo que debe coincidir con el sobre.
receiver_pubkeyLa clave pública del destinatario.
discriminant11.
timestampSegundos Unix, dentro de ±300 s de la hora del servidor.
suppress_pushOpcional. true no envía ninguna notificación push.
contact_requestOpcional. Marca un primer mensaje a alguien que exige verificación de contacto (consulte Verificación de contactos).

4. Acuse de recibo

json
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
  • durable (y queued) es true una vez que el mensaje se ha almacenado en la cola sin conexión del destinatario. Trate durable: true como «enviado».
  • delivered significa que el destinatario tenía una conexión activa en primer plano. No es prueba de recepción; para ello, use las confirmaciones de entrega.

Espere hasta 15 segundos al acuse de recibo antes de considerar fallido el intento.

Rechazos y errores

json
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
RespuestaMotivoAcción
send_rejectedverification_requiredEl destinatario solo acepta mensajes de contactos. No reintente.
send_rejectedcontact_request_rate_limitedSe alcanzó el límite de solicitudes de contacto para este destinatario. No reintente.
errormissing_fieldsFalta un campo obligatorio o está vacío.
errorinvalid_timestamp, timestamp_expiredCorrija el reloj y vuelva a generar la marca de tiempo.
errorinvalid_payload_sigpayload_sig no supera la verificación.
errorrate_limitedReintente transcurridos retry_after segundos.

Reintentos

Reintente un mensaje sin acuse de recibo con el mismo msg_id y los mismos bytes de sobre. Como el relé rechaza las tramas con más de 300 segundos de antigüedad, calcule un timestamp de trama y un payload_sig nuevos para cada reintento; el sobre conserva su marca de tiempo original. El relé y los receptores eliminan duplicados por msg_id.

Alternativa HTTPS

Cuando el WebSocket no está disponible, el mismo mensaje puede publicarse a través de HTTPS:

http
POST /api/relay/push/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
json
{
  "receiver_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>"
}

El éxito es HTTP 200 con {"success": true}. Los fallos de verificación de contacto devuelven 400 con verification_required o contact_request_rate_limited, y el límite de frecuencia devuelve 429 con Retry-After. Los mensajes enviados por esta vía se encolan para el destinatario, pero no generan una notificación push, así que vuelva a enviarlos por el WebSocket cuando se reconecte.

Recepción de mensajes

Los mensajes entrantes llegan como relay_envelope:

json
{
  "type": "relay_envelope",
  "sender_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
  "from_offline": false
}

Para discriminant: 11:

  1. Elimine duplicados por msg_id. El mismo mensaje puede llegar en vivo y de nuevo desde la cola sin conexión.
  2. Analice el sobre y compruebe que su receiver_pubkey es su identidad.
  3. En las tramas en vivo (from_offline: false), descarte el mensaje si la marca de tiempo del sobre difiere en más de 300 segundos de su reloj.
  4. Verifique la firma del sobre con el sender_pubkey del sobre. Esa clave, y no el sender_pubkey de la trama, es el remitente autenticado.
  5. Descifre con la clave de chat derivada de ese remitente. Las versiones muy antiguas de la App cifraban con la salida X25519 sin procesar; pruébela si la clave HKDF falla.
  6. Almacene el mensaje de forma persistente y, a continuación, envíe una confirmación de entrega y, para from_offline: true, un acuse de recibo de la cola sin conexión.

Los sobres 1:1 se entregan sin payload_sig; la firma del sobre es la comprobación de autenticidad. Las tramas también pueden incluir contact_request: true.

Si un mensaje no va dirigido a usted, no supera la verificación o no se puede descifrar, descártelo sin mostrar nada.

Entrega sin conexión

Cada mensaje, edición, revocación, reacción, confirmación de entrega y confirmación de lectura se escribe en la cola sin conexión del destinatario antes de la entrega en vivo. La cola se reproduce automáticamente tras cada inicio de sesión y bajo petición:

json
{ "type": "relay_pull" }

El relé reproduce todos los elementos encolados con sus tipos de trama habituales y from_offline: true, ordenados por marca de tiempo, seguidos de:

json
{ "type": "relay_pull_done", "count": 12, "has_more": false }

La entrega es de tipo «al menos una vez». Los elementos permanecen en la cola hasta que se confirma su recepción:

json
{ "type": "relay_offline_ack", "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834" }

Confirme las reacciones con "reaction_id" en lugar de "msg_id". Confirme solo después de que el elemento se haya almacenado de forma persistente en el dispositivo. El relé responde con {"type":"relay_offline_ack","msg_id":"...","success":true}.

Límites de la cola:

LímiteValor
Elementos por destinatario1.000. Cuando está llena, los elementos nuevos se rechazan y el remitente recibe durable: false.
Retención72 horas desde que se añadió el elemento más reciente.
Tamaño de la carga útil1 MiB decodificado, dentro del límite de trama de 1 MiB.

El relé es un búfer de entrega, no un historial de mensajes. Conserve el historial en el dispositivo.

Confirmaciones de entrega y de lectura

Confirmación de entrega

Envíela después de almacenar un mensaje:

json
{ "type": "message_receipt", "receiver_pubkey": "<original sender>", "msg_id": "...", "timestamp": 1780000010 }

El relé responde message_receipt_ack y entrega {"type":"message_receipt","sender_pubkey":"...","msg_id":"...","timestamp":...} al remitente original.

Confirmación de lectura

json
{ "type": "message_read", "receiver_pubkey": "<original sender>", "msg_id": "<latest read message>", "timestamp": 1780000200, "enabled": true }

Las confirmaciones de lectura funcionan como marcas de nivel: la App marca como leídos el mensaje indicado y todos los mensajes salientes anteriores de la conversación. Envíe una solo cuando el usuario tenga activadas las confirmaciones de lectura.

El relé aplica una regla de reciprocidad en el envío, en la entrega en vivo y en la reproducción. Una confirmación de lectura solo se entrega si ambos usuarios son contactos mutuos y ambos tienen read_receipts_enabled. En caso contrario, el remitente recibe:

json
{ "type": "message_read_ack", "msg_id": "...", "delivered": false, "suppressed": true, "reason": "receiver_read_receipts_disabled" }
MotivoSignificado
client_disabledLa trama incluía enabled: false o read_receipts_enabled: false.
not_mutual_contactLos usuarios no son contactos mutuos.
reader_read_receipts_disabledEl lector tiene desactivadas las confirmaciones de lectura.
receiver_read_receipts_disabledEl remitente original tiene desactivadas las confirmaciones de lectura.
invalid_pubkeyUna clave pública está mal formada.

Una confirmación de lectura entregada se confirma con {"type":"message_read_ack","msg_id":"...","delivered":true}.

Ediciones y revocaciones

Edición

Una edición es un nuevo sobre sellado (con su propio message_id aleatorio) que contiene el contenido de reemplazo completo, enviado con referencia al ID del mensaje original:

json
{
  "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 usa la fórmula de Firmar la trama con el discriminante 11. Una edición que elimina todos los archivos adjuntos establece "attachments_edited": true en su JSON en texto claro. El relé responde message_edit_ack. Los receptores verifican y descifran el sobre como un mensaje y solo deben aplicar una edición si su remitente escribió el mensaje original.

Revocación

json
{
  "type": "message_revoke",
  "receiver_pubkey": "<64 hex>",
  "sender_pubkey": "<64 hex>",
  "msg_id": "<original msg_id>",
  "timestamp": 1780000500,
  "payload_sig": "<128 hex>"
}

La firma de revocación se calcula sobre los bytes sin procesar, sin hash:

text
payload_sig = Ed25519(identity_key,
    "aeronyx-message-revoke-v1" || sender_pubkey[32] || receiver_pubkey[32]
    || UTF-8(msg_id) || timestamp as u64 little-endian)

El relé responde message_revoke_ack. El relé no comprueba la autoría: los receptores deben verificar la firma y aplicar una revocación solo si su remitente escribió el mensaje original. Para eliminar los archivos adjuntos de un mensaje revocado, llame a POST /api/relay/blob/{blob_id}/delete/.

Reacciones

Una reacción 1:1 es un sobre sellado cuyo message_id es el ID de la reacción, con content_type 2 y el texto claro {"emoji": "❤️", "op": "add"} (o "remove"):

json
{
  "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 usa el discriminante 12.
  • reaction_id es la clave de idempotencia. Un reaction_id repetido en un plazo de 72 horas se confirma con "duplicate": true y no se vuelve a entregar.
  • El relé responde {"type":"message_reaction_ack","msg_id":"...","reaction_id":"...","delivered":...,"queued":...}.
  • Las reacciones están limitadas a 20 por remitente y conversación dentro de la ventana de frecuencia.

Las reacciones de grupo se describen en Grupos.

Estado de conexión e indicador de escritura

Las tramas de estado de conexión y de escritura son metadatos en texto claro y son visibles para el relé.

Estado de conexión

Suscríbase a los contactos:

json
{ "type": "presence_subscribe", "pubkeys": ["<64 hex>", "<64 hex>"] }

Se consideran hasta 200 claves por trama; las claves mal formadas y duplicadas se ignoran. Las suscripciones se acumulan durante toda la vida de la conexión. Suscríbase únicamente a sus propios contactos.

json
{
  "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
}

El estado de conexión solo es visible entre contactos mutuos, y solo si el destinatario de la suscripción tiene presence_enabled. Las entradas ocultas (reason not_mutual_contact o presence_hidden) no contienen online ni last_seen_ts. last_seen_ts solo está presente cuando last_seen_visible es true; en caso contrario, muestre un estado genérico como «visto recientemente».

Los cambios en vivo llegan como:

json
{ "type": "presence_update", "pubkey": "<a>", "online": true, "presence_visible": true, "last_seen_visible": true, "last_seen_ts": 1780000100 }

Indicador de escritura

json
{ "type": "typing", "receiver_pubkey": "<64 hex>", "is_typing": true }
{ "type": "group_typing", "group_id": "<uuid>", "is_typing": true }

El indicador de escritura solo se reenvía cuando el estado de conexión del remitente sería visible para el destinatario (contactos mutuos y presence_enabled), o a los demás miembros de un grupo al que pertenece el remitente. Nunca se almacena, se encola ni se envía como notificación push.

Perfil y configuración de privacidad

http
GET /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
json
{
  "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"
  }
}

Una identidad sin perfil recibe campos vacíos y los tres indicadores de privacidad en true.

http
PATCH /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
json
{ "display_name": "Alice", "privacy": { "presence_enabled": true, "last_seen_enabled": false, "read_receipts_enabled": false } }
CampoReglas
display_nameHasta 50 caracteres.
bioHasta 200 caracteres.
avatar_urlURL https:// o vacío.
handlea-z y 0-9, de 5 a 24 caracteres. Los nombres de usuario están sujetos a reglas de membresía y a periodos de espera entre cambios.
privacy.*Booleanos. Los tres indicadores también pueden enviarse en el nivel superior.

Los errores incluyen no_valid_fields, <flag>_invalid_boolean, handle_taken y handle_change_cooldown:<date>.

Mantenga el cliente coherente con esta configuración: no envíe confirmaciones de lectura cuando estén desactivadas y no muestre el estado de lectura de un interlocutor mientras sus propias confirmaciones de lectura estén desactivadas.

Verificación de contactos

Un usuario puede exigir que los desconocidos se verifiquen antes de enviarle mensajes. Cuando el destinatario ha activado esta opción y no ha añadido al remitente como contacto, relay_send se rechaza con verification_required.

Para iniciar una conversación, envíe un mensaje con "contact_request": true. Las solicitudes de contacto omiten la comprobación y están limitadas a 3 por remitente y destinatario dentro de una ventana móvil de 24 horas; las solicitudes adicionales se rechazan con contact_request_rate_limited. El indicador contact_request se entrega al destinatario para que la App pueda presentar el mensaje como una solicitud.

La verificación de contactos se aplica a relay_send y a la alternativa HTTPS.

Grupos

Mensajes de grupo

El contenido de grupo se cifra con una clave de grupo compartida de 32 bytes mediante AES-256-GCM:

text
payload_b64 = base64(nonce[12] || AES-256-GCM(group_key, plaintext_json) || tag[16])

El JSON en texto claro contiene text, type (text, media, system o reaction), sender_pubkey, created_at y, opcionalmente, attachments, mentions, reply, forwarded, forwarded_from_name, forwarded_from_pubkey y link_preview.

json
{
  "type": "group_send",
  "msg_id": "<32 hex>",
  "group_id": "<uuid>",
  "payload_b64": "<base64>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>",
  "key_version": 3
}
text
payload_sig = Ed25519(identity_key,
    SHA256(UTF-8(group_id) || sender_pubkey[32] || 0x19 || timestamp as u64 little-endian
           || decoded payload))

El relé comprueba la firma y que el remitente sea un miembro activo, almacena el mensaje para cada uno de los demás miembros activos y después lo entrega en vivo. Las cargas útiles de grupo no usan el sobre 1:1, y los sobres de grupo se entregan con group_id, key_version y payload_sig para que los receptores puedan verificar al remitente antes de descifrar.

json
{
  "type": "group_delivered",
  "msg_id": "...",
  "group_id": "...",
  "accepted": true,
  "accepted_count": 5,
  "delivered_count": 2,
  "queued_count": 5,
  "failed_count": 0,
  "member_count": 6
}

Un remitente que no es miembro recibe error con el motivo not_a_member.

Ediciones, revocaciones y reacciones en grupos

TramaFirma
group_message_edit (group_id, msg_id, target_msg_id, payload_b64, payload_sig, key_version, timestamp)Fórmula de grupo anterior.
group_message_reaction (group_id, msg_id, reaction_id, payload_b64, payload_sig, key_version, timestamp)Fórmula de grupo anterior. La carga útil es una carga útil de grupo con type: "reaction".
group_message_revoke (group_id, sender_pubkey, msg_id, timestamp, payload_sig)Ed25519 sin hash sobre `"aeronyx-group-message-revoke-v1"

Cada una se confirma con la trama _ack correspondiente, que incluye los recuentos de entrega. Los receptores aplican ediciones y revocaciones solo si proceden del autor original.

Claves de grupo

El propietario del grupo distribuye las claves de grupo como paquetes de claves por miembro: base64(nonce[12] || AES-256-GCM(chat_key(owner, member), group_key) || tag[16]). Los endpoints REST bajo /api/relay/groups/ crean grupos, gestionan miembros e invitaciones, suben paquetes de claves, rotan claves (keys/rotate/) y obtienen el paquete actual del solicitante (keys/me/). Los remitentes cifran con la versión de clave más reciente que poseen; los receptores que no disponen de una versión de clave deben obtener keys/me/ y retener el mensaje hasta que llegue la clave.

Archivos adjuntos cifrados

Los archivos adjuntos se cifran en el dispositivo, se suben como texto cifrado opaco y se referencian desde el interior del mensaje cifrado.

Cifrar el archivo

Para cada archivo, genere una clave aleatoria de 32 bytes y un nonce aleatorio de 12 bytes:

text
blob = nonce[12] || AES-256-GCM(file_key, file_bytes) || tag[16]

Suba blob. Coloque file_key y todos los campos descriptivos dentro del mensaje cifrado, nunca en una solicitud de subida.

Objeto de archivo adjunto

ClaveObligatorioSignificado
blob_idsíID devuelto por la subida.
file_keysíBase64 de la clave de archivo de 32 bytes.
media_typesíTipo MIME del archivo en texto claro.
file_namesíNombre para mostrar.
file_sizesíTamaño en texto claro, en bytes.
thumb_b64noMiniatura JPEG en Base64, de hasta 64 KiB.
duration_msnoDuración del audio o del vídeo.
waveformnoHasta 96 números en [0, 1] para mensajes de voz.
sticker, sticker_pack, sticker_posenoIdentidad del sticker.
livenoParte en movimiento de una Live Photo: {blob_id, file_key, media_type, file_size, duration_ms?, width?, height?}.

Los receptores ignoran los archivos adjuntos que carecen de blob_id o file_key. Los mensajes de voz de la App son AAC-LC en un contenedor MP4 (audio/mp4).

Subida: solicitud única (hasta 10 MiB)

http
POST /api/relay/blob/presign/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
json
{ "file_size": 482141, "media_type": "image/jpeg", "media_kind": "image", "ttl_days": 7 }

file_size es el tamaño del texto cifrado. media_kind es uno de voice, image, video, file, avatar, other. ttl_days se limita al rango de 1 a 30 (7 por defecto).

json
{
  "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
}

A continuación:

  1. Envíe con PUT el texto cifrado a upload_url en un plazo de 15 minutos, solo con un encabezado Content-Type. No envíe el encabezado Authorization al almacenamiento.
  2. Llame a POST /api/relay/blob/{blob_id}/complete/ con RelayAuth. El relé confirma el objeto y devuelve {blob_id, file_size, expires_at, storage}.

Subida: multiparte (hasta 100 MiB)

http
POST /api/relay/blob/multipart/create/
json
{ "total_size": 52428800, "media_type": "video/mp4", "media_kind": "video", "ttl_days": 7 }
json
{
  "blob_id": "<uuid>",
  "upload_id": "...",
  "part_size": 8388608,
  "total_parts": 7,
  "part_urls": ["https://...", "..."],
  "storage": "r2",
  "expires_at": "...",
  "max_bytes": 104857600
}

part_size es de 8 MiB por defecto y puede solicitarse entre 5 y 16 MiB. Las URL de las partes son válidas durante 60 minutos. Envíe con PUT cada parte a su URL y registre el encabezado de respuesta ETag; a continuación:

http
POST /api/relay/blob/multipart/complete/
json
{ "blob_id": "<uuid>", "upload_id": "...", "parts": [ { "part_number": 1, "etag": "\"...\"" } ] }

La AeroNyx App usa la subida de solicitud única hasta 8 MiB de texto cifrado y la subida multiparte por encima de ese tamaño.

Descarga

http
GET /api/relay/blob/{blob_id}/

El relé responde 302 con un Location en la red de distribución de contenido y los encabezados X-AeroNyx-Blob-Size, X-AeroNyx-Blob-Media-Type y X-AeroNyx-Blob-Storage. Siga la redirección usted mismo sin reenviar ningún encabezado Authorization y, después, verifique y descifre el blob con su file_key. Limite la descarga al tamaño esperado.

Un blob_id es una capacidad al portador: cualquiera que lo posea puede obtener el texto cifrado, motivo por el cual la clave viaja únicamente dentro del mensaje cifrado. access_mode y la caducidad se aplican en la redirección del relé. No confíe en ellos para la confidencialidad.

Eliminación de archivos adjuntos

http
POST /api/relay/blob/{blob_id}/delete/

Solo quien subió un blob puede eliminarlo. La respuesta es {"blob_id": "...", "deleted": true}.

Errores de archivos adjuntos

Los errores usan {"success": false, "error": "<text>", "error_code": "<code>"}. Base la lógica en error_code.

HTTPerror_codeSignificado
400blob_id_invalidID de blob mal formado.
400blob_missing_file_size, blob_missing_total_size, blob_missing_parts, blob_bad_parts, blob_bad_part_countSolicitud de subida no válida.
400blob_not_r2, blob_multipart_complete_failedLa finalización falló; inicie una nueva subida.
401auth_requiredEl blob requiere RelayAuth.
403blob_not_uploader, download_forbiddenEl solicitante no tiene permiso.
404blob_not_found, blob_not_uploadedBlob desconocido, o finalización antes de que terminara la subida.
410blob_expiredEl blob ha caducado. Pida al remitente que lo vuelva a enviar.
413blob_too_largeSe superó el límite; la respuesta incluye max_bytes y chunked_max_bytes.
503blob_r2_unavailableAlmacenamiento no disponible temporalmente; reintente con retroceso.

Endpoints de subida heredados

POST /api/relay/blob/ (formulario multiparte, hasta 10 MiB) y la API de sesiones reanudables bajo /api/relay/blob/session/ (hasta 100 MiB, fragmentos de 64 KiB a 4 MiB, sesiones válidas durante 24 horas) siguen disponibles como alternativa. Los clientes nuevos deben usar los endpoints anteriores.

Notificaciones push

El relé envía notificaciones de Apple Push Notification service (APNs) para iOS y macOS. Los clientes de Android reciben los mensajes únicamente a través del WebSocket.

Se envía una notificación push para relay_send y group_send cuando el destinatario no tiene una conexión activa en primer plano, el remitente no estableció suppress_push y el destinatario no ha silenciado la conversación. Las ediciones, revocaciones, reacciones y confirmaciones no generan notificaciones push. Las cargas útiles push no contienen texto cifrado:

json
{
  "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 es p2p_message (con sender_pubkey) o group_message (con group_id). Las notificaciones de llamadas usan missed_call y cargas útiles específicas de llamada. Al recibir una notificación push, conéctese y ejecute relay_pull.

EndpointCuerpo
POST /api/relay/push/register/token (64 hex), platform (ios o macos), bundle_id, environment (production o sandbox), token_type opcional (alert o voip) y provider (apns).
POST /api/relay/push/unregister/token, platform, token_type y provider opcionales.
POST /api/relay/push/mute/kind (p2p o group), target (clave pública o ID de grupo), muted (booleano).

Los tres requieren RelayAuth. Registrar un token lo asocia a la identidad que realiza la llamada.

Llamadas

La señalización de llamadas de voz y vídeo viaja por el mismo WebSocket como metadatos en texto claro; los medios fluyen por separado. Las tramas son call_invite, call_answer, call_reject, call_hangup y call_busy para llamadas 1:1, y group_call_invite, group_call_invite_broadcast, group_call_answer y group_call_hangup_broadcast para grupos, además de tramas de admisión para reuniones con anfitrión.

El relé valida room_name con respecto a los participantes: p2p_ seguido de los primeros 16 caracteres hexadecimales de SHA256(lower_key + ":" + higher_key) para llamadas 1:1, y grp_<first 8 characters of group_id>_<first 8 hex of SHA256(group_id)> para grupos. Las señales de llamada para un destinatario sin conexión se retienen durante 120 segundos.

Límites de frecuencia

ÁmbitoLímiteRespuesta
relay_send, group_send y push HTTPS, por identidad50 por ventana corta; 100.000 al díaWebSocket: error rate_limited con retry_after. HTTPS: 429 con Retry-After.
group_send por remitente y grupo10 por ventana cortaerror rate_limited, scope: "sender".
group_send por grupo50 por ventana cortaerror rate_limited, scope: "group".
Reacciones por remitente y conversación20 por ventana cortaerror rate_limited, scope: "reaction".
Solicitudes de contacto por remitente y destinatario3 cada 24 horassend_rejected contact_request_rate_limited.

Espere al menos retry_after segundos antes de reintentar. No reenvíe en un bucle cerrado: los contadores siguen corriendo mientras reintenta.

Lista de comprobación de implementación

  1. Genere y almacene una identidad Ed25519; use claves en hexadecimal en minúscula en todas partes.
  2. Implemente RelayAuth y el inicio de sesión, el latido y presence_state del WebSocket.
  3. Implemente el sobre sellado y verifíquelo con el vector de referencia de Central Chat HTTPS API v1.
  4. Envíe con relay_send, trate durable: true como enviado y vuelva a generar timestamp y payload_sig en los reintentos.
  5. Reciba relay_envelope: elimine duplicados, verifique, descifre y almacene; después envíe message_receipt y relay_offline_ack.
  6. Ejecute relay_pull tras el inicio de sesión y al despertar por una notificación push; confirme solo después del almacenamiento persistente.
  7. Lea los indicadores de privacidad del perfil y respételos para el estado de conexión y las confirmaciones de lectura.
  8. Cifre los archivos adjuntos en el dispositivo y súbalos mediante presign o multiparte; mantenga file_key dentro del mensaje cifrado.
  9. Verifique la autoría antes de aplicar ediciones y revocaciones.
  10. Mantenga la búsqueda y el historial de mensajes en el dispositivo. El relé no ofrece búsqueda de contenido y no es un archivo.
<!-- faq:start -->

Preguntas frecuentes

¿Puede AeroNyx Chat Relay leer mis mensajes?

No. Los mensajes, las ediciones, las reacciones, los mensajes de grupo y los archivos adjuntos se cifran y firman en el dispositivo del remitente antes de llegar al relé, y las claves nunca salen de los dispositivos de las personas que participan en la conversación. El relé solo almacena y reenvía texto cifrado.

¿Qué puede ver AeroNyx Chat Relay?

El relé ve metadatos de entrega: las claves públicas del remitente y del destinatario, los ID de grupo y de mensaje, las marcas de tiempo, los tamaños de las cargas útiles, el estado de entrega y de lectura, las señales de estado de conexión y de escritura, los metadatos de señalización de llamadas, los tamaños de los archivos adjuntos y las direcciones IP de conexión. No ve el contenido de los mensajes, el contenido de los archivos adjuntos ni las claves. La lista completa figura en el modelo de confianza, al principio de esta página.

¿Qué cifrado usa el chat de AeroNyx?

Cada identidad es un par de claves Ed25519. Dos personas derivan una clave de chat compartida con X25519 y HKDF-SHA256. Los mensajes 1:1 se cifran con XChaCha20-Poly1305 y se firman con Ed25519. Los mensajes de grupo y los archivos adjuntos se cifran con AES-256-GCM. Los formatos exactos de bytes y un vector de prueba se publican en la documentación de Central Chat HTTPS API v1.

¿Cómo se protegen las fotos, los vídeos y los archivos?

Cada archivo se cifra en el dispositivo con su propia clave AES-256-GCM aleatoria antes de subirse. El almacenamiento solo recibe texto cifrado. La clave del archivo viaja dentro del mensaje cifrado de extremo a extremo, por lo que solo los destinatarios pueden descifrar el archivo.

¿Qué ocurre si el destinatario no está conectado?

El relé conserva los mensajes cifrados en la cola sin conexión del destinatario durante un máximo de 72 horas y los entrega cuando el destinatario vuelve a conectarse. En iOS y macOS, el destinatario también recibe una notificación push que no contiene el contenido del mensaje.

¿Conserva AeroNyx mi historial de chat?

No. El relé es un búfer de entrega: los elementos se eliminan después de que el dispositivo del destinatario confirma que los ha almacenado. El historial de chat y la búsqueda residen en sus dispositivos.

¿Puedo crear mi propio cliente o bot de AeroNyx?

Sí. Cualquier software que disponga de una identidad Ed25519 e implemente los formatos de esta página puede intercambiar mensajes con usuarios de la AeroNyx App. Para una integración más sencilla de solicitud/respuesta sin WebSocket, use Central Chat HTTPS API v1.

¿Es descentralizado AeroNyx Chat Relay?

Chat Relay es el servicio de entrega centralizado de AeroNyx. AeroNyx también opera una red de nodos de código abierto (AGPL-3.0) que puede transportar el texto cifrado del chat por una ruta de dos saltos con diversidad de red. Ambas rutas coexisten: el relé ofrece una entrega rápida y fiable y colas sin conexión, y la ruta de nodos es una ruta opcional que reduce lo que puede observar cualquier operador individual.

¿Por qué se rechaza mi mensaje con verification_required?

El destinatario solo acepta mensajes de sus contactos. Envíe un único primer mensaje con contact_request: true, que el destinatario verá como una solicitud de contacto. Se permiten hasta tres solicitudes de contacto por destinatario en cualquier período de 24 horas.

<!-- faq:end --> <!-- verified-two-hop-delivery-v1:start -->

Entrega verificada en dos saltos

Para el tráfico ChatRelay autenticado que cumpla los requisitos, el origen puede elegir una ruta de dos saltos con diversidad de red y contabilizar la entrega solo después de validar la confirmación firmada del nodo terminal esperado. Los nodos de retransmisión enrutan texto cifrado y no analizan la carga útil E2E. Consulte el modelo de evidencia completo en Descubrimiento de nodos y entrega cifrada verificada a través de relés.

<!-- verified-two-hop-delivery-v1:end -->