AeroNyx 聊天中繼用戶端整合
AeroNyx 聊天中繼整合參考:WebSocket 身分驗證、密封的一對一與群組訊息、離線遞送、回條、表情回應、上線狀態、加密附件、推播通知與速率限制。
產生接入提示詞
選擇技術棧與接入目標,即可複製給程式設計助手。提示詞在本機產生,不會傳送至 AI 服務。
產生的提示詞
本文是 AeroNyx 聊天中繼(Chat Relay)的整合參考文件。聊天中繼是部署於 api.aeronyx.network 的 WebSocket 與 HTTPS 服務,負責在 AeroNyx 身分之間傳遞端對端加密的一對一訊息、群組訊息、回條、表情回應、上線狀態以及加密附件。
本文的讀者為開發 AeroNyx 相容用戶端、機器人與服務的工程師,以及負責實作這些功能的 AI 程式開發代理。本頁所列的每一個訊框、欄位與限制,皆與截至 2026 年 10 月的正式環境中繼及 AeroNyx App 一致。
聊天中繼是中心化的遞送路徑。它與去中心化的節點路徑(洋蔥路由與匿名信箱,請參閱經驗證的雙跳遞送)並存,用戶端可同時使用兩者。若需不經 WebSocket 的請求/回應式 HTTPS 整合,請參閱中心化聊天 HTTPS API v1。
信任模型
中繼無法看見任何內容。用戶端在資料抵達中繼之前即完成所有加密與簽章,中繼僅負責對不透明的密文進行路由、排入佇列與速率限制。
中繼絕不會收到:
- 明文形式的訊息文字、表情回應 emoji、編輯內容或群組酬載
- 聊天金鑰、群組金鑰、附件金鑰或 nonce
- 附件內容、檔案名稱、縮圖、波形或逐字稿
- 身分私鑰
中繼能夠觀察到遞送中繼資料,整合方應將這些資訊視為營運方可見:
- 傳送方與接收方的公鑰、群組 ID 與訊息 ID
- 時間戳記、酬載大小,以及遞送、回條與已讀狀態
- 上線狀態、前景狀態與正在輸入提示(以明文訊框傳送)
- 通話信令中繼資料(房間名稱、通話 ID、視訊旗標)
- 附件密文大小、宣告的媒體類型與到期時間
contact_request旗標以及 IP 層級的連線中繼資料
內容的機密性來自端對端加密,而非中繼上的存取控制。請依此進行設計:即使攻擊者取得已儲存的密文,也必須仍然無法讀取其內容。
身分與金鑰
AeroNyx 聊天身分是一組 Ed25519 金鑰對。32 位元組的公鑰以 64 個小寫十六進位字元表示,即為位址。請在裝置上產生金鑰,且絕不可將私鑰傳送至任何地方。
由身分金鑰對衍生出兩種金鑰:
- 聊天金鑰(一對一)。
HKDF-SHA256(salt = empty, ikm = X25519(my_secret, peer_public), info = "AERONYX-P2P-KEY", length = 32),其中兩把 Ed25519 金鑰皆須轉換為 X25519(私鑰取SHA-512(seed)[0..32]並進行鉗位處理,公鑰則採用 Edwards 曲線至 Montgomery 曲線的轉換)。通訊雙方會衍生出相同的金鑰。 - 訊息簽章。 以身分金鑰進行 Ed25519 簽章,簽章對象為本頁定義的確切位元組字串。
訊框中的公鑰一律使用小寫十六進位。中繼並不會在每個佇列鍵中都將大小寫正規化。
身分驗證
RelayAuth 簽章
WebSocket 登入以及所有需要身分驗證的 HTTPS 端點,皆使用同一種簽章:
digest = SHA256("AeroNyx-RelayAuth-v1" || pubkey[32] || timestamp as u64 little-endian)
signature = Ed25519(identity_key, digest)
"AeroNyx-RelayAuth-v1" 為 20 個 ASCII 位元組,不含結尾字元。timestamp 為以秒為單位的 Unix 時間戳記,與伺服器時間的誤差必須在 300 秒以內。
此簽章僅繫結身分與時間,不繫結請求方法、路徑、請求本文或連線,也不含 nonce。請為每個請求產生新的時間戳記,僅透過 TLS 傳送,且絕不可寫入記錄檔。
HTTPS 標頭
Authorization: Relay <pubkey-hex>:<timestamp>:<signature-hex>
驗證失敗時會回傳 HTTP 401 與 {"success": false, "error": "<reason>"}。原因包括 missing_auth_header、malformed_auth_header、invalid_timestamp、timestamp_expired、invalid_pubkey 與 invalid_signature。
WebSocket 連線
端點
wss://api.aeronyx.network/ws/relay/
原生用戶端連線時不帶 Origin 標頭。瀏覽器必須從允許的來源發起連線;任何其他來源或未知的 Host,都會以代碼 1008 關閉連線。
登入
伺服器接受 socket 連線後,要求在 30 秒內收到 auth 訊框:
{
"type": "auth",
"pubkey": "<64 hex>",
"timestamp": 1780000000,
"signature": "<128 hex>"
}
成功:
{ "type": "auth_ack", "session_id": "<uuid>", "server_ts": 1780000001 }
請使用 server_ts 估算時鐘誤差;訊息時間戳記同樣以 ±300 秒的時間範圍進行檢查。
收到 auth_ack 後,伺服器會啟動心跳、為此連線訂閱對應頻道,並立即重播離線佇列(請參閱離線遞送)。
失敗時,伺服器會傳送 {"type": "auth_error", "reason": "<reason>"},並以代碼 4001 關閉 socket。原因包括:missing_fields、timestamp_expired、invalid_pubkey、invalid_signature_length、invalid_signature_encoding、invalid_signature、internal_error。登入逾時會傳送原因 timeout,並以 4002 關閉連線。
登入前收到的任何其他訊框,伺服器皆以原因為 authentication_required 的 auth_error 回應,socket 則維持開啟。
心跳與前景狀態
| 方向 | 訊框 | 行為 |
|---|---|---|
| 伺服器 → 用戶端 | {"type":"ping"},每 30 秒一次 | 回覆 {"type":"pong"}。 |
| 用戶端 → 伺服器 | {"type":"ping"} | 伺服器回覆 {"type":"pong"}。 |
| 用戶端 → 伺服器 | {"type":"presence_state","foreground":true} | 將此連線標記為使用中。 |
| 用戶端 → 伺服器 | {"type":"presence_state","foreground":false} | 將此連線標記為背景狀態,讓中繼可在有新訊息時傳送推播。 |
唯有當某身分的連線至少每 90 秒傳送一次 ping、pong 或 foreground: true 的 presence_state 時,中繼才會將該身分視為上線。AeroNyx App 在前景時每 15 秒傳送一次 ping,連續遺失超過三個 pong 即視為連線已中斷。
長時間連線應至少每 24 小時重新連線一次。即使連線在無任何提示的情況下停止接收即時訊框,訊息也不會遺失,因為每則訊息同時會排入佇列並於登入時重播;但即時遞送須待重新連線後才會恢復。
訊框規則
- 僅支援文字訊框,每個訊框一個 JSON 物件。二進位訊框會被忽略。
- 訊框大小上限為 1,048,576 個字元。超過上限的訊框會遭拒絕,並回傳
{"type":"error","reason":"message_too_large"}。請將payload_b64控制在約 800 KiB 以內,為訊框其餘部分保留空間。 - 格式錯誤的訊框會回傳
error,原因為invalid_json、invalid_json_type或unknown_type。
通用錯誤訊框:
{ "type": "error", "reason": "<reason>", "retry_after": 0 }
速率限制錯誤另會附帶 scope。
關閉代碼
| 代碼 | 意義 |
|---|---|
1008 | Host 或 Origin 不在允許範圍內。 |
4000 | 伺服器無法送出心跳。 |
4001 | 登入失敗。 |
4002 | 登入逾時。 |
請以加入隨機抖動的指數退避方式重新連線。AeroNyx App 的等待時間為 2^(attempt-1) 秒,限制在 1–60 秒之間,再乘以 0.8 至 1.2 之間的隨機係數。
傳送一對一訊息
1. 建立密封信封
一對一訊息的酬載是經過簽章與加密的 ChatEnvelope。其確切的建構方式、Python 參考實作與黃金測試向量,請參閱中心化聊天 HTTPS API v1:密封信封格式。兩套 API 使用同一種信封。
簡言之:以聊天金鑰進行 XChaCha20-Poly1305 加密,對 121 位元組的簽章原文進行 Ed25519 簽章,並採用固定的二進位配置。所有訊息(包括含附件的訊息)的 content_type 皆為 0。
一般訊息的明文為 UTF-8 文字;含附件、回覆、轉傳或連結預覽的訊息,其明文則為一個 JSON 物件:
{
"type": "aeronyx_message",
"text": "See the attached file",
"attachments": [ { "blob_id": "...", "file_key": "...", "media_type": "image/jpeg", "file_name": "photo.jpg", "file_size": 482113 } ],
"reply": { "msg_id": "...", "sender_pubkey": "...", "text": "..." },
"forwarded": true,
"forwarded_from_name": "...",
"forwarded_from_pubkey": "...",
"link_preview": { "url": "https://...", "title": "...", "description": "...", "site_name": "..." }
}
除 type 與 text 外,其餘欄位皆為選填。凡不屬於 "type": "aeronyx_message" JSON 物件的明文,接收方皆應以純文字呈現。附件物件的定義請參閱加密附件。
2. 為訊框簽章
每個訊息訊框都帶有第二個簽章 payload_sig,中繼在接受訊框之前會先驗證此簽章:
payload_sig = Ed25519(identity_key,
SHA256(receiver_pubkey[32] || sender_pubkey[32] || discriminant as 1 byte
|| timestamp as u64 little-endian || envelope_bytes))
envelope_bytes 是解碼後的 payload_b64。訊息與編輯使用判別值 11,表情回應使用判別值 12。payload_sig 以十六進位編碼。
3. 傳送
{
"type": "relay_send",
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"receiver_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>"
}
| 欄位 | 規則 |
|---|---|
msg_id | 32 個小寫十六進位字元,即信封中 16 位元組的 message_id。接收端 App 會以此 ID 儲存訊息,因此必須與信封一致。 |
receiver_pubkey | 接收方的公鑰。 |
discriminant | 11。 |
timestamp | 以秒為單位的 Unix 時間戳記,與伺服器時間的誤差在 ±300 秒以內。 |
suppress_push | 選填。設為 true 時不傳送推播通知。 |
contact_request | 選填。用於標記傳給要求聯絡人驗證之使用者的第一則訊息(請參閱聯絡人驗證)。 |
4. 確認
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
- 訊息存入接收方的離線佇列後,
durable(以及queued)即為true。請將durable: true視為「已傳送」。 delivered表示接收方當時有使用中的前景連線。這並不能證明對方已收到訊息;如需確認,請使用遞送回條。
請最多等候 15 秒以接收確認,逾時後再將此次嘗試視為失敗。
拒絕與錯誤
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
| 回應 | 原因 | 處理方式 |
|---|---|---|
send_rejected | verification_required | 接收方僅接受聯絡人傳送的訊息。請勿重試。 |
send_rejected | contact_request_rate_limited | 對此接收方的聯絡請求已達上限。請勿重試。 |
error | missing_fields | 缺少必要欄位,或欄位為空。 |
error | invalid_timestamp,timestamp_expired | 校正時鐘後重新產生時間戳記。 |
error | invalid_payload_sig | payload_sig 驗證失敗。 |
error | rate_limited | 於 retry_after 秒後重試。 |
重試
對於未獲確認的訊息,請以相同的 msg_id 與相同的信封位元組重試。由於中繼會拒絕超過 300 秒的訊框,每次重試都須重新計算訊框的 timestamp 與 payload_sig;信封本身則保留原始時間戳記。中繼與接收方皆以 msg_id 去除重複項目。
HTTPS 備援管道
WebSocket 無法使用時,可透過 HTTPS 送出同一則訊息:
POST /api/relay/push/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{
"receiver_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>"
}
成功時回傳 HTTP 200 與 {"success": true}。聯絡人驗證失敗時回傳 400,並附帶 verification_required 或 contact_request_rate_limited;觸發速率限制時回傳 429,並附帶 Retry-After。以此方式傳送的訊息會排入接收方的佇列,但不會觸發推播通知,因此請於 WebSocket 重新連線後,再透過 WebSocket 重新傳送。
接收訊息
傳入的訊息會以 relay_envelope 的形式抵達:
{
"type": "relay_envelope",
"sender_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"from_offline": false
}
對於 discriminant: 11:
- 依
msg_id去除重複。同一則訊息可能先即時抵達,之後又從離線佇列再次抵達。 - 解析信封,並確認其
receiver_pubkey為您的身分。 - 對於即時訊框(
from_offline: false),若信封時間戳記與本機時鐘相差超過 300 秒,即捨棄該訊息。 - 以信封中的
sender_pubkey驗證信封簽章。通過驗證的傳送方是這把金鑰,而非訊框中的sender_pubkey。 - 以從該傳送方衍生的聊天金鑰解密。非常舊的 App 版本直接以原始 X25519 輸出加密;若 HKDF 金鑰解密失敗,可改用此方式嘗試。
- 將訊息持久儲存,接著傳送遞送回條;若為
from_offline: true的訊息,還須傳送離線確認。
一對一信封在遞送時不帶 payload_sig,信封簽章本身即為真實性驗證。訊框也可能帶有 contact_request: true。
若訊息並非傳給您、驗證失敗或解密失敗,請直接捨棄,不顯示任何內容。
離線遞送
每一則訊息、編輯、收回、表情回應、回條與已讀回條,在即時遞送之前都會先寫入接收方的離線佇列。每次登入後佇列會自動重播,也可視需要要求重播:
{ "type": "relay_pull" }
中繼會依時間戳記順序,以各項目原本的訊框類型並附帶 from_offline: true,重播所有已排入佇列的項目,接著傳送:
{ "type": "relay_pull_done", "count": 12, "has_more": false }
遞送語意為至少一次。各項目在獲得確認之前會一直保留在佇列中:
{ "type": "relay_offline_ack", "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834" }
確認表情回應時,請使用 "reaction_id" 而非 "msg_id"。請僅在項目已持久儲存於裝置後再進行確認。中繼會回覆 {"type":"relay_offline_ack","msg_id":"...","success":true}。
佇列限制:
| 限制項目 | 值 |
|---|---|
| 每位接收方的項目數 | 1,000。佇列已滿時,新項目會遭拒絕,傳送方會看到 durable: false。 |
| 保留期限 | 自最近一個項目加入起 72 小時。 |
| 酬載大小 | 解碼後 1 MiB,且不超過 1 MiB 的訊框大小上限。 |
中繼是遞送緩衝區,而非訊息歷程記錄。請將歷程記錄保存在裝置上。
遞送回條與已讀回條
遞送回條
儲存訊息後傳送:
{ "type": "message_receipt", "receiver_pubkey": "<original sender>", "msg_id": "...", "timestamp": 1780000010 }
中繼會回覆 message_receipt_ack,並將 {"type":"message_receipt","sender_pubkey":"...","msg_id":"...","timestamp":...} 遞送給原傳送方。
已讀回條
{ "type": "message_read", "receiver_pubkey": "<original sender>", "msg_id": "<latest read message>", "timestamp": 1780000200, "enabled": true }
已讀回條採用水位標記機制:App 會將指定訊息以及該對話中所有更早的外送訊息標記為已讀。僅在使用者啟用已讀回條時才傳送。
中繼在傳送、即時遞送與重播三個環節皆執行對等規則:唯有雙方互為聯絡人,且皆已啟用 read_receipts_enabled 時,已讀回條才會遞送。否則傳送方會收到:
{ "type": "message_read_ack", "msg_id": "...", "delivered": false, "suppressed": true, "reason": "receiver_read_receipts_disabled" }
| 原因 | 意義 |
|---|---|
client_disabled | 訊框中帶有 enabled: false 或 read_receipts_enabled: false。 |
not_mutual_contact | 雙方並非互為聯絡人。 |
reader_read_receipts_disabled | 閱讀方已關閉已讀回條。 |
receiver_read_receipts_disabled | 原傳送方已關閉已讀回條。 |
invalid_pubkey | 某個公鑰格式錯誤。 |
已遞送的已讀回條會以 {"type":"message_read_ack","msg_id":"...","delivered":true} 確認。
編輯與收回
編輯
編輯是一個新的密封信封(具有自己的隨機 message_id),內含完整的替換內容,並針對原訊息 ID 傳送:
{
"type": "message_edit",
"receiver_pubkey": "<64 hex>",
"msg_id": "<original msg_id>",
"target_msg_id": "<original msg_id>",
"payload_b64": "<base64 envelope>",
"payload_sig": "<128 hex>",
"timestamp": 1780000400
}
payload_sig 採用為訊框簽章一節中的公式,判別值為 11。移除所有附件的編輯,會在其明文 JSON 中設定 "attachments_edited": true。中繼會回覆 message_edit_ack。接收方應以處理一般訊息的方式驗證並解密此信封,且唯有編輯的傳送方正是原訊息作者時,才可套用該編輯。
收回
{
"type": "message_revoke",
"receiver_pubkey": "<64 hex>",
"sender_pubkey": "<64 hex>",
"msg_id": "<original msg_id>",
"timestamp": 1780000500,
"payload_sig": "<128 hex>"
}
收回簽章直接作用於原始位元組,不經雜湊:
payload_sig = Ed25519(identity_key,
"aeronyx-message-revoke-v1" || sender_pubkey[32] || receiver_pubkey[32]
|| UTF-8(msg_id) || timestamp as u64 little-endian)
中繼會回覆 message_revoke_ack。中繼不會檢查作者身分:接收方必須驗證簽章,且唯有收回的傳送方正是原訊息作者時,才可套用該收回。如需刪除已收回訊息的附件,請呼叫 POST /api/relay/blob/{blob_id}/delete/。
表情回應
一對一表情回應是一個密封信封,其 message_id 為回應 ID,content_type 為 2,明文為 {"emoji": "❤️", "op": "add"}(或 "remove"):
{
"type": "message_reaction",
"receiver_pubkey": "<64 hex>",
"msg_id": "<message being reacted to>",
"reaction_id": "<32 hex>",
"payload_b64": "<base64 envelope>",
"payload_sig": "<128 hex>",
"timestamp": 1780000300
}
payload_sig使用判別值12。reaction_id是冪等鍵。72 小時內重複的reaction_id會以"duplicate": true確認,但不會再次遞送。- 中繼會回覆
{"type":"message_reaction_ack","msg_id":"...","reaction_id":"...","delivered":...,"queued":...}。 - 在速率時間範圍內,每位傳送方在每個對話中最多可傳送 20 個表情回應。
群組表情回應請參閱群組一節。
上線狀態與正在輸入
上線狀態與正在輸入訊框屬於明文中繼資料,中繼可以看見。
上線狀態
訂閱聯絡人:
{ "type": "presence_subscribe", "pubkeys": ["<64 hex>", "<64 hex>"] }
每個訊框最多處理 200 把公鑰;格式錯誤與重複的公鑰會被忽略。訂閱會在連線的整個存續期間持續累積。請僅訂閱您自己的聯絡人。
{
"type": "presence_subscribe_ack",
"count": 2,
"updates": [
{ "pubkey": "<a>", "visible": true, "presence_visible": true, "last_seen_visible": true, "online": false, "last_seen_ts": 1780000000, "reason": "allowed" },
{ "pubkey": "<b>", "visible": false, "presence_visible": false, "last_seen_visible": false, "reason": "not_mutual_contact" }
],
"server_ts": 1780000001
}
上線狀態僅在互為聯絡人的雙方之間可見,且前提是對方已啟用 presence_enabled。遭隱藏的項目(reason 為 not_mutual_contact 或 presence_hidden)不含 online 或 last_seen_ts。唯有 last_seen_visible 為 true 時才會包含 last_seen_ts;否則請顯示通用狀態,例如「最近上線」。
即時變更會以下列形式抵達:
{ "type": "presence_update", "pubkey": "<a>", "online": true, "presence_visible": true, "last_seen_visible": true, "last_seen_ts": 1780000100 }
正在輸入
{ "type": "typing", "receiver_pubkey": "<64 hex>", "is_typing": true }
{ "type": "group_typing", "group_id": "<uuid>", "is_typing": true }
唯有傳送方的上線狀態對接收方可見(互為聯絡人且已啟用 presence_enabled),或接收方是傳送方所屬群組的其他成員時,正在輸入提示才會轉送。此提示絕不會被儲存、排入佇列或推播。
個人資料與隱私設定
GET /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
{
"success": true,
"profile": {
"pubkey_hex": "<64 hex>",
"display_name": "AeroNyx User",
"bio": "",
"handle": "",
"avatar_url": "",
"status_text": "",
"privacy": { "presence_enabled": true, "last_seen_enabled": true, "read_receipts_enabled": true },
"updated_at": "2026-10-09T10:00:00Z"
}
}
尚未設定個人資料的身分會收到空白欄位,且三個隱私旗標皆為 true。
PATCH /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{ "display_name": "Alice", "privacy": { "presence_enabled": true, "last_seen_enabled": false, "read_receipts_enabled": false } }
| 欄位 | 規則 |
|---|---|
display_name | 最多 50 個字元。 |
bio | 最多 200 個字元。 |
avatar_url | https:// URL 或空值。 |
handle | 僅限 a-z 與 0-9,長度 5–24 個字元。使用者名稱受會員規則與變更冷卻期限制。 |
privacy.* | 布林值。這三個旗標也可放在最上層傳送。 |
錯誤包括 no_valid_fields、<flag>_invalid_boolean、handle_taken 與 handle_change_cooldown:<date>。
請讓用戶端行為與這些設定保持一致:已讀回條關閉時不要傳送已讀回條;自己的已讀回條關閉時,不要顯示對方的已讀狀態。
聯絡人驗證
使用者可以要求陌生人在傳送訊息前先通過驗證。當接收方啟用此功能,且尚未將傳送方加為聯絡人時,relay_send 會以 verification_required 遭拒絕。
如要開啟對話,請傳送一則帶有 "contact_request": true 的訊息。聯絡請求可略過此檢查,但在滾動的 24 小時時間範圍內,每組傳送方與接收方之間最多 3 次;超出的請求會以 contact_request_rate_limited 遭拒絕。contact_request 旗標會一併遞送給接收方,讓 App 能將該訊息以聯絡請求的形式呈現。
聯絡人驗證適用於 relay_send 與 HTTPS 備援管道。
群組
群組訊息
群組內容以共用的 32 位元組群組金鑰,透過 AES-256-GCM 加密:
payload_b64 = base64(nonce[12] || AES-256-GCM(group_key, plaintext_json) || tag[16])
明文 JSON 包含 text、type(text、media、system 或 reaction)、sender_pubkey、created_at,以及選填的 attachments、mentions、reply、forwarded、forwarded_from_name、forwarded_from_pubkey 與 link_preview。
{
"type": "group_send",
"msg_id": "<32 hex>",
"group_id": "<uuid>",
"payload_b64": "<base64>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>",
"key_version": 3
}
payload_sig = Ed25519(identity_key,
SHA256(UTF-8(group_id) || sender_pubkey[32] || 0x19 || timestamp as u64 little-endian
|| decoded payload))
中繼會檢查簽章並確認傳送方為有效成員,為其他每位有效成員儲存該訊息,接著進行即時遞送。群組酬載不使用一對一信封;群組信封在遞送時會帶有 group_id、key_version 與 payload_sig,讓接收方能在解密前驗證傳送方。
{
"type": "group_delivered",
"msg_id": "...",
"group_id": "...",
"accepted": true,
"accepted_count": 5,
"delivered_count": 2,
"queued_count": 5,
"failed_count": 0,
"member_count": 6
}
非成員的傳送方會收到原因為 not_a_member 的 error。
群組編輯、收回與表情回應
| 訊框 | 簽章 |
|---|---|
group_message_edit (group_id,msg_id,target_msg_id,payload_b64,payload_sig,key_version,timestamp) | 採用上述群組簽章公式。 |
group_message_reaction (group_id,msg_id,reaction_id,payload_b64,payload_sig,key_version,timestamp) | 採用上述群組簽章公式。酬載為 type: "reaction" 的群組酬載。 |
group_message_revoke (group_id,sender_pubkey,msg_id,timestamp,payload_sig) | 對 `"aeronyx-group-message-revoke-v1" |
上述每種訊框都會以對應的 _ack 訊框確認,其中帶有遞送計數。接收方僅套用原作者送出的編輯與收回。
群組金鑰
群組金鑰由群組擁有者以每位成員各自的金鑰包形式發送:base64(nonce[12] || AES-256-GCM(chat_key(owner, member), group_key) || tag[16])。/api/relay/groups/ 下的 REST 端點可用於建立群組、管理成員與邀請、上傳金鑰包、輪替金鑰(keys/rotate/),以及取得呼叫方目前的金鑰包(keys/me/)。傳送方以自己持有的最新金鑰版本加密;缺少某個金鑰版本的接收方應請求 keys/me/,並暫存該訊息,直到取得對應金鑰為止。
加密附件
附件在裝置上加密,以不透明密文的形式上傳,並在加密訊息內部引用。
加密檔案
為每個檔案產生一把隨機的 32 位元組金鑰與一個隨機的 12 位元組 nonce:
blob = nonce[12] || AES-256-GCM(file_key, file_bytes) || tag[16]
上傳 blob。請將 file_key 及所有描述性欄位放在加密訊息中,絕不可放入上傳請求。
附件物件
| 鍵 | 必要 | 意義 |
|---|---|---|
blob_id | 是 | 上傳時回傳的 ID。 |
file_key | 是 | 32 位元組檔案金鑰的 Base64 編碼。 |
media_type | 是 | 明文檔案的 MIME 類型。 |
file_name | 是 | 顯示名稱。 |
file_size | 是 | 明文大小(位元組)。 |
thumb_b64 | 否 | Base64 編碼的 JPEG 縮圖,最大 64 KiB。 |
duration_ms | 否 | 音訊或影片長度。 |
waveform | 否 | 語音訊息的波形資料,最多 96 個介於 [0, 1] 的數值。 |
sticker,sticker_pack,sticker_pose | 否 | 貼圖識別資訊。 |
live | 否 | Live Photo 的動態部分:{blob_id, file_key, media_type, file_size, duration_ms?, width?, height?}。 |
接收方會忽略缺少 blob_id 或 file_key 的附件。App 送出的語音訊息為以 MP4 容器封裝的 AAC-LC 音訊(audio/mp4)。
上傳:單一請求(最大 10 MiB)
POST /api/relay/blob/presign/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{ "file_size": 482141, "media_type": "image/jpeg", "media_kind": "image", "ttl_days": 7 }
file_size 為密文大小。media_kind 為 voice、image、video、file、avatar、other 其中之一。ttl_days 限制在 1–30 之間(預設為 7)。
{
"blob_id": "<uuid>",
"upload_url": "https://...",
"upload_method": "PUT",
"storage": "r2",
"expires_at": "2026-10-16T10:00:00Z",
"media_type": "image/jpeg",
"media_kind": "image",
"access_mode": "capability",
"ttl_days": 7,
"max_bytes": 10485760
}
接著:
- 於 15 分鐘內以
PUT將密文上傳至upload_url,僅帶Content-Type標頭。請勿將Authorization標頭傳送給儲存服務。 - 帶上 RelayAuth 呼叫
POST /api/relay/blob/{blob_id}/complete/。中繼確認物件後會回傳{blob_id, file_size, expires_at, storage}。
上傳:分段上傳(最大 100 MiB)
POST /api/relay/blob/multipart/create/
{ "total_size": 52428800, "media_type": "video/mp4", "media_kind": "video", "ttl_days": 7 }
{
"blob_id": "<uuid>",
"upload_id": "...",
"part_size": 8388608,
"total_parts": 7,
"part_urls": ["https://...", "..."],
"storage": "r2",
"expires_at": "...",
"max_bytes": 104857600
}
part_size 預設為 8 MiB,亦可要求 5 至 16 MiB 之間的值。分段 URL 的有效期限為 60 分鐘。以 PUT 將每個分段上傳至對應的 URL,並記錄 ETag 回應標頭,接著:
POST /api/relay/blob/multipart/complete/
{ "blob_id": "<uuid>", "upload_id": "...", "parts": [ { "part_number": 1, "etag": "\"...\"" } ] }
AeroNyx App 在密文不超過 8 MiB 時使用單一請求上傳,超過 8 MiB 時則使用分段上傳。
下載
GET /api/relay/blob/{blob_id}/
中繼會以 302 回應,其中 Location 指向內容傳遞網路,並附帶 X-AeroNyx-Blob-Size、X-AeroNyx-Blob-Media-Type 與 X-AeroNyx-Blob-Storage 標頭。請由用戶端自行追蹤重新導向,且不可轉送任何 Authorization 標頭,接著以對應的 file_key 驗證並解密該 blob。下載量應以預期大小為上限。
blob_id 是一種持有者能力憑證:任何持有者皆可取得密文,這正是金鑰只在加密訊息內部傳遞的原因。access_mode 與到期時間會在中繼重新導向時強制執行,但請勿依賴它們來確保機密性。
刪除附件
POST /api/relay/blob/{blob_id}/delete/
僅上傳者可以刪除 blob。回應為 {"blob_id": "...", "deleted": true}。
附件錯誤
錯誤採用 {"success": false, "error": "<text>", "error_code": "<code>"} 格式。請依 error_code 進行分支處理。
| HTTP | error_code | 意義 |
|---|---|---|
| 400 | blob_id_invalid | blob ID 格式錯誤。 |
| 400 | blob_missing_file_size,blob_missing_total_size,blob_missing_parts,blob_bad_parts,blob_bad_part_count | 上傳請求無效。 |
| 400 | blob_not_r2,blob_multipart_complete_failed | 完成上傳失敗;請重新開始上傳。 |
| 401 | auth_required | 此 blob 需要 RelayAuth 身分驗證。 |
| 403 | blob_not_uploader,download_forbidden | 呼叫方無權執行此操作。 |
| 404 | blob_not_found,blob_not_uploaded | blob 不存在,或在上傳尚未完成時即呼叫完成端點。 |
| 410 | blob_expired | blob 已到期。請要求傳送方重新傳送。 |
| 413 | blob_too_large | 超過大小上限;回應中包含 max_bytes 與 chunked_max_bytes。 |
| 503 | blob_r2_unavailable | 儲存服務暫時無法使用;請以退避方式重試。 |
舊版上傳端點
POST /api/relay/blob/(multipart 表單,最大 10 MiB)以及 /api/relay/blob/session/ 下的可續傳工作階段 API(最大 100 MiB,區塊大小為 64 KiB 至 4 MiB,工作階段有效期限 24 小時)仍保留作為備援方案。新的用戶端應使用上述端點。
推播通知
中繼透過 Apple 推播通知服務(APNs)向 iOS 與 macOS 傳送通知。Android 用戶端僅透過 WebSocket 接收訊息。
當接收方沒有使用中的前景連線、傳送方未設定 suppress_push,且接收方未將該對話設為靜音時,relay_send 與 group_send 會觸發推播。編輯、收回、表情回應與回條不會觸發推播。推播酬載不含任何密文:
{
"aps": { "alert": { "title": "AeroNyx", "body": "New encrypted message" }, "sound": "default", "badge": 1, "content-available": 1 },
"kind": "p2p_message",
"sender_pubkey": "<64 hex>",
"message_id": "..."
}
kind 為 p2p_message(附帶 sender_pubkey)或 group_message(附帶 group_id)。通話通知使用 missed_call 以及專用的通話酬載。收到推播後,請建立連線並執行 relay_pull。
| 端點 | 請求本文 |
|---|---|
POST /api/relay/push/register/ | token(64 位十六進位)、platform(ios 或 macos)、bundle_id、environment(production 或 sandbox),以及選填的 token_type(alert 或 voip)與 provider(apns)。 |
POST /api/relay/push/unregister/ | token,以及選填的 platform、token_type、provider。 |
POST /api/relay/push/mute/ | kind(p2p 或 group)、target(公鑰或群組 ID)、muted(布林值)。 |
以上三個端點皆需 RelayAuth 身分驗證。註冊權杖時,該權杖會轉移至呼叫方的身分名下。
通話
語音與視訊通話的信令以明文中繼資料的形式,經由同一條 WebSocket 傳輸,媒體則另行傳輸。一對一通話使用的訊框為 call_invite、call_answer、call_reject、call_hangup 與 call_busy;群組通話使用 group_call_invite、group_call_invite_broadcast、group_call_answer 與 group_call_hangup_broadcast;主持的會議另有會議准入訊框。
中繼會依參與者驗證 room_name:一對一通話為 p2p_ 後接 SHA256(lower_key + ":" + higher_key) 的前 16 個十六進位字元;群組通話為 grp_<first 8 characters of group_id>_<first 8 hex of SHA256(group_id)>。傳給離線接收方的通話信令會保留 120 秒。
速率限制
| 範圍 | 限制 | 回應 |
|---|---|---|
relay_send、group_send 與 HTTPS 推送端點,以身分計 | 每個短時間範圍 50 次;每天 100,000 次 | WebSocket:error rate_limited,附帶 retry_after。HTTPS:429,附帶 Retry-After。 |
group_send,以傳送方與群組計 | 每個短時間範圍 10 次 | error rate_limited,scope: "sender"。 |
group_send,以群組計 | 每個短時間範圍 50 次 | error rate_limited,scope: "group"。 |
| 表情回應,以傳送方與對話計 | 每個短時間範圍 20 次 | error rate_limited,scope: "reaction"。 |
| 聯絡請求,以傳送方與接收方計 | 每 24 小時 3 次 | send_rejected contact_request_rate_limited。 |
請至少退避 retry_after 秒。請勿在密集迴圈中重複傳送:重試期間計數器仍會持續累計。
實作檢查清單
- 產生並儲存 Ed25519 身分;所有情境一律使用小寫十六進位公鑰。
- 實作 RelayAuth,以及 WebSocket 登入、心跳與
presence_state。 - 實作密封信封,並以中心化聊天 HTTPS API v1中的黃金向量進行驗證。
- 以
relay_send傳送,將durable: true視為已傳送,重試時重新產生timestamp與payload_sig。 - 接收
relay_envelope:去除重複、驗證、解密、儲存,接著傳送message_receipt與relay_offline_ack。 - 於登入後及被推播喚醒時執行
relay_pull;僅在持久儲存完成後再進行確認。 - 讀取個人資料中的隱私旗標,並在上線狀態與已讀回條上遵循這些設定。
- 在裝置上加密附件,並透過
presign或分段上傳進行上傳;將file_key保留在加密訊息內部。 - 在套用編輯與收回之前驗證作者身分。
- 在裝置上保存訊息搜尋與歷程記錄。中繼不提供內容搜尋,也不是封存服務。
常見問題
AeroNyx 聊天中繼能讀取我的訊息嗎?
不能。訊息、編輯、表情回應、群組訊息與附件在抵達中繼之前,就已在傳送方的裝置上完成加密與簽章,且金鑰絕不會離開對話參與者的裝置。中繼僅儲存並轉送密文。
AeroNyx 聊天中繼能看見什麼?
中繼能看見遞送中繼資料:傳送方與接收方的公鑰、群組 ID 與訊息 ID、時間戳記、酬載大小、遞送與已讀狀態、上線狀態與正在輸入訊號、通話信令中繼資料、附件大小,以及連線的 IP 位址。它看不見訊息內容、附件內容或金鑰。完整清單請見本頁頂端的信任模型。
AeroNyx 聊天使用哪種加密?
每個身分都是一組 Ed25519 金鑰對。雙方透過 X25519 與 HKDF-SHA256 衍生出共用的聊天金鑰。一對一訊息以 XChaCha20-Poly1305 加密,並以 Ed25519 簽章。群組訊息與附件以 AES-256-GCM 加密。確切的位元組格式與測試向量已公布於中心化聊天 HTTPS API v1 文件中。
照片、影片與檔案如何受到保護?
每個檔案在上傳之前,都會在裝置上以其專屬的隨機 AES-256-GCM 金鑰加密。儲存服務僅會收到密文。檔案金鑰在端對端加密的訊息內傳遞,因此只有接收方才能解密該檔案。
如果接收方離線會怎麼樣?
中繼會將加密訊息保存在接收方的離線佇列中,最長 72 小時,並在接收方重新連線時遞送。在 iOS 與 macOS 上,接收方還會收到一則不含訊息內容的推播通知。
AeroNyx 會保存我的聊天紀錄嗎?
不會。中繼只是遞送緩衝區:接收方裝置確認已儲存項目後,這些項目即會被移除。聊天紀錄與搜尋都保存在您的裝置上。
我可以打造自己的 AeroNyx 用戶端或機器人嗎?
可以。任何持有 Ed25519 身分並實作本頁所述格式的軟體,都能與 AeroNyx App 使用者交換訊息。若需不使用 WebSocket、更簡單的請求/回應式整合,請使用中心化聊天 HTTPS API v1。
AeroNyx 聊天中繼是去中心化的嗎?
聊天中繼是 AeroNyx 的中心化遞送服務。AeroNyx 另外營運一個開放原始碼(AGPL-3.0)的節點網路,可透過網路多樣化的雙跳路由傳送聊天密文。兩條路徑並存:中繼提供快速、可靠的遞送與離線佇列,而節點路徑是一條可選的路由,可減少任何單一營運方能夠觀察到的資訊。
為什麼我的訊息因 verification_required 遭到拒絕?
接收方只接受來自聯絡人的訊息。請傳送一則帶有 contact_request: true 的第一則訊息,接收方會將其視為聯絡人請求。在任何 24 小時的時間範圍內,每位接收方最多允許三次聯絡人請求。
經驗證的雙跳遞送
對於符合條件且已通過身分驗證的 ChatRelay 流量,來源端可選擇具備網路多樣性的雙跳路徑,且唯有在驗證預期終端節點的簽章回條之後,才將其計為遞送成功。中繼節點僅負責路由密文,不會解析端對端加密酬載。完整的證據模型請參閱節點探索與經驗證的加密中繼遞送。
<!-- verified-two-hop-delivery-v1:end -->