Intégration client du Chat Relay AeroNyx
Référence d'intégration du Chat Relay AeroNyx : authentification WebSocket, messages 1:1 et de groupe scellés, remise hors ligne, accusés de remise et de lecture, réactions, statut de présence, pièces jointes chiffrées, notifications push et limitation de débit.
Préparer une consigne d’intégration
Choisissez la technologie et la tâche, puis copiez la consigne dans votre assistant de programmation. Elle est générée localement, sans envoi à un service d’IA.
Consigne générée
Ce document est la référence d'intégration du Chat Relay AeroNyx : le service WebSocket et HTTPS exposé sur api.aeronyx.network, qui achemine entre identités AeroNyx les messages 1:1 chiffrés de bout en bout, les messages de groupe, les accusés, les réactions, le statut de présence et les pièces jointes chiffrées.
Il s'adresse aux ingénieurs qui développent des clients, bots et services compatibles AeroNyx, ainsi qu'aux agents de programmation IA chargés de les implémenter. Chaque trame, champ et limite décrits sur cette page correspondent au relais de production et à l'AeroNyx App en date d'octobre 2026.
Le Chat Relay constitue le chemin de remise centralisé. Il coexiste avec le chemin décentralisé via les nœuds (routage en oignon et boîtes aux lettres anonymes, voir Remise vérifiée à deux sauts) ; un client peut utiliser les deux. Pour une intégration HTTPS de type requête/réponse sans WebSocket, consultez API HTTPS Central Chat v1.
Modèle de confiance
Le relais n'a aucune visibilité sur le contenu. Les clients chiffrent et signent tout avant que les données n'atteignent le relais, et celui-ci se contente d'acheminer, de mettre en file d'attente et de soumettre à une limitation de débit un texte chiffré opaque.
Le relais ne reçoit jamais :
- le texte des messages, les emojis de réaction, les modifications ou les charges utiles de groupe en clair
- les clés de discussion, les clés de groupe, les clés de pièces jointes ou les nonces
- le contenu des pièces jointes, les noms de fichiers, les miniatures, les formes d'onde ou les transcriptions
- les clés privées d'identité
Le relais observe en revanche les métadonnées de remise, que les intégrateurs doivent considérer comme visibles par l'opérateur :
- les clés publiques de l'expéditeur et du destinataire, les identifiants de groupe et les identifiants de message
- les horodatages, la taille des charges utiles ainsi que l'état de remise, d'accusé et de lecture
- le statut de présence, l'état de premier plan et les indicateurs de saisie (envoyés sous forme de trames en clair)
- les métadonnées de signalisation des appels (nom de la salle, identifiant d'appel, indicateur vidéo)
- la taille du texte chiffré des pièces jointes, le type de média déclaré et la date d'expiration
- l'indicateur
contact_requestet les métadonnées de connexion au niveau IP
La confidentialité du contenu repose sur le chiffrement de bout en bout, et non sur le contrôle d'accès au relais. Concevez votre intégration en conséquence : un attaquant qui obtient un texte chiffré stocké doit rester incapable de le lire.
Identités et clés
Une identité de discussion AeroNyx est une paire de clés Ed25519. La clé publique de 32 octets, écrite sous forme de 64 caractères hexadécimaux en minuscules, constitue l'adresse. Générez les clés sur l'appareil et n'envoyez jamais la clé privée où que ce soit.
Deux clés sont dérivées d'une paire d'identité :
- Clé de discussion (1:1).
HKDF-SHA256(salt = empty, ikm = X25519(my_secret, peer_public), info = "AERONYX-P2P-KEY", length = 32), où les deux clés Ed25519 sont converties en X25519 (SHA-512(seed)[0..32]avec clamping pour la clé secrète, conversion d'Edwards vers Montgomery pour la clé publique). Les deux pairs dérivent la même clé. - Signatures des messages. Ed25519 avec la clé d'identité, sur les chaînes d'octets exactes définies sur cette page.
Utilisez toujours l'hexadécimal en minuscules pour les clés publiques dans les trames. Le relais ne normalise pas la casse dans toutes les clés de file d'attente.
Authentification
Signature RelayAuth
La connexion WebSocket et chaque point de terminaison HTTPS authentifié utilisent la même signature :
digest = SHA256("AeroNyx-RelayAuth-v1" || pubkey[32] || timestamp as u64 little-endian)
signature = Ed25519(identity_key, digest)
"AeroNyx-RelayAuth-v1" correspond aux 20 octets ASCII, sans terminateur. timestamp est exprimé en secondes Unix et doit se situer à moins de 300 secondes de l'heure du serveur.
La signature ne lie que l'identité et l'heure. Elle ne lie ni la méthode, ni le chemin, ni le corps, ni la connexion, et aucun nonce n'est utilisé. Générez un horodatage neuf pour chaque requête, transmettez-le uniquement via TLS et ne le journalisez jamais.
En-tête HTTPS
Authorization: Relay <pubkey-hex>:<timestamp>:<signature-hex>
Un échec de vérification renvoie HTTP 401 avec {"success": false, "error": "<reason>"}. Les motifs possibles sont notamment missing_auth_header, malformed_auth_header, invalid_timestamp, timestamp_expired, invalid_pubkey et invalid_signature.
Connexion WebSocket
Point de terminaison
wss://api.aeronyx.network/ws/relay/
Les clients natifs se connectent sans en-tête Origin. Les navigateurs doivent se connecter depuis une origine autorisée ; toute autre origine, ou un Host inconnu, entraîne la fermeture avec le code 1008.
Ouverture de session
Le serveur accepte le socket, puis attend une trame auth dans un délai de 30 secondes :
{
"type": "auth",
"pubkey": "<64 hex>",
"timestamp": 1780000000,
"signature": "<128 hex>"
}
En cas de succès :
{ "type": "auth_ack", "session_id": "<uuid>", "server_ts": 1780000001 }
Utilisez server_ts pour estimer le décalage d'horloge ; les horodatages des messages sont vérifiés selon la même fenêtre de ±300 secondes.
Après auth_ack, le serveur démarre son battement de cœur, abonne la connexion à ses canaux et rejoue immédiatement la file hors ligne (voir Remise hors ligne).
En cas d'échec, le serveur envoie {"type": "auth_error", "reason": "<reason>"} et ferme le socket avec le code 4001. Motifs : missing_fields, timestamp_expired, invalid_pubkey, invalid_signature_length, invalid_signature_encoding, invalid_signature, internal_error. L'expiration du délai d'ouverture de session envoie le motif timeout et ferme avec le code 4002.
Toute autre trame reçue avant l'ouverture de session reçoit en réponse auth_error avec le motif authentication_required ; le socket reste ouvert.
Battement de cœur et état de premier plan
| Sens | Trame | Comportement |
|---|---|---|
| serveur → client | {"type":"ping"} toutes les 30 s | Répondre {"type":"pong"}. |
| client → serveur | {"type":"ping"} | Le serveur répond {"type":"pong"}. |
| client → serveur | {"type":"presence_state","foreground":true} | Marque cette connexion comme active. |
| client → serveur | {"type":"presence_state","foreground":false} | La marque comme étant en arrière-plan, ce qui permet au relais d'envoyer une notification push pour les nouveaux messages. |
Le relais considère une identité comme en ligne uniquement tant que sa connexion envoie ping, pong ou presence_state avec foreground: true au moins toutes les 90 secondes. L'AeroNyx App envoie un ping toutes les 15 secondes lorsqu'elle est au premier plan et considère la connexion comme morte au-delà de trois pongs manqués.
Les connexions de longue durée doivent se reconnecter au moins une fois toutes les 24 heures. Aucun message n'est perdu lorsqu'une connexion cesse silencieusement de recevoir les trames en direct, car chaque message est également placé en file d'attente et rejoué à l'ouverture de session ; la remise en direct ne reprend toutefois qu'après la reconnexion.
Règles relatives aux trames
- Trames texte uniquement, un objet JSON par trame. Les trames binaires sont ignorées.
- La taille maximale d'une trame est de 1 048 576 caractères. Les trames plus volumineuses sont refusées avec
{"type":"error","reason":"message_too_large"}. Maintenezpayload_b64en dessous d'environ 800 Kio afin de laisser de la place au reste de la trame. - Les trames mal formées renvoient
erroravec le motifinvalid_json,invalid_json_typeouunknown_type.
Trame d'erreur générique :
{ "type": "error", "reason": "<reason>", "retry_after": 0 }
Les erreurs de limitation de débit comportent également scope.
Codes de fermeture
| Code | Signification |
|---|---|
1008 | Host ou Origin non autorisé. |
4000 | Le serveur n'a pas pu transmettre son battement de cœur. |
4001 | Échec de l'ouverture de session. |
4002 | Délai d'ouverture de session dépassé. |
Reconnectez-vous avec un backoff exponentiel et une gigue aléatoire. L'AeroNyx App attend 2^(attempt-1) secondes, borné entre 1 et 60 secondes, multiplié par un facteur aléatoire compris entre 0,8 et 1,2.
Envoi d'un message 1:1
1. Construire l'enveloppe scellée
La charge utile d'un message 1:1 est un ChatEnvelope signé et chiffré. Sa construction exacte, une implémentation de référence en Python et un vecteur de test de référence figurent dans API HTTPS Central Chat v1 : format de l'enveloppe scellée. La même enveloppe est utilisée par les deux API.
En résumé : XChaCha20-Poly1305 sous la clé de discussion, une signature Ed25519 sur une transcription de 121 octets et une disposition binaire fixe. content_type vaut 0 pour tous les messages, y compris ceux qui comportent des pièces jointes.
Le texte en clair est le texte UTF-8 pour un message simple, ou un objet JSON pour les messages comportant des pièces jointes, des réponses, des transferts ou des aperçus de liens :
{
"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": "..." }
}
Tous les champs autres que type et text sont facultatifs. Les destinataires doivent afficher sous forme de texte brut tout texte en clair qui n'est pas un objet JSON avec "type": "aeronyx_message". L'objet pièce jointe est défini dans Pièces jointes chiffrées.
2. Signer la trame
Chaque trame de message porte une seconde signature, payload_sig, que le relais vérifie avant de l'accepter :
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 correspond au payload_b64 décodé. Utilisez le discriminant 11 pour les messages et les modifications, et 12 pour les réactions. payload_sig est encodé en hexadécimal.
3. Envoyer
{
"type": "relay_send",
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"receiver_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>"
}
| Champ | Règles |
|---|---|
msg_id | 32 caractères hexadécimaux en minuscules : le message_id de 16 octets de l'enveloppe. L'App destinataire stocke le message sous cet identifiant, qui doit donc correspondre à l'enveloppe. |
receiver_pubkey | La clé publique du destinataire. |
discriminant | 11. |
timestamp | Secondes Unix, à ±300 s de l'heure du serveur. |
suppress_push | Facultatif. true n'envoie aucune notification push. |
contact_request | Facultatif. Signale un premier message adressé à une personne qui exige une vérification des contacts (voir Vérification des contacts). |
4. Accusé de réception
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
durable(etqueued) vauttruedès que le message est stocké dans la file hors ligne du destinataire. Considérezdurable: truecomme « envoyé ».deliveredsignifie que le destinataire disposait d'une connexion active au premier plan. Ce n'est pas une preuve de réception ; utilisez pour cela les accusés de remise.
Attendez l'accusé de réception jusqu'à 15 secondes avant de considérer la tentative comme ayant échoué.
Rejets et erreurs
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
| Réponse | Motif | Action |
|---|---|---|
send_rejected | verification_required | Le destinataire n'accepte que les messages de ses contacts. Ne pas réessayer. |
send_rejected | contact_request_rate_limited | Limite de demandes de contact atteinte pour ce destinataire. Ne pas réessayer. |
error | missing_fields | Un champ obligatoire est absent ou vide. |
error | invalid_timestamp, timestamp_expired | Corriger l'horloge et horodater à nouveau. |
error | invalid_payload_sig | La vérification de payload_sig échoue. |
error | rate_limited | Réessayer après retry_after secondes. |
Nouvelles tentatives
Renvoyez un message non acquitté avec le même msg_id et les mêmes octets d'enveloppe. Comme le relais rejette les trames de plus de 300 secondes, calculez un nouveau timestamp de trame et un nouveau payload_sig pour chaque nouvelle tentative ; l'enveloppe conserve son horodatage d'origine. Le relais et les destinataires dédoublonnent par msg_id.
Repli HTTPS
Lorsque le WebSocket est indisponible, le même message peut être publié via 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>"
}
Un succès renvoie HTTP 200 avec {"success": true}. Les échecs de vérification des contacts renvoient 400 avec verification_required ou contact_request_rate_limited, et la limitation de débit renvoie 429 avec Retry-After. Les messages envoyés de cette manière sont placés en file d'attente pour le destinataire mais ne déclenchent pas de notification push ; renvoyez-les donc via le WebSocket dès qu'il est reconnecté.
Réception des messages
Les messages entrants arrivent sous la forme relay_envelope :
{
"type": "relay_envelope",
"sender_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"from_offline": false
}
Pour discriminant: 11 :
- Dédoublonnez par
msg_id. Un même message peut arriver en direct puis à nouveau depuis la file hors ligne. - Analysez l'enveloppe et vérifiez que son
receiver_pubkeycorrespond à votre identité. - Pour les trames en direct (
from_offline: false), rejetez le message si l'horodatage de l'enveloppe s'écarte de plus de 300 secondes de votre horloge. - Vérifiez la signature de l'enveloppe à l'aide du
sender_pubkeyde l'enveloppe. C'est cette clé, et non lesender_pubkeyde la trame, qui identifie l'expéditeur authentifié. - Déchiffrez avec la clé de discussion dérivée de cet expéditeur. Les très anciennes versions de l'App chiffraient avec la sortie X25519 brute ; essayez-la si la clé HKDF échoue.
- Stockez le message de manière durable, puis envoyez un accusé de remise et, pour
from_offline: true, un acquittement hors ligne.
Les enveloppes 1:1 sont remises sans payload_sig ; la signature de l'enveloppe sert de contrôle d'authenticité. Les trames peuvent également comporter contact_request: true.
Si un message ne vous est pas adressé, échoue à la vérification ou au déchiffrement, rejetez-le sans rien afficher.
Remise hors ligne
Chaque message, modification, révocation, réaction, accusé de remise et accusé de lecture est écrit dans la file hors ligne du destinataire avant la remise en direct. La file est rejouée automatiquement après chaque ouverture de session, ainsi que sur demande :
{ "type": "relay_pull" }
Le relais rejoue tous les éléments en file d'attente sous leurs types de trame habituels avec from_offline: true, triés par horodatage, puis envoie :
{ "type": "relay_pull_done", "count": 12, "has_more": false }
La remise est de type « au moins une fois ». Les éléments restent dans la file jusqu'à leur acquittement :
{ "type": "relay_offline_ack", "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834" }
Acquittez les réactions avec "reaction_id" au lieu de "msg_id". N'acquittez un élément qu'après l'avoir stocké de manière durable sur l'appareil. Le relais répond par {"type":"relay_offline_ack","msg_id":"...","success":true}.
Limites de la file :
| Limite | Valeur |
|---|---|
| Éléments par destinataire | 1 000. Lorsque la file est pleine, les nouveaux éléments sont rejetés et l'expéditeur reçoit durable: false. |
| Rétention | 72 heures après l'ajout de l'élément le plus récent. |
| Taille de la charge utile | 1 Mio après décodage, dans la limite de trame de 1 Mio. |
Le relais est un tampon de remise, et non un historique des messages. Conservez l'historique sur l'appareil.
Accusés de remise et de lecture
Accusé de remise
À envoyer après avoir stocké un message :
{ "type": "message_receipt", "receiver_pubkey": "<original sender>", "msg_id": "...", "timestamp": 1780000010 }
Le relais répond message_receipt_ack et remet {"type":"message_receipt","sender_pubkey":"...","msg_id":"...","timestamp":...} à l'expéditeur d'origine.
Accusé de lecture
{ "type": "message_read", "receiver_pubkey": "<original sender>", "msg_id": "<latest read message>", "timestamp": 1780000200, "enabled": true }
Les accusés de lecture fonctionnent comme des filigranes : l'App marque comme lus le message indiqué ainsi que tous les messages sortants antérieurs de la conversation. N'en envoyez un que si l'utilisateur a activé les accusés de lecture.
Le relais applique une règle de réciprocité à l'envoi, lors de la remise en direct et lors du rejeu. Un accusé de lecture n'est remis que si les deux utilisateurs sont des contacts mutuels et ont tous deux read_receipts_enabled. Dans le cas contraire, l'expéditeur reçoit :
{ "type": "message_read_ack", "msg_id": "...", "delivered": false, "suppressed": true, "reason": "receiver_read_receipts_disabled" }
| Motif | Signification |
|---|---|
client_disabled | La trame comportait enabled: false ou read_receipts_enabled: false. |
not_mutual_contact | Les utilisateurs ne sont pas des contacts mutuels. |
reader_read_receipts_disabled | Le lecteur a désactivé les accusés de lecture. |
receiver_read_receipts_disabled | L'expéditeur d'origine a désactivé les accusés de lecture. |
invalid_pubkey | Une clé publique est mal formée. |
Un accusé de lecture remis est acquitté par {"type":"message_read_ack","msg_id":"...","delivered":true}.
Modifications et révocations
Modification
Une modification est une nouvelle enveloppe scellée (dotée de son propre message_id aléatoire) contenant l'intégralité du contenu de remplacement, envoyée en référence à l'identifiant du message d'origine :
{
"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 utilise la formule décrite dans Signer la trame avec le discriminant 11. Une modification qui supprime toutes les pièces jointes définit "attachments_edited": true dans son JSON en clair. Le relais répond message_edit_ack. Les destinataires vérifient et déchiffrent l'enveloppe comme un message et ne doivent appliquer une modification que si son expéditeur est l'auteur du message d'origine.
Révocation
{
"type": "message_revoke",
"receiver_pubkey": "<64 hex>",
"sender_pubkey": "<64 hex>",
"msg_id": "<original msg_id>",
"timestamp": 1780000500,
"payload_sig": "<128 hex>"
}
La signature de révocation porte sur les octets bruts, sans hachage :
payload_sig = Ed25519(identity_key,
"aeronyx-message-revoke-v1" || sender_pubkey[32] || receiver_pubkey[32]
|| UTF-8(msg_id) || timestamp as u64 little-endian)
Le relais répond message_revoke_ack. Le relais ne vérifie pas l'auteur : les destinataires doivent vérifier la signature et n'appliquer une révocation que si son expéditeur est l'auteur du message d'origine. Pour supprimer les pièces jointes d'un message révoqué, appelez POST /api/relay/blob/{blob_id}/delete/.
Réactions
Une réaction 1:1 est une enveloppe scellée dont le message_id est l'identifiant de la réaction, avec content_type 2 et le texte en clair {"emoji": "❤️", "op": "add"} (ou "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_sigutilise le discriminant12.reaction_idest la clé d'idempotence. Unreaction_idrépété dans un délai de 72 heures est acquitté avec"duplicate": trueet n'est pas remis une seconde fois.- Le relais répond
{"type":"message_reaction_ack","msg_id":"...","reaction_id":"...","delivered":...,"queued":...}. - Les réactions sont limitées à 20 par expéditeur et par conversation au sein de la fenêtre de limitation.
Les réactions de groupe sont décrites dans la section Groupes.
Présence et saisie en cours
Les trames de présence et de saisie sont des métadonnées en clair, visibles par le relais.
Présence
S'abonner aux contacts :
{ "type": "presence_subscribe", "pubkeys": ["<64 hex>", "<64 hex>"] }
Jusqu'à 200 clés sont prises en compte par trame ; les clés mal formées et en double sont ignorées. Les abonnements se cumulent pendant toute la durée de vie de la connexion. Abonnez-vous uniquement à vos propres contacts.
{
"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
}
Le statut de présence n'est visible qu'entre contacts mutuels, et uniquement si la cible a activé presence_enabled. Les entrées masquées (reason not_mutual_contact ou presence_hidden) ne contiennent ni online ni last_seen_ts. last_seen_ts n'est présent que lorsque last_seen_visible vaut true ; dans le cas contraire, affichez un état générique tel que « vu récemment ».
Les changements en direct arrivent sous la forme :
{ "type": "presence_update", "pubkey": "<a>", "online": true, "presence_visible": true, "last_seen_visible": true, "last_seen_ts": 1780000100 }
Saisie en cours
{ "type": "typing", "receiver_pubkey": "<64 hex>", "is_typing": true }
{ "type": "group_typing", "group_id": "<uuid>", "is_typing": true }
L'indicateur de saisie n'est transmis que lorsque le statut de présence de l'expéditeur serait visible par le destinataire (contacts mutuels et presence_enabled), ou aux autres membres d'un groupe auquel appartient l'expéditeur. Il n'est jamais stocké, mis en file d'attente ni envoyé par notification push.
Profil et paramètres de confidentialité
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"
}
}
Une identité sans profil reçoit des champs vides et les trois indicateurs de confidentialité à 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 } }
| Champ | Règles |
|---|---|
display_name | Jusqu'à 50 caractères. |
bio | Jusqu'à 200 caractères. |
avatar_url | URL https:// ou vide. |
handle | a-z et 0-9, de 5 à 24 caractères. Les identifiants sont soumis aux règles d'abonnement et à des délais de modification. |
privacy.* | Booléens. Les trois indicateurs peuvent également être envoyés au niveau racine. |
Les erreurs possibles sont notamment no_valid_fields, <flag>_invalid_boolean, handle_taken et handle_change_cooldown:<date>.
Maintenez la cohérence du client avec ces paramètres : n'envoyez pas d'accusés de lecture lorsqu'ils sont désactivés, et n'affichez pas l'état de lecture d'un pair tant que vos propres accusés de lecture sont désactivés.
Vérification des contacts
Un utilisateur peut exiger que les inconnus se fassent vérifier avant de lui écrire. Lorsque le destinataire a activé cette option et n'a pas ajouté l'expéditeur à ses contacts, relay_send est rejeté avec verification_required.
Pour entamer une conversation, envoyez un message avec "contact_request": true. Les demandes de contact contournent ce contrôle et sont limitées à 3 par expéditeur et par destinataire sur une fenêtre glissante de 24 heures ; les demandes supplémentaires sont rejetées avec contact_request_rate_limited. L'indicateur contact_request est remis au destinataire afin que l'App puisse présenter le message comme une demande.
La vérification des contacts s'applique à relay_send et au repli HTTPS.
Groupes
Messages de groupe
Le contenu des groupes est chiffré avec une clé de groupe partagée de 32 octets au moyen d'AES-256-GCM :
payload_b64 = base64(nonce[12] || AES-256-GCM(group_key, plaintext_json) || tag[16])
Le JSON en clair contient text, type (text, media, system ou reaction), sender_pubkey, created_at et, de manière facultative, attachments, mentions, reply, forwarded, forwarded_from_name, forwarded_from_pubkey et 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))
Le relais vérifie la signature et l'appartenance active de l'expéditeur au groupe, stocke le message pour chacun des autres membres actifs, puis le remet en direct. Les charges utiles de groupe n'utilisent pas l'enveloppe 1:1, et les enveloppes de groupe sont remises avec group_id, key_version et payload_sig afin que les destinataires puissent vérifier l'expéditeur avant de déchiffrer.
{
"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 expéditeur qui n'est pas membre reçoit error avec le motif not_a_member.
Modifications, révocations et réactions de groupe
| Trame | Signature |
|---|---|
group_message_edit (group_id, msg_id, target_msg_id, payload_b64, payload_sig, key_version, timestamp) | Formule de groupe ci-dessus. |
group_message_reaction (group_id, msg_id, reaction_id, payload_b64, payload_sig, key_version, timestamp) | Formule de groupe ci-dessus. La charge utile est une charge utile de groupe avec type: "reaction". |
group_message_revoke (group_id, sender_pubkey, msg_id, timestamp, payload_sig) | Ed25519 brut sur `"aeronyx-group-message-revoke-v1" |
Chacune est acquittée par la trame _ack correspondante, qui porte les compteurs de remise. Les destinataires n'appliquent les modifications et les révocations que si elles proviennent de l'auteur d'origine.
Clés de groupe
Les clés de groupe sont distribuées par le propriétaire du groupe sous forme de lots de clés par membre : base64(nonce[12] || AES-256-GCM(chat_key(owner, member), group_key) || tag[16]). Les points de terminaison REST sous /api/relay/groups/ permettent de créer des groupes, de gérer les membres et les invitations, de téléverser les lots de clés, d'effectuer la rotation des clés (keys/rotate/) et de récupérer le lot actuel de l'appelant (keys/me/). Les expéditeurs chiffrent avec la version de clé la plus récente dont ils disposent ; les destinataires à qui il manque une version de clé doivent interroger keys/me/ et conserver le message jusqu'à l'arrivée de la clé.
Pièces jointes chiffrées
Les pièces jointes sont chiffrées sur l'appareil, téléversées sous forme de texte chiffré opaque et référencées depuis l'intérieur du message chiffré.
Chiffrer le fichier
Pour chaque fichier, générez une clé aléatoire de 32 octets et un nonce aléatoire de 12 octets :
blob = nonce[12] || AES-256-GCM(file_key, file_bytes) || tag[16]
Téléversez blob. Placez file_key et tous les champs descriptifs dans le message chiffré, jamais dans une requête de téléversement.
Objet pièce jointe
| Clé | Obligatoire | Signification |
|---|---|---|
blob_id | oui | Identifiant renvoyé par le téléversement. |
file_key | oui | Encodage Base64 de la clé de fichier de 32 octets. |
media_type | oui | Type MIME du fichier en clair. |
file_name | oui | Nom d'affichage. |
file_size | oui | Taille en clair, en octets. |
thumb_b64 | non | Miniature JPEG encodée en Base64, jusqu'à 64 Kio. |
duration_ms | non | Durée de l'audio ou de la vidéo. |
waveform | non | Jusqu'à 96 nombres dans [0, 1] pour les messages vocaux. |
sticker, sticker_pack, sticker_pose | non | Identité de l'autocollant. |
live | non | Partie animée d'une Live Photo : {blob_id, file_key, media_type, file_size, duration_ms?, width?, height?}. |
Les destinataires ignorent les pièces jointes dépourvues de blob_id ou de file_key. Les messages vocaux de l'App sont encodés en AAC-LC dans un conteneur MP4 (audio/mp4).
Téléversement : requête unique (jusqu'à 10 Mio)
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 correspond à la taille du texte chiffré. media_kind prend l'une des valeurs voice, image, video, file, avatar, other. ttl_days est borné entre 1 et 30 (7 par défaut).
{
"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
}
Ensuite :
- Envoyez le texte chiffré par
PUTversupload_urldans un délai de 15 minutes, avec uniquement un en-têteContent-Type. N'envoyez pas l'en-têteAuthorizationau stockage. - Appelez
POST /api/relay/blob/{blob_id}/complete/avec RelayAuth. Le relais confirme l'objet et renvoie{blob_id, file_size, expires_at, storage}.
Téléversement : multipart (jusqu'à 100 Mio)
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 vaut 8 Mio par défaut et peut être demandé entre 5 et 16 Mio. Les URL des parties sont valables 60 minutes. Envoyez chaque partie par PUT vers son URL et relevez l'en-tête de réponse ETag, puis :
POST /api/relay/blob/multipart/complete/
{ "blob_id": "<uuid>", "upload_id": "...", "parts": [ { "part_number": 1, "etag": "\"...\"" } ] }
L'AeroNyx App utilise le téléversement en requête unique jusqu'à 8 Mio de texte chiffré, et le téléversement multipart au-delà.
Téléchargement
GET /api/relay/blob/{blob_id}/
Le relais répond 302 avec un Location pointant vers le réseau de diffusion de contenu, ainsi que les en-têtes X-AeroNyx-Blob-Size, X-AeroNyx-Blob-Media-Type et X-AeroNyx-Blob-Storage. Suivez vous-même la redirection sans transmettre d'en-tête Authorization, puis vérifiez et déchiffrez le blob avec son file_key. Plafonnez le téléchargement à la taille attendue.
Un blob_id est une capacité au porteur : quiconque le détient peut récupérer le texte chiffré, c'est pourquoi la clé ne circule qu'à l'intérieur du message chiffré. access_mode et la date d'expiration sont appliqués lors de la redirection du relais. Ne vous en remettez pas à eux pour garantir la confidentialité.
Suppression des pièces jointes
POST /api/relay/blob/{blob_id}/delete/
Seul l'auteur du téléversement peut supprimer un blob. La réponse est {"blob_id": "...", "deleted": true}.
Erreurs liées aux pièces jointes
Les erreurs utilisent le format {"success": false, "error": "<text>", "error_code": "<code>"}. Basez votre logique sur error_code.
| HTTP | error_code | Signification |
|---|---|---|
| 400 | blob_id_invalid | Identifiant de blob mal formé. |
| 400 | blob_missing_file_size, blob_missing_total_size, blob_missing_parts, blob_bad_parts, blob_bad_part_count | Requête de téléversement invalide. |
| 400 | blob_not_r2, blob_multipart_complete_failed | La finalisation a échoué ; lancez un nouveau téléversement. |
| 401 | auth_required | Le blob exige RelayAuth. |
| 403 | blob_not_uploader, download_forbidden | L'appelant n'est pas autorisé. |
| 404 | blob_not_found, blob_not_uploaded | Blob inconnu, ou finalisation demandée avant la fin du téléversement. |
| 410 | blob_expired | Le blob a expiré. Demandez à l'expéditeur de le renvoyer. |
| 413 | blob_too_large | Limite dépassée ; la réponse inclut max_bytes et chunked_max_bytes. |
| 503 | blob_r2_unavailable | Stockage temporairement indisponible ; réessayez avec backoff. |
Anciens points de terminaison de téléversement
POST /api/relay/blob/ (formulaire multipart, jusqu'à 10 Mio) et l'API de session reprenable sous /api/relay/blob/session/ (jusqu'à 100 Mio, segments de 64 Kio à 4 Mio, sessions valables 24 heures) restent disponibles comme solution de repli. Les nouveaux clients doivent utiliser les points de terminaison décrits ci-dessus.
Notifications push
Le relais envoie des notifications via Apple Push Notification service (APNs) pour iOS et macOS. Les clients Android reçoivent les messages uniquement via le WebSocket.
Une notification push est envoyée pour relay_send et group_send lorsque le destinataire n'a aucune connexion active au premier plan, que l'expéditeur n'a pas défini suppress_push et que le destinataire n'a pas mis la conversation en sourdine. Les modifications, révocations, réactions et accusés ne déclenchent pas de notification push. Les charges utiles push ne contiennent aucun texte chiffré :
{
"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 vaut p2p_message (avec sender_pubkey) ou group_message (avec group_id). Les notifications d'appel utilisent missed_call et des charges utiles d'appel dédiées. À la réception d'une notification push, connectez-vous et exécutez relay_pull.
| Point de terminaison | Corps |
|---|---|
POST /api/relay/push/register/ | token (64 hex), platform (ios ou macos), bundle_id, environment (production ou sandbox), et de manière facultative token_type (alert ou voip) et provider (apns). |
POST /api/relay/push/unregister/ | token, et de manière facultative platform, token_type, provider. |
POST /api/relay/push/mute/ | kind (p2p ou group), target (clé publique ou identifiant de groupe), muted (booléen). |
Les trois exigent RelayAuth. L'enregistrement d'un jeton le rattache à l'identité appelante.
Appels
La signalisation des appels vocaux et vidéo transite par le même WebSocket sous forme de métadonnées en clair ; les flux média circulent séparément. Les trames sont call_invite, call_answer, call_reject, call_hangup et call_busy pour les appels 1:1, et group_call_invite, group_call_invite_broadcast, group_call_answer et group_call_hangup_broadcast pour les groupes, complétées par des trames d'admission pour les réunions animées par un hôte.
Le relais valide room_name par rapport aux participants : p2p_ suivi des 16 premiers caractères hexadécimaux de SHA256(lower_key + ":" + higher_key) pour les appels 1:1, et grp_<first 8 characters of group_id>_<first 8 hex of SHA256(group_id)> pour les groupes. Les signaux d'appel destinés à un destinataire hors ligne sont conservés pendant 120 secondes.
Limitation de débit
| Portée | Limite | Réponse |
|---|---|---|
relay_send, group_send et push HTTPS, par identité | 50 par fenêtre courte ; 100 000 par jour | WebSocket : error rate_limited avec retry_after. HTTPS : 429 avec Retry-After. |
group_send par expéditeur et par groupe | 10 par fenêtre courte | error rate_limited, scope: "sender". |
group_send par groupe | 50 par fenêtre courte | error rate_limited, scope: "group". |
| Réactions par expéditeur et par conversation | 20 par fenêtre courte | error rate_limited, scope: "reaction". |
| Demandes de contact par expéditeur et par destinataire | 3 par 24 heures | send_rejected contact_request_rate_limited. |
Patientez au moins retry_after secondes. Ne renvoyez pas en boucle serrée : les compteurs continuent de tourner pendant vos nouvelles tentatives.
Liste de contrôle d'implémentation
- Générez et stockez une identité Ed25519 ; utilisez partout des clés en hexadécimal minuscule.
- Implémentez RelayAuth ainsi que l'ouverture de session WebSocket, le battement de cœur et
presence_state. - Implémentez l'enveloppe scellée et validez-la à l'aide du vecteur de référence de API HTTPS Central Chat v1.
- Envoyez avec
relay_send, considérezdurable: truecomme « envoyé », et recalculeztimestampetpayload_sigà chaque nouvelle tentative. - Recevez
relay_envelope: dédoublonnez, vérifiez, déchiffrez, stockez, puis envoyezmessage_receiptetrelay_offline_ack. - Exécutez
relay_pullaprès l'ouverture de session et au réveil par notification push ; n'acquittez qu'après un stockage durable. - Lisez les indicateurs de confidentialité du profil et respectez-les pour le statut de présence et les accusés de lecture.
- Chiffrez les pièces jointes sur l'appareil et téléversez-les via
presignou en multipart ; conservezfile_keyà l'intérieur du message chiffré. - Vérifiez l'auteur avant d'appliquer les modifications et les révocations.
- Conservez la recherche et l'historique des messages sur l'appareil. Le relais n'offre aucune recherche de contenu et ne constitue pas une archive.
Questions fréquentes
Le relais de chat AeroNyx peut-il lire mes messages ?
Non. Les messages, modifications, réactions, messages de groupe et pièces jointes sont chiffrés et signés sur l'appareil de l'expéditeur avant d'atteindre le relais, et les clés ne quittent jamais les appareils des participants à la conversation. Le relais ne stocke et ne transmet que du texte chiffré.
Que peut voir le relais de chat AeroNyx ?
Le relais voit les métadonnées de distribution : les clés publiques de l'expéditeur et du destinataire, les identifiants de groupe et de message, les horodatages, la taille des charges utiles, l'état de distribution et de lecture, les signaux de présence et de saisie, les métadonnées de signalisation des appels, la taille des pièces jointes et les adresses IP de connexion. Il ne voit ni le contenu des messages, ni le contenu des pièces jointes, ni les clés. La liste complète figure dans le modèle de confiance en haut de cette page.
Quel chiffrement le chat AeroNyx utilise-t-il ?
Chaque identité est une paire de clés Ed25519. Deux personnes dérivent une clé de chat partagée avec X25519 et HKDF-SHA256. Les messages 1:1 sont chiffrés avec XChaCha20-Poly1305 et signés avec Ed25519. Les messages de groupe et les pièces jointes sont chiffrés avec AES-256-GCM. Les formats exacts au niveau des octets et un vecteur de test sont publiés dans la documentation de l'API HTTPS de chat central v1.
Comment les photos, vidéos et fichiers sont-ils protégés ?
Chaque fichier est chiffré sur l'appareil avec sa propre clé AES-256-GCM aléatoire avant l'envoi. Le stockage ne reçoit que du texte chiffré. La clé du fichier est transmise à l'intérieur du message chiffré de bout en bout, de sorte que seuls les destinataires peuvent déchiffrer le fichier.
Que se passe-t-il si le destinataire est hors ligne ?
Le relais conserve les messages chiffrés dans la file d'attente hors ligne du destinataire pendant 72 heures au maximum et les distribue lorsque le destinataire se reconnecte. Sur iOS et macOS, le destinataire reçoit également une notification push qui ne contient aucun contenu de message.
AeroNyx conserve-t-il mon historique de chat ?
Non. Le relais est une mémoire tampon de distribution : les éléments sont supprimés une fois que l'appareil du destinataire a confirmé les avoir stockés. L'historique des conversations et la recherche résident sur vos appareils.
Puis-je créer mon propre client ou bot AeroNyx ?
Oui. Tout logiciel qui détient une identité Ed25519 et implémente les formats décrits sur cette page peut échanger des messages avec les utilisateurs de l'AeroNyx App. Pour une intégration requête/réponse plus simple, sans WebSocket, utilisez l'API HTTPS de chat central v1.
Le relais de chat AeroNyx est-il décentralisé ?
Le relais de chat est le service de distribution centralisé d'AeroNyx. AeroNyx exploite également un réseau de nœuds open source (AGPL-3.0) capable d'acheminer le texte chiffré des conversations par un itinéraire à deux sauts traversant des réseaux distincts. Les deux voies coexistent : le relais assure une distribution rapide et fiable ainsi que des files d'attente hors ligne, tandis que la voie des nœuds est un itinéraire facultatif qui réduit ce qu'un opérateur unique, quel qu'il soit, peut observer.
Pourquoi mon message est-il rejeté avec verification_required ?
Le destinataire n'accepte que les messages de ses contacts. Envoyez un unique premier message avec contact_request: true, que le destinataire verra comme une demande de contact. Trois demandes de contact au maximum par destinataire sont autorisées sur toute fenêtre de 24 heures.
Remise vérifiée à deux sauts
Pour le trafic ChatRelay authentifié éligible, la source peut choisir un chemin à deux sauts à diversité réseau et ne comptabiliser la remise qu'après avoir validé l'accusé signé du terminal attendu. Les nœuds relais acheminent le texte chiffré et n'analysent pas la charge utile chiffrée de bout en bout. Le modèle de preuve complet est présenté dans Découverte des nœuds et remise chiffrée vérifiée par relais.
<!-- verified-two-hop-delivery-v1:end -->