Hướng dẫn tích hợp AeroNyx Chat API
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
- 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.
- 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). - 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).
- Gửi bằng
POST /messages/. - Nhận bằng
GET /inbox/, giải mã và lưu từng tin nhắn, rồi xác nhận bằngPOST /acks/.
| Endpoint | Xác thực | Mục đích |
|---|---|---|
GET /capabilities/ | không | Giới hạn dịch vụ và các tính năng được hỗ trợ |
POST /messages/ | bắt buộc | Gửi một tin nhắn đã niêm phong |
GET /inbox/ | bắt buộc | Lấy các tin nhắn chưa được xác nhận |
POST /acks/ | bắt buộc | Xá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ó.
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:
Authorization: AeroNyx-Chat-V1 <public-key-hex>:<unix-seconds>:<nonce-hex>:<signature-hex>
public-key-hexlà địa chỉ trò chuyện 64 ký tự của bạn.unix-secondsphải nằm trong khoảng 60 giây so với đồng hồ của máy chủ.nonce-hexlà 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-hexlà chữ ký Ed25519, viết dưới dạng 128 ký tự thập lục phân, trên digest 32 byte sau:
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ầu | PATH_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.
{
"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:
- Đồng hồ:
unix-secondsnằm trong khoảng 60 giây so với thời gian thực. - 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.
- Đường dẫn được ký bao gồm
/api/relay/developer/v1và chuỗi truy vấn. - Phương thức được ký viết hoa và khớp với yêu cầu.
- 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ý.
- 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/
{
"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_pubkeylà đị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_idlà 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ộtmessage_idvới các trường khác nhau sẽ trả về409 idempotency_conflict.discriminantlà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_atlà 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_b64là 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:
{
"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.
{
"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ĩa | msg_id |
|---|---|---|
message_receipt | App 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_read | Ngườ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_edit | Ngườ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_revoke | Người gửi đã thu hồi một tin nhắn. | ID của tin nhắn bị thu hồi |
message_reaction | Mộ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/
{
"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_typekết hợp vớimsg_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ý.
- 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. - 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_offlinelàtrue), rồi gửi từng mục mới ngay khi nó đến. Tin nhắn đến dưới dạng khungrelay_envelopevớ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ótypelàcontrol_typetrong bảng trên. - Sau khi đã lưu một mục, hãy xác nhận nó:
{"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ạn | Giá trị |
|---|---|
| Payload đã niêm phong | 191 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ầu | 256 KiB |
| Một trang hộp thư đến và một lô xác nhận | 100 mục |
| Thời gian lưu trữ ngoại tuyến | 72 giờ |
| Trạng thái | Ý nghĩa |
|---|---|
400 invalid_json | Phần thân không phải là một đối tượng JSON. |
400 invalid_request | Phầ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_payload | Payload sau khi giải mã bị rỗng hoặc lớn hơn 191 KiB. |
400 message_expired | sent_at chênh lệch so với thời gian máy chủ hơn 300 giây. |
401 | Thiế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. |
403 | Chính sách của người nhận đã từ chối người gửi này. |
409 | Xung độ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. |
503 | Việ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:
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:
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ì
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)
"""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.
{
"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.
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 -->