Hướng dẫn tích hợp AeroNyx Chat API

16 phút đọc

Gửi và nhận tin nhắn AeroNyx được mã hóa đầu cuối qua HTTPS: ký yêu cầu, hướng dẫn bắt đầu nhanh có thể chạy được, định dạng phong bì đã niêm phong, mã lỗi và các vector kiểm thử.

AeroNyx Chat API cho phép máy chủ, bot hoặc ứng dụng của bạn gửi và nhận tin nhắn một-một được mã hóa đầu cuối với người dùng AeroNyx qua HTTPS thông thường. API này không sử dụng AeroNyx Privacy Network, VPN, định tuyến onion hay hộp thư ẩn danh.

Việc gửi diễn ra tức thì: nếu App của người nhận đang kết nối, tin nhắn của bạn đến ngay. Việc nhận qua API này hoạt động theo kiểu thăm dò (polling): GET /inbox/ trả về những gì đang chờ và không bao giờ giữ yêu cầu mở. Để nhận theo thời gian thực, hãy kết nối cùng danh tính đó vào WebSocket của Chat Relay (wss://api.aeronyx.network/ws/relay/). Xem phần "Phân phối thời gian thực" bên dưới.

URL gốc: https://api.aeronyx.network/api/relay/developer/v1

Cách hoạt động

  1. Tạo danh tính. Một cặp khóa Ed25519 chính là danh tính trò chuyện của bạn. Khóa công khai 32 byte, viết dưới dạng 64 ký tự thập lục phân viết thường, là địa chỉ trò chuyện của bạn. Không cần tài khoản AeroNyx.
  2. Ký mọi yêu cầu. Mỗi lệnh gọi mang một header Authorization được ký bằng khóa của bạn (Xác thực).
  3. Niêm phong từng tin nhắn. Hãy mã hóa và ký tin nhắn ở phía bạn, rồi chỉ gửi phong bì thu được (Phong bì tin nhắn).
  4. Gửi bằng POST /messages/.
  5. Nhận bằng GET /inbox/, giải mã và lưu từng tin nhắn, rồi xác nhận bằng POST /acks/.
EndpointXác thựcMục đích
GET /capabilities/khôngGiới hạn dịch vụ và các tính năng được hỗ trợ
POST /messages/bắt buộcGửi một tin nhắn đã niêm phong
GET /inbox/bắt buộcLấy các tin nhắn chưa được xác nhận
POST /acks/bắt buộcXác nhận các tin nhắn bạn đã lưu

Bắt đầu nhanh

Đoạn script này gửi một tin nhắn từ một danh tính mới sang danh tính mới khác, đọc lại, giải mã và xác nhận tin nhắn đó. Hãy lưu triển khai tham chiếu bên dưới thành aeronyx_chat.py, cài các phụ thuộc bằng pip install pynacl requests, rồi chạy nó.

python
from __future__ import annotations

import hashlib, json, os, time, uuid

import requests
from nacl.signing import SigningKey

from aeronyx_chat import open_, seal

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


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


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

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

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

Xác thực

Mọi endpoint ngoại trừ GET /capabilities/ đều yêu cầu header sau:

text
Authorization: AeroNyx-Chat-V1 <public-key-hex>:<unix-seconds>:<nonce-hex>:<signature-hex>
  • public-key-hex là địa chỉ trò chuyện 64 ký tự của bạn.
  • unix-seconds phải nằm trong khoảng 60 giây so với đồng hồ của máy chủ.
  • nonce-hex là 16 byte ngẫu nhiên, viết dưới dạng 32 ký tự thập lục phân viết thường. Mỗi nonce chỉ được dùng một lần.
  • signature-hex là chữ ký Ed25519, viết dưới dạng 128 ký tự thập lục phân, trên digest 32 byte sau:
text
SHA256(
  "AeroNyx/DeveloperChatRequestV1\0" ||
  UPPERCASE_HTTP_METHOD || "\0" ||
  PATH_WITH_QUERY || "\0" ||
  ASCII_UNIX_SECONDS || "\0" ||
  NONCE_BYTES ||
  SHA256(EXACT_HTTP_BODY_BYTES)
)

PATH_WITH_QUERY là toàn bộ đường dẫn yêu cầu đúng như được gửi, bao gồm tiền tố /api/relay/developer/v1 và mọi chuỗi truy vấn. Ví dụ:

Yêu cầuPATH_WITH_QUERY cần ký
POST https://api.aeronyx.network/api/relay/developer/v1/messages//api/relay/developer/v1/messages/
GET https://api.aeronyx.network/api/relay/developer/v1/inbox/?limit=50/api/relay/developer/v1/inbox/?limit=50

EXACT_HTTP_BODY_BYTES là các byte của phần thân yêu cầu đúng như được gửi. Vì vậy khoảng trắng và thứ tự khóa của JSON đều quan trọng: hãy tuần tự hóa một lần, ký chính các byte đó và gửi chính các byte đó. Yêu cầu không có phần thân sẽ ký SHA-256 của chuỗi rỗng.

Vector kiểm thử xác thực

Hãy dùng vector này để kiểm tra mã ký của bạn trước khi gọi API. Khóa này chỉ dùng để kiểm thử; tuyệt đối không dùng cho tin nhắn thật.

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

Chữ ký Ed25519 mang tính xác định, vì vậy cùng đầu vào phải cho ra chính xác signature_hex này. Nếu body_sha256_hex của bạn khác, nghĩa là các byte phần thân của bạn khác.

Khắc phục sự cố 401

API trả về cùng một lỗi 401 cho mọi lỗi xác thực, vì vậy hãy kiểm tra lần lượt các mục sau:

  1. Đồng hồ: unix-seconds nằm trong khoảng 60 giây so với thời gian thực.
  2. Nonce là mới cho mỗi yêu cầu, dài 16 byte, viết dưới dạng 32 ký tự thập lục phân viết thường.
  3. Đường dẫn được ký bao gồm /api/relay/developer/v1 và chuỗi truy vấn.
  4. Phương thức được ký viết hoa và khớp với yêu cầu.
  5. Các byte phần thân được ký chính là các byte bạn gửi. Không tuần tự hóa lại JSON sau khi ký.
  6. Digest và chữ ký của bạn khớp với vector kiểm thử ở trên.

Các endpoint

Thông tin dịch vụ

GET /capabilities/ không cần xác thực và trả về các giới hạn hiện hành cùng các tính năng được hỗ trợ.

Gửi tin nhắn

POST /messages/

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

Cả năm trường đều bắt buộc và không được có trường nào khác.

  • receiver_pubkey là địa chỉ trò chuyện của người nhận. Nó phải khác địa chỉ của chính bạn.
  • message_id là một UUID. Hãy dùng một UUID ngẫu nhiên mới cho mỗi tin nhắn và giữ nguyên phần thân để thử lại. Gửi cùng một message_id với các trường khác nhau sẽ trả về 409 idempotency_conflict.
  • discriminant là 11 đối với tin nhắn một-một mà người dùng AeroNyx App cần nhìn thấy.
  • sent_at là giây Unix và phải nằm trong khoảng 300 giây so với thời gian của máy chủ.
  • payload_b64 là phong bì đã niêm phong được mô tả trong Phong bì tin nhắn.

Relay chỉ kiểm tra rằng payload_b64 là Base64 hợp lệ có độ dài từ 1 byte đến 191 KiB. Nó không thể phân biệt phong bì với dữ liệu khác, nên một payload không phải phong bì hợp lệ vẫn được chấp nhận ở bước này và bị App của người nhận lặng lẽ loại bỏ. Hãy kiểm thử phong bì của bạn với vector chuẩn.

Yêu cầu thành công trả về HTTP 202:

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

accepted nghĩa là relay đã lưu bản mã một cách bền vững. delivered_live là true khi relay đã chuyển tin nhắn sang phân phối trực tiếp mà không gặp lỗi. Giá trị này vẫn là true ngay cả khi người nhận không mở kết nối nào, nên nó không cho biết có ai đang trực tuyến hay không. Không trường nào cho biết người nhận đã giải mã hoặc hiển thị tin nhắn; điều đó được thể hiện bằng biên nhận phân phối trong hộp thư đến của bạn (xem "Sự kiện trong hộp thư đến").

Lấy hộp thư đến

GET /inbox/?limit=50

Trả về tối đa 100 sự kiện chưa được xác nhận. Cùng một sự kiện có thể xuất hiện lại sau khi hết thời gian chờ hoặc sau khi khởi động lại.

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

Nếu has_more là true, hãy xác nhận những gì bạn đã lưu rồi lấy tiếp.

Sự kiện trong hộp thư đến

Hộp thư đến chứa mọi thứ relay đã xếp hàng cho danh tính của bạn, không chỉ tin nhắn mới. Mục không có control_type là tin nhắn: mở payload_b64 như mô tả trong "Phong bì tin nhắn". Mục có control_type là một sự kiện liên quan đến một tin nhắn. Hãy kiểm tra control_type trước khi giải mã bất cứ thứ gì: biên nhận không có payload_b64.

control_typeÝ nghĩamsg_id
message_receiptApp của người nhận đã giải mã và lưu tin nhắn bạn gửi.message_id của tin nhắn của bạn
message_readNgười nhận đã đọc tin nhắn của bạn. Chỉ gửi khi họ bật biên nhận đã đọc.message_id của tin nhắn của bạn
message_editNgười gửi đã sửa một tin nhắn (target_msg_id, cùng ID).ID của tin nhắn được sửa
message_revokeNgười gửi đã thu hồi một tin nhắn.ID của tin nhắn bị thu hồi
message_reactionMột phản ứng với target_msg_id đã được thêm hoặc gỡ.ID của phản ứng

Các lần sửa, thu hồi và phản ứng đều được ký (payload_sig); định dạng của chúng được mô tả trong Tích hợp máy khách AeroNyx Chat Relay. Xác nhận một ID sẽ xóa mọi mục trong hàng đợi mang ID đó, vì vậy hãy xử lý hết một trang rồi mới xác nhận. Hãy xác nhận cả những loại sự kiện bạn không xử lý; nếu không, chúng sẽ được trả về lần nữa.

Xác nhận tin nhắn

POST /acks/

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

Chỉ xác nhận một tin nhắn sau khi bạn đã xác thực, giải mã và ghi nó vào bộ lưu trữ bền vững. Các ID không xác định sẽ không có trong danh sách acknowledged của phản hồi.

Mô hình phân phối

  • Phân phối ít nhất một lần. Loại bỏ trùng lặp theo control_type kết hợp với msg_id: biên nhận mang ID của tin nhắn mà nó xác nhận.
  • Bản mã chưa được phân phối được giữ trong 72 giờ. Đừng coi dịch vụ là nơi lưu lịch sử vĩnh viễn.
  • Hãy thử lại một lần gửi thất bại với chính xác cùng phần thân và một nonce xác thực mới.

Phân phối thời gian thực

Gửi

POST /messages/ chuyển tin nhắn của bạn đến mọi kết nối mà người nhận đang mở, đồng thời giữ nó trong hàng đợi cho đến khi App của người nhận xác nhận. Người nhận có App đang kết nối sẽ nhận ngay; người nhận ngoại tuyến sẽ nhận khi App kết nối lần tới, trong vòng 72 giờ.

Tin nhắn gửi qua API này không kích hoạt thông báo đẩy. Người nhận đang đóng App sẽ không được báo và sẽ thấy tin nhắn khi mở App lần tới.

Nhận bằng thăm dò

GET /inbox/ trả về ngay, dù có gì đang chờ hay không. Tin nhắn chỉ đến với bạn ở lần gọi tiếp theo, nên độ trễ bạn thấy chính là khoảng thời gian thăm dò. Với hội thoại, hãy nhận qua WebSocket.

Nhận theo thời gian thực

Kết nối cùng danh tính vào WebSocket của Chat Relay. Không cần tài khoản hay đăng ký.

  1. Mở wss://api.aeronyx.network/ws/relay/ và đăng nhập bằng cùng khóa Ed25519, như mô tả ở phần đăng nhập trong Tích hợp máy khách AeroNyx Chat Relay.
  2. Sau khi đăng nhập, relay trước tiên gửi lại mọi thứ còn trong hàng đợi của bạn (from_offline là true), rồi gửi từng mục mới ngay khi nó đến. Tin nhắn đến dưới dạng khung relay_envelope với các trường giống tin nhắn trong hộp thư đến; biên nhận, lần sửa, thu hồi và phản ứng đến dưới dạng khung có type là control_type trong bảng trên.
  3. Sau khi đã lưu một mục, hãy xác nhận nó:
json
{"type": "relay_offline_ack", "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"}

WebSocket và GET /inbox/ đọc cùng một hàng đợi: mục đã xác nhận ở bên này cũng biến mất ở bên kia, và POST /acks/ dùng được cho cả hai. Bạn có thể tiếp tục gửi qua HTTPS trong khi nhận qua WebSocket.

Giới hạn và lỗi

Giới hạnGiá trị
Payload đã niêm phong191 KiB (yêu cầu JSON chứa Base64 vẫn nằm dưới giới hạn yêu cầu)
Phần thân yêu cầu256 KiB
Một trang hộp thư đến và một lô xác nhận100 mục
Thời gian lưu trữ ngoại tuyến72 giờ
Trạng tháiÝ nghĩa
400 invalid_jsonPhần thân không phải là một đối tượng JSON.
400 invalid_requestPhần thân không chứa đúng năm trường, receiver_pubkey không phải 64 ký tự thập lục phân viết thường hoặc trùng với địa chỉ của bạn, discriminant nằm ngoài khoảng 1-255, message_id không phải UUID, hoặc payload_b64 không phải Base64 hợp lệ.
400 invalid_payloadPayload sau khi giải mã bị rỗng hoặc lớn hơn 191 KiB.
400 message_expiredsent_at chênh lệch so với thời gian máy chủ hơn 300 giây.
401Thiếu xác thực, xác thực đã hết hạn, không hợp lệ hoặc bị phát lại. Xem Khắc phục sự cố 401.
403Chính sách của người nhận đã từ chối người gửi này.
409Xung đột idempotency, hoặc một yêu cầu giống hệt vẫn đang được xử lý.
429Đã đạt giới hạn tốc độ của bên gửi: 50 yêu cầu mỗi giây và 100.000 yêu cầu mỗi ngày UTC cho mỗi danh tính. Hãy chờ khoảng thời gian trong tiêu đề Retry-After.
503Việc phân phối bền vững hoặc xác thực tạm thời không khả dụng. Hãy thử lại đúng phần thân đó với một nonce mới.

Những gì hướng dẫn này không bao gồm

Hướng dẫn này chỉ bao gồm tin nhắn một-một qua HTTPS. Với mọi thứ khác, hãy kết nối cùng danh tính đó vào WebSocket của trạm chuyển tiếp trò chuyện và xem Tích hợp máy khách AeroNyx Chat Relay:

  • Tin nhắn nhóm, trạng thái hiện diện và chỉ báo đang nhập.
  • Tệp đính kèm được mã hóa, gồm cả tin nhắn thoại và tệp video.
  • Cuộc gọi thoại và video. Các khung báo hiệu được liệt kê trong bài đó, nhưng hướng dẫn tích hợp cuộc gọi đầy đủ (token phương tiện, chữ ký lời mời và mã hóa cuộc gọi) vẫn chưa được công bố.

Quyền riêng tư và khả năng hiển thị

Nội dung tin nhắn được mã hóa đầu cuối: nó được niêm phong ở phía bạn bằng một khóa dẫn xuất từ danh tính của người gửi và người nhận, khóa mà AeroNyx không bao giờ nắm giữ, và API chỉ lưu và chuyển tiếp bản mã. Để phân phối tin nhắn, dịch vụ có thể thấy khóa công khai của người gửi và người nhận, thời gian tin nhắn, kích thước, trạng thái phân phối và địa chỉ IP của mỗi yêu cầu. HTTPS không che giấu siêu dữ liệu này với dịch vụ.

Phong bì tin nhắn

Các tin nhắn mà người dùng AeroNyx App cần nhìn thấy dùng discriminant: 11 và một phong bì được niêm phong đầu cuối trong payload_b64. Relay không bao giờ thấy khóa hay văn bản gốc. Mọi số nguyên đều theo thứ tự byte little-endian.

1. Khóa chung

Cả hai bên dẫn xuất cùng một khóa 32 byte từ danh tính trò chuyện Ed25519 của mình:

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

Với libsodium, đó là crypto_sign_ed25519_sk_to_curve25519, crypto_sign_ed25519_pk_to_curve25519 và crypto_scalarmult, theo sau là HKDF. Người gửi luôn dùng khóa HKDF. Các phiên bản App rất cũ đã gửi trực tiếp bằng raw, nên người nhận có thể thử raw nếu khóa HKDF không giải mã được.

2. Văn bản gốc

Tin nhắn văn bản chính là văn bản UTF-8 của nó. Các tin nhắn từ App có đính kèm tệp, trả lời hoặc chuyển tiếp là một đối tượng JSON có "type": "aeronyx_message" và một trường text; hãy hiển thị text nếu bạn không hỗ trợ phần còn lại.

3. Mã hóa

XChaCha20-Poly1305 với nonce ngẫu nhiên mới dài 24 byte và không có dữ liệu liên kết. ciphertext bên dưới là đầu ra của thuật toán mã hóa, bao gồm thẻ xác thực 16 byte.

4. Chữ ký

Người gửi ký 121 byte sau bằng Ed25519:

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

5. Phong bì

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

payload_b64 là Base64 chuẩn của các byte này. Lưu ý rằng content_type dài 1 byte trong dữ liệu được ký nhưng dài 4 byte trong phong bì.

Nhận tin

Phân tích phong bì, kiểm tra receiver_pubkey là danh tính của bạn, xác minh chữ ký với sender_pubkey của phong bì, rồi giải mã bằng khóa dẫn xuất từ người gửi đó. Hãy loại bỏ tin nhắn nếu bất kỳ bước nào thất bại. App loại bỏ các tin nhắn trực tiếp có timestamp lệch so với đồng hồ của nó hơn 300 giây.

Triển khai tham chiếu (Python)

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

from __future__ import annotations

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

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

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


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


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


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


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


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

Vector chuẩn

Khóa, nonce, ID và thời gian cố định, chỉ dùng để kiểm thử. Tuyệt đối không dùng các khóa này.

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

seal(alice, bob_public, "Hello from AeroNyx", message_id, 1780000000, nonce=bytes(range(24))) phải tạo ra chính xác payload_b64 này, và open_(bob, payload_b64) phải trả về văn bản gốc. Vector này đã được kiểm chứng với cơ chế thỏa thuận khóa, bộ ký và thuật toán mã hóa của chính AeroNyx App, và với bộ giải mã phong bì nghiêm ngặt của node.

<!-- faq:start -->

Câu hỏi thường gặp

AeroNyx Chat API là gì?

Đây là một API HTTPS có phiên bản dùng để trao đổi tin nhắn một-một được mã hóa đầu cuối với người dùng AeroNyx từ máy chủ, bot hoặc ứng dụng. API có bốn endpoint: capabilities, gửi, hộp thư đến và xác nhận. Nó chỉ truyền tải các phong bì đã niêm phong; nội dung được mã hóa ở phía bạn.

Tôi có cần tài khoản AeroNyx để dùng API không?

Không. Một cặp khóa Ed25519 chính là danh tính. Hãy tạo nó cục bộ và dùng nó để ký từng yêu cầu; khóa công khai là địa chỉ trò chuyện của bạn.

Tôi ký /messages/ hay toàn bộ đường dẫn?

Toàn bộ đường dẫn đúng như được gửi, bắt đầu bằng /api/relay/developer/v1, cộng với chuỗi truy vấn nếu có. Chỉ ký /messages/ sẽ gây ra lỗi 401. Vector kiểm thử xác thực trên trang này cho thấy chính xác các đầu vào.

Tại sao tôi nhận được 401?

Mọi lỗi xác thực đều trả về cùng một 401. Các nguyên nhân thường gặp là đồng hồ lệch hơn 60 giây, dùng lại nonce, đường dẫn hoặc phần thân được ký khác với những gì đã gửi, hoặc phương thức HTTP viết thường trong dữ liệu được ký. Hãy làm theo danh sách kiểm tra trong phần Khắc phục sự cố 401 và so sánh digest của bạn với vector kiểm thử.

Tại sao API trả về 202 nhưng tin nhắn không bao giờ xuất hiện trong AeroNyx App?

Relay chấp nhận bất kỳ payload Base64 hợp lệ nào, nhưng App chỉ hiển thị một phong bì đã niêm phong mà nó có thể xác minh và giải mã. Hãy kiểm tra payload_b64 của bạn với vector chuẩn trên trang này, đảm bảo receiver_pubkey của phong bì là người nhận, và giữ dấu thời gian của phong bì trong khoảng 300 giây so với thời gian hiện tại. Gửi văn bản thuần được mã hóa thành Base64 sẽ được chấp nhận nhưng không bao giờ được hiển thị.

Tại sao API trả về invalid_request?

Các nguyên nhân phổ biến nhất là gửi đến chính khóa công khai của bạn, phần thân không chứa đúng năm trường đã được tài liệu hóa, message_id không phải UUID, hoặc discriminant nằm ngoài khoảng 1-255. Xem bảng lỗi trong phần Giới hạn và lỗi.

AeroNyx có đọc được tin nhắn gửi qua API này không?

Không. Các phong bì được mã hóa bằng một khóa dẫn xuất từ danh tính của người gửi và người nhận, khóa mà AeroNyx không bao giờ nắm giữ. Dịch vụ có thể thấy siêu dữ liệu phân phối như khóa công khai, thời gian, kích thước và địa chỉ IP.

Tin nhắn chưa được phân phối được giữ trong bao lâu?

Tối đa 72 giờ. Chỉ xác nhận mỗi tin nhắn sau khi bạn đã giải mã và lưu nó; các tin nhắn chưa được xác nhận có thể được phân phối lại.

Tôi có thể dùng những ngôn ngữ lập trình nào?

Bất kỳ ngôn ngữ nào có Ed25519, X25519, HKDF-SHA256 và XChaCha20-Poly1305, bao gồm các binding libsodium cho hầu hết các ngôn ngữ. Trang này có một triển khai tham chiếu Python hoàn chỉnh và một hướng dẫn bắt đầu nhanh có thể chạy được.

AeroNyx Chat API có phải là thời gian thực không?

Việc gửi thì có: người nhận có App đang kết nối nhận tin nhắn của bạn ngay. Việc nhận qua HTTPS là thăm dò, nên bạn thấy tin nhắn mới ở lần gọi GET /inbox/ tiếp theo. Để nhận theo thời gian thực, hãy kết nối cùng danh tính vào WebSocket của Chat Relay tại wss://api.aeronyx.network/ws/relay/; nó đọc cùng một hàng đợi.

Người dùng AeroNyx có nhận được thông báo cho tin nhắn tôi gửi không?

Hiện tại thì không. Tin nhắn gửi qua API này không kích hoạt thông báo đẩy, nên người dùng đang đóng App sẽ thấy tin nhắn của bạn khi mở App lần tới. Người dùng có App đang kết nối sẽ thấy ngay.

<!-- faq:end -->