AeroNyx 聊天中繼用戶端整合

AeroNyx20 分鐘閱讀

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 端點,皆使用同一種簽章:

text
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 標頭

http
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 連線

端點

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

原生用戶端連線時不帶 Origin 標頭。瀏覽器必須從允許的來源發起連線;任何其他來源或未知的 Host,都會以代碼 1008 關閉連線。

登入

伺服器接受 socket 連線後,要求在 30 秒內收到 auth 訊框:

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

成功:

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

通用錯誤訊框:

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

速率限制錯誤另會附帶 scope。

關閉代碼

代碼意義
1008Host 或 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 物件:

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,中繼在接受訊框之前會先驗證此簽章:

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 是解碼後的 payload_b64。訊息與編輯使用判別值 11,表情回應使用判別值 12。payload_sig 以十六進位編碼。

3. 傳送

json
{
  "type": "relay_send",
  "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
  "receiver_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>"
}
欄位規則
msg_id32 個小寫十六進位字元,即信封中 16 位元組的 message_id。接收端 App 會以此 ID 儲存訊息,因此必須與信封一致。
receiver_pubkey接收方的公鑰。
discriminant11。
timestamp以秒為單位的 Unix 時間戳記,與伺服器時間的誤差在 ±300 秒以內。
suppress_push選填。設為 true 時不傳送推播通知。
contact_request選填。用於標記傳給要求聯絡人驗證之使用者的第一則訊息(請參閱聯絡人驗證)。

4. 確認

json
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
  • 訊息存入接收方的離線佇列後,durable(以及 queued)即為 true。請將 durable: true 視為「已傳送」。
  • delivered 表示接收方當時有使用中的前景連線。這並不能證明對方已收到訊息;如需確認,請使用遞送回條。

請最多等候 15 秒以接收確認,逾時後再將此次嘗試視為失敗。

拒絕與錯誤

json
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
回應原因處理方式
send_rejectedverification_required接收方僅接受聯絡人傳送的訊息。請勿重試。
send_rejectedcontact_request_rate_limited對此接收方的聯絡請求已達上限。請勿重試。
errormissing_fields缺少必要欄位,或欄位為空。
errorinvalid_timestamp,timestamp_expired校正時鐘後重新產生時間戳記。
errorinvalid_payload_sigpayload_sig 驗證失敗。
errorrate_limited於 retry_after 秒後重試。

重試

對於未獲確認的訊息,請以相同的 msg_id 與相同的信封位元組重試。由於中繼會拒絕超過 300 秒的訊框,每次重試都須重新計算訊框的 timestamp 與 payload_sig;信封本身則保留原始時間戳記。中繼與接收方皆以 msg_id 去除重複項目。

HTTPS 備援管道

WebSocket 無法使用時,可透過 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>"
}

成功時回傳 HTTP 200 與 {"success": true}。聯絡人驗證失敗時回傳 400,並附帶 verification_required 或 contact_request_rate_limited;觸發速率限制時回傳 429,並附帶 Retry-After。以此方式傳送的訊息會排入接收方的佇列,但不會觸發推播通知,因此請於 WebSocket 重新連線後,再透過 WebSocket 重新傳送。

接收訊息

傳入的訊息會以 relay_envelope 的形式抵達:

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

對於 discriminant: 11:

  1. 依 msg_id 去除重複。同一則訊息可能先即時抵達,之後又從離線佇列再次抵達。
  2. 解析信封,並確認其 receiver_pubkey 為您的身分。
  3. 對於即時訊框(from_offline: false),若信封時間戳記與本機時鐘相差超過 300 秒,即捨棄該訊息。
  4. 以信封中的 sender_pubkey 驗證信封簽章。通過驗證的傳送方是這把金鑰,而非訊框中的 sender_pubkey。
  5. 以從該傳送方衍生的聊天金鑰解密。非常舊的 App 版本直接以原始 X25519 輸出加密;若 HKDF 金鑰解密失敗,可改用此方式嘗試。
  6. 將訊息持久儲存,接著傳送遞送回條;若為 from_offline: true 的訊息,還須傳送離線確認。

一對一信封在遞送時不帶 payload_sig,信封簽章本身即為真實性驗證。訊框也可能帶有 contact_request: true。

若訊息並非傳給您、驗證失敗或解密失敗,請直接捨棄,不顯示任何內容。

離線遞送

每一則訊息、編輯、收回、表情回應、回條與已讀回條,在即時遞送之前都會先寫入接收方的離線佇列。每次登入後佇列會自動重播,也可視需要要求重播:

json
{ "type": "relay_pull" }

中繼會依時間戳記順序,以各項目原本的訊框類型並附帶 from_offline: true,重播所有已排入佇列的項目,接著傳送:

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

遞送語意為至少一次。各項目在獲得確認之前會一直保留在佇列中:

json
{ "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 的訊框大小上限。

中繼是遞送緩衝區,而非訊息歷程記錄。請將歷程記錄保存在裝置上。

遞送回條與已讀回條

遞送回條

儲存訊息後傳送:

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

中繼會回覆 message_receipt_ack,並將 {"type":"message_receipt","sender_pubkey":"...","msg_id":"...","timestamp":...} 遞送給原傳送方。

已讀回條

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

已讀回條採用水位標記機制:App 會將指定訊息以及該對話中所有更早的外送訊息標記為已讀。僅在使用者啟用已讀回條時才傳送。

中繼在傳送、即時遞送與重播三個環節皆執行對等規則:唯有雙方互為聯絡人,且皆已啟用 read_receipts_enabled 時,已讀回條才會遞送。否則傳送方會收到:

json
{ "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 傳送:

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 採用為訊框簽章一節中的公式,判別值為 11。移除所有附件的編輯,會在其明文 JSON 中設定 "attachments_edited": true。中繼會回覆 message_edit_ack。接收方應以處理一般訊息的方式驗證並解密此信封,且唯有編輯的傳送方正是原訊息作者時,才可套用該編輯。

收回

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

收回簽章直接作用於原始位元組,不經雜湊:

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)

中繼會回覆 message_revoke_ack。中繼不會檢查作者身分:接收方必須驗證簽章,且唯有收回的傳送方正是原訊息作者時,才可套用該收回。如需刪除已收回訊息的附件,請呼叫 POST /api/relay/blob/{blob_id}/delete/。

表情回應

一對一表情回應是一個密封信封,其 message_id 為回應 ID,content_type 為 2,明文為 {"emoji": "❤️", "op": "add"}(或 "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 使用判別值 12。
  • reaction_id 是冪等鍵。72 小時內重複的 reaction_id 會以 "duplicate": true 確認,但不會再次遞送。
  • 中繼會回覆 {"type":"message_reaction_ack","msg_id":"...","reaction_id":"...","delivered":...,"queued":...}。
  • 在速率時間範圍內,每位傳送方在每個對話中最多可傳送 20 個表情回應。

群組表情回應請參閱群組一節。

上線狀態與正在輸入

上線狀態與正在輸入訊框屬於明文中繼資料,中繼可以看見。

上線狀態

訂閱聯絡人:

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

每個訊框最多處理 200 把公鑰;格式錯誤與重複的公鑰會被忽略。訂閱會在連線的整個存續期間持續累積。請僅訂閱您自己的聯絡人。

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
}

上線狀態僅在互為聯絡人的雙方之間可見,且前提是對方已啟用 presence_enabled。遭隱藏的項目(reason 為 not_mutual_contact 或 presence_hidden)不含 online 或 last_seen_ts。唯有 last_seen_visible 為 true 時才會包含 last_seen_ts;否則請顯示通用狀態,例如「最近上線」。

即時變更會以下列形式抵達:

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

正在輸入

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

唯有傳送方的上線狀態對接收方可見(互為聯絡人且已啟用 presence_enabled),或接收方是傳送方所屬群組的其他成員時,正在輸入提示才會轉送。此提示絕不會被儲存、排入佇列或推播。

個人資料與隱私設定

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

尚未設定個人資料的身分會收到空白欄位,且三個隱私旗標皆為 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 } }
欄位規則
display_name最多 50 個字元。
bio最多 200 個字元。
avatar_urlhttps:// 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 加密:

text
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。

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))

中繼會檢查簽章並確認傳送方為有效成員,為其他每位有效成員儲存該訊息,接著進行即時遞送。群組酬載不使用一對一信封;群組信封在遞送時會帶有 group_id、key_version 與 payload_sig,讓接收方能在解密前驗證傳送方。

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
}

非成員的傳送方會收到原因為 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:

text
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)

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 為密文大小。media_kind 為 voice、image、video、file、avatar、other 其中之一。ttl_days 限制在 1–30 之間(預設為 7)。

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
}

接著:

  1. 於 15 分鐘內以 PUT 將密文上傳至 upload_url,僅帶 Content-Type 標頭。請勿將 Authorization 標頭傳送給儲存服務。
  2. 帶上 RelayAuth 呼叫 POST /api/relay/blob/{blob_id}/complete/。中繼確認物件後會回傳 {blob_id, file_size, expires_at, storage}。

上傳:分段上傳(最大 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 預設為 8 MiB,亦可要求 5 至 16 MiB 之間的值。分段 URL 的有效期限為 60 分鐘。以 PUT 將每個分段上傳至對應的 URL,並記錄 ETag 回應標頭,接著:

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

AeroNyx App 在密文不超過 8 MiB 時使用單一請求上傳,超過 8 MiB 時則使用分段上傳。

下載

http
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 與到期時間會在中繼重新導向時強制執行,但請勿依賴它們來確保機密性。

刪除附件

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

僅上傳者可以刪除 blob。回應為 {"blob_id": "...", "deleted": true}。

附件錯誤

錯誤採用 {"success": false, "error": "<text>", "error_code": "<code>"} 格式。請依 error_code 進行分支處理。

HTTPerror_code意義
400blob_id_invalidblob ID 格式錯誤。
400blob_missing_file_size,blob_missing_total_size,blob_missing_parts,blob_bad_parts,blob_bad_part_count上傳請求無效。
400blob_not_r2,blob_multipart_complete_failed完成上傳失敗;請重新開始上傳。
401auth_required此 blob 需要 RelayAuth 身分驗證。
403blob_not_uploader,download_forbidden呼叫方無權執行此操作。
404blob_not_found,blob_not_uploadedblob 不存在,或在上傳尚未完成時即呼叫完成端點。
410blob_expiredblob 已到期。請要求傳送方重新傳送。
413blob_too_large超過大小上限;回應中包含 max_bytes 與 chunked_max_bytes。
503blob_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 會觸發推播。編輯、收回、表情回應與回條不會觸發推播。推播酬載不含任何密文:

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 為 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 秒。請勿在密集迴圈中重複傳送:重試期間計數器仍會持續累計。

實作檢查清單

  1. 產生並儲存 Ed25519 身分;所有情境一律使用小寫十六進位公鑰。
  2. 實作 RelayAuth,以及 WebSocket 登入、心跳與 presence_state。
  3. 實作密封信封,並以中心化聊天 HTTPS API v1中的黃金向量進行驗證。
  4. 以 relay_send 傳送,將 durable: true 視為已傳送,重試時重新產生 timestamp 與 payload_sig。
  5. 接收 relay_envelope:去除重複、驗證、解密、儲存,接著傳送 message_receipt 與 relay_offline_ack。
  6. 於登入後及被推播喚醒時執行 relay_pull;僅在持久儲存完成後再進行確認。
  7. 讀取個人資料中的隱私旗標,並在上線狀態與已讀回條上遵循這些設定。
  8. 在裝置上加密附件,並透過 presign 或分段上傳進行上傳;將 file_key 保留在加密訊息內部。
  9. 在套用編輯與收回之前驗證作者身分。
  10. 在裝置上保存訊息搜尋與歷程記錄。中繼不提供內容搜尋,也不是封存服務。
<!-- faq:start -->

常見問題

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 小時的時間範圍內,每位接收方最多允許三次聯絡人請求。

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

經驗證的雙跳遞送

對於符合條件且已通過身分驗證的 ChatRelay 流量,來源端可選擇具備網路多樣性的雙跳路徑,且唯有在驗證預期終端節點的簽章回條之後,才將其計為遞送成功。中繼節點僅負責路由密文,不會解析端對端加密酬載。完整的證據模型請參閱節點探索與經驗證的加密中繼遞送。

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