Panduan Integrasi AeroNyx Chat API

12 menit baca

Kirim dan terima pesan AeroNyx terenkripsi ujung ke ujung melalui HTTPS: penandatanganan permintaan, mulai cepat yang dapat dijalankan, format amplop tersegel, kode kesalahan, dan vektor uji.

AeroNyx Chat API memungkinkan server, bot, atau aplikasi mengirim dan menerima pesan satu-ke-satu terenkripsi ujung ke ujung dengan pengguna AeroNyx melalui HTTPS biasa. API ini tidak menggunakan AeroNyx Privacy Network, VPN, onion routing, atau kotak surat anonim.

Pengiriman berlangsung seketika: jika App penerima sedang terhubung, pesan Anda langsung sampai. Penerimaan melalui API ini bekerja dengan polling: GET /inbox/ mengembalikan apa pun yang sedang menunggu dan tidak pernah menahan permintaan tetap terbuka. Untuk menerima secara real-time, hubungkan identitas yang sama ke WebSocket Chat Relay (wss://api.aeronyx.network/ws/relay/). Lihat bagian "Pengiriman real-time" di bawah.

URL dasar: https://api.aeronyx.network/api/relay/developer/v1

Cara kerjanya

  1. Buat identitas. Pasangan kunci Ed25519 adalah identitas chat Anda. Kunci publik 32 byte, yang ditulis sebagai 64 karakter heksadesimal huruf kecil, adalah alamat chat Anda. Akun AeroNyx tidak diperlukan.
  2. Tandatangani setiap permintaan. Setiap panggilan membawa header Authorization yang ditandatangani dengan kunci Anda (Autentikasi).
  3. Segel setiap pesan. Enkripsi dan tandatangani pesan di sisi Anda, lalu kirim hanya amplop yang dihasilkan (Amplop pesan).
  4. Kirim dengan POST /messages/.
  5. Terima dengan GET /inbox/, dekripsi dan simpan setiap pesan, lalu konfirmasikan dengan POST /acks/.
EndpointAutentikasiTujuan
GET /capabilities/tidak adaBatas layanan dan fitur yang didukung
POST /messages/wajibMengirim pesan tersegel
GET /inbox/wajibMengambil pesan yang belum dikonfirmasi
POST /acks/wajibMengonfirmasi pesan yang sudah Anda simpan

Mulai cepat

Skrip ini mengirim pesan dari satu identitas baru ke identitas lain, membacanya kembali, mendekripsinya, lalu mengonfirmasinya. Simpan implementasi referensi di bawah ini sebagai aeronyx_chat.py, pasang dependensinya dengan pip install pynacl requests, lalu jalankan.

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

Autentikasi

Setiap endpoint kecuali GET /capabilities/ memerlukan header ini:

text
Authorization: AeroNyx-Chat-V1 <public-key-hex>:<unix-seconds>:<nonce-hex>:<signature-hex>
  • public-key-hex adalah alamat chat Anda yang terdiri dari 64 karakter.
  • unix-seconds harus berada dalam selisih 60 detik dari jam server.
  • nonce-hex adalah 16 byte acak yang ditulis sebagai 32 karakter heksadesimal huruf kecil. Setiap nonce hanya dapat dipakai sekali.
  • signature-hex adalah tanda tangan Ed25519, ditulis sebagai 128 karakter heksadesimal, atas digest 32 byte berikut:
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 adalah path permintaan lengkap sebagaimana dikirim, termasuk awalan /api/relay/developer/v1 dan string kueri apa pun. Contoh:

PermintaanPATH_WITH_QUERY yang ditandatangani
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 adalah byte isi permintaan persis seperti yang dikirim. Karena itu spasi putih JSON dan urutan kunci berpengaruh: serialisasikan sekali, tandatangani byte tersebut, dan kirim byte yang sama. Permintaan tanpa isi menandatangani SHA-256 dari string kosong.

Vektor uji autentikasi

Gunakan ini untuk memeriksa kode penandatanganan Anda sebelum memanggil API. Kunci ini hanya untuk pengujian; jangan pernah memakainya untuk pesan sungguhan.

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

Tanda tangan Ed25519 bersifat deterministik, sehingga masukan yang sama harus menghasilkan signature_hex persis seperti ini. Jika body_sha256_hex Anda berbeda, berarti byte isi Anda berbeda.

Pemecahan masalah 401

API menjawab setiap kegagalan autentikasi dengan 401 yang sama, jadi periksa hal-hal berikut secara berurutan:

  1. Jam: unix-seconds berada dalam selisih 60 detik dari waktu sebenarnya.
  2. Nonce baru untuk setiap permintaan, sepanjang 16 byte, ditulis sebagai 32 karakter heksadesimal huruf kecil.
  3. Path yang ditandatangani menyertakan /api/relay/developer/v1 dan string kueri.
  4. Metode yang ditandatangani menggunakan huruf kapital dan cocok dengan permintaan.
  5. Byte isi yang ditandatangani adalah byte yang Anda kirim. Jangan serialisasikan ulang JSON setelah penandatanganan.
  6. Digest dan tanda tangan Anda cocok dengan vektor uji di atas.

Endpoint

Informasi layanan

GET /capabilities/ tidak memerlukan autentikasi dan mengembalikan batas saat ini serta fitur yang didukung.

Mengirim pesan

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

Kelima kolom wajib diisi dan tidak ada kolom lain yang diizinkan.

  • receiver_pubkey adalah alamat chat penerima. Nilainya harus berbeda dari alamat Anda sendiri.
  • message_id adalah UUID. Gunakan UUID acak baru untuk setiap pesan dan simpan isi permintaan yang persis sama untuk percobaan ulang. Mengirim message_id yang sama dengan kolom yang berbeda mengembalikan 409 idempotency_conflict.
  • discriminant bernilai 11 untuk pesan satu-ke-satu yang harus dilihat pengguna AeroNyx App.
  • sent_at adalah detik Unix dan harus berada dalam selisih 300 detik dari waktu server.
  • payload_b64 adalah amplop tersegel yang dijelaskan di Amplop pesan.

Relay hanya memeriksa bahwa payload_b64 adalah Base64 yang valid berukuran 1 byte hingga 191 KiB. Relay tidak dapat membedakan amplop dari data lain, sehingga payload yang bukan amplop valid diterima di sini dan dibuang secara diam-diam oleh App penerima. Uji amplop Anda terhadap vektor emas.

Permintaan yang berhasil mengembalikan HTTP 202:

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

accepted berarti relay telah menyimpan ciphertext secara permanen. delivered_live bernilai true ketika relay telah menyerahkan pesan ke pengiriman langsung tanpa galat. Nilainya tetap true meskipun penerima tidak memiliki koneksi terbuka, jadi field ini tidak memberi tahu apakah seseorang sedang online. Tidak satu pun dari kedua field berarti penerima telah mendekripsi atau menampilkan pesan; yang menunjukkan hal itu adalah tanda terima pengiriman di kotak masuk Anda (lihat "Peristiwa di kotak masuk").

Mengambil kotak masuk

GET /inbox/?limit=50

Mengembalikan hingga 100 peristiwa yang belum dikonfirmasi. Peristiwa yang sama dapat muncul lagi setelah batas waktu habis atau setelah restart.

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

Jika has_more bernilai true, konfirmasikan pesan yang sudah Anda simpan lalu ambil lagi.

Peristiwa di kotak masuk

Kotak masuk berisi semua yang telah diantrekan relay untuk identitas Anda, bukan hanya pesan baru. Item tanpa control_type adalah pesan: buka payload_b64 seperti dijelaskan di "Amplop pesan". Item dengan control_type adalah peristiwa tentang sebuah pesan. Periksa control_type sebelum mendekripsi apa pun: tanda terima tidak memiliki payload_b64.

control_typeArtimsg_id
message_receiptApp penerima telah mendekripsi dan menyimpan pesan yang Anda kirim.message_id pesan Anda
message_readPenerima telah membaca pesan Anda. Hanya dikirim jika tanda terima baca mereka aktif.message_id pesan Anda
message_editPengirim mengedit sebuah pesan (target_msg_id, ID yang sama).ID pesan yang diedit
message_revokePengirim menarik sebuah pesan.ID pesan yang ditarik
message_reactionReaksi terhadap target_msg_id ditambahkan atau dihapus.ID reaksi

Edit, penarikan, dan reaksi ditandatangani (payload_sig); formatnya dijelaskan di Integrasi Klien AeroNyx Chat Relay. Mengonfirmasi sebuah ID menghapus semua item antrean yang membawanya, jadi proses satu halaman penuh sebelum mengonfirmasi. Konfirmasi juga jenis peristiwa yang tidak Anda tangani; jika tidak, item tersebut akan dikembalikan lagi.

Mengonfirmasi pesan

POST /acks/

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

Konfirmasikan sebuah pesan hanya setelah Anda memvalidasinya, mendekripsinya, dan menuliskannya ke penyimpanan yang tahan lama. ID yang tidak dikenal tidak dimasukkan ke dalam daftar acknowledged pada respons.

Model pengiriman

  • Pengiriman bersifat setidaknya sekali. Hilangkan duplikat berdasarkan control_type bersama msg_id: tanda terima membawa ID pesan yang dikonfirmasinya.
  • Ciphertext yang belum terkirim disimpan selama 72 jam. Jangan anggap layanan ini sebagai riwayat permanen.
  • Ulangi pengiriman yang gagal dengan isi permintaan yang persis sama dan nonce autentikasi yang baru.

Pengiriman real-time

Mengirim

POST /messages/ menyerahkan pesan Anda ke setiap koneksi yang sedang dibuka penerima dan, pada saat yang sama, menyimpannya dalam antrean sampai App penerima mengonfirmasinya. Penerima yang App-nya terhubung langsung menerimanya; penerima yang offline menerimanya saat App terhubung berikutnya, dalam 72 jam.

Pesan yang dikirim melalui API ini tidak memicu notifikasi push. Penerima yang App-nya tertutup tidak diberi tahu dan akan melihat pesan saat membuka App berikutnya.

Menerima dengan polling

GET /inbox/ langsung kembali, ada atau tidak ada yang menunggu. Pesan baru sampai kepada Anda pada panggilan berikutnya, jadi jeda yang Anda lihat sama dengan interval polling Anda. Untuk percakapan, terimalah melalui WebSocket.

Menerima secara real-time

Hubungkan identitas yang sama ke WebSocket Chat Relay. Tidak perlu akun atau pendaftaran.

  1. Buka wss://api.aeronyx.network/ws/relay/ dan login dengan kunci Ed25519 yang sama, seperti dijelaskan di bagian login pada Integrasi Klien AeroNyx Chat Relay.
  2. Setelah login, relay terlebih dahulu mengirim ulang semua yang masih antre untuk Anda (from_offline bernilai true), lalu mengirim setiap item baru segera setelah tiba. Pesan tiba sebagai frame relay_envelope dengan field yang sama seperti pesan di kotak masuk; tanda terima, edit, penarikan, dan reaksi tiba sebagai frame dengan type berupa control_type pada tabel di atas.
  3. Setelah menyimpan sebuah item, konfirmasikan:
json
{"type": "relay_offline_ack", "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"}

WebSocket dan GET /inbox/ membaca antrean yang sama: item yang dikonfirmasi di satu sisi hilang dari sisi lain, dan POST /acks/ berlaku untuk keduanya. Anda dapat tetap mengirim melalui HTTPS sambil menerima melalui WebSocket.

Batas dan kesalahan

BatasNilai
Payload tersegel191 KiB (permintaan JSON Base64 tetap di bawah batas permintaan)
Isi permintaan256 KiB
Halaman kotak masuk dan kumpulan konfirmasi100 item
Retensi offline72 jam
StatusArti
400 invalid_jsonIsi permintaan bukan objek JSON.
400 invalid_requestIsi permintaan tidak memuat tepat lima kolom, receiver_pubkey bukan 64 karakter heksadesimal huruf kecil atau sama dengan alamat Anda sendiri, discriminant di luar rentang 1-255, message_id bukan UUID, atau payload_b64 bukan Base64 yang valid.
400 invalid_payloadPayload yang didekode kosong atau lebih besar dari 191 KiB.
400 message_expiredsent_at berbeda dari waktu server lebih dari 300 detik.
401Autentikasi tidak ada, kedaluwarsa, tidak valid, atau diputar ulang. Lihat Pemecahan masalah 401.
403Kebijakan penerima menolak pengirim ini.
409Konflik idempotensi, atau permintaan yang identik masih diproses.
429Batas laju pengirim tercapai: 50 permintaan per detik dan 100.000 per hari UTC untuk setiap identitas. Tunggu selama waktu yang ditunjukkan di header Retry-After.
503Pengiriman tahan lama atau autentikasi untuk sementara tidak tersedia. Ulangi dengan isi permintaan yang persis sama dan nonce baru.

Yang tidak dicakup panduan ini

Panduan ini mencakup pesan satu lawan satu melalui HTTPS. Untuk hal lain, hubungkan identitas yang sama ke WebSocket relay chat dan lihat Integrasi Klien AeroNyx Chat Relay:

  • Pesan grup, status kehadiran, dan indikator mengetik.
  • Lampiran terenkripsi, termasuk pesan suara dan berkas video.
  • Panggilan suara dan video. Frame sinyal tercantum di artikel tersebut, tetapi panduan integrasi panggilan yang lengkap (token media, tanda tangan undangan, dan enkripsi panggilan) belum dipublikasikan.

Privasi dan visibilitas

Isi pesan dienkripsi ujung ke ujung: pesan disegel di sisi Anda dengan kunci yang diturunkan dari identitas pengirim dan penerima, yang tidak pernah dipegang AeroNyx, dan API hanya menyimpan serta meneruskan ciphertext. Untuk mengirimkan pesan, layanan dapat melihat kunci publik pengirim dan penerima, waktu pesan, ukuran, status pengiriman, dan alamat IP setiap permintaan. HTTPS tidak menyembunyikan metadata ini dari layanan.

Amplop pesan

Pesan yang harus dilihat pengguna AeroNyx App menggunakan discriminant: 11 dan amplop tersegel ujung ke ujung di payload_b64. Relay tidak pernah melihat kunci maupun plaintext. Semua bilangan bulat berformat little-endian.

1. Kunci bersama

Kedua pihak menurunkan kunci 32 byte yang sama dari identitas chat Ed25519 mereka:

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)

Dengan libsodium, ini adalah crypto_sign_ed25519_sk_to_curve25519, crypto_sign_ed25519_pk_to_curve25519, dan crypto_scalarmult, diikuti HKDF. Pengirim selalu menggunakan kunci HKDF. Versi App yang sangat lama mengirim langsung dengan raw, sehingga penerima dapat mencoba raw jika kunci HKDF gagal mendekripsi.

2. Plaintext

Pesan teks adalah teks UTF-8-nya. Pesan dari App yang membawa lampiran, balasan, atau teruskan berupa objek JSON dengan "type": "aeronyx_message" dan kolom text; tampilkan text jika Anda tidak mendukung bagian lainnya.

3. Enkripsi

XChaCha20-Poly1305 dengan nonce acak baru sepanjang 24 byte dan tanpa data terkait. ciphertext di bawah adalah keluaran cipher termasuk tag 16 byte-nya.

4. Tanda tangan

Pengirim menandatangani 121 byte berikut dengan 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. Amplop

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 adalah Base64 standar dari byte-byte ini. Perhatikan bahwa content_type berukuran 1 byte di dalam data yang ditandatangani tetapi 4 byte di dalam amplop.

Menerima

Urai amplop, periksa bahwa receiver_pubkey adalah identitas Anda, verifikasi tanda tangan terhadap sender_pubkey pada amplop, lalu dekripsi dengan kunci yang diturunkan dari pengirim tersebut. Buang pesan jika ada langkah yang gagal. App membuang pesan langsung yang timestamp-nya berselisih lebih dari 300 detik dari jamnya.

Implementasi referensi (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"),
    }

Vektor emas

Kunci, nonce, ID, dan waktu yang tetap, hanya untuk pengujian. Jangan pernah memakai kunci-kunci ini.

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))) harus menghasilkan payload_b64 persis seperti ini, dan open_(bob, payload_b64) harus mengembalikan plaintext. Vektor ini telah diverifikasi terhadap kesepakatan kunci, penanda tangan, dan cipher milik AeroNyx App sendiri, serta terhadap dekoder amplop ketat milik node.

<!-- faq:start -->

Pertanyaan yang sering diajukan

Apa itu AeroNyx Chat API?

API HTTPS berversi untuk bertukar pesan satu-ke-satu terenkripsi ujung ke ujung dengan pengguna AeroNyx dari server, bot, atau aplikasi. API ini memiliki empat endpoint: capabilities, kirim, kotak masuk, dan konfirmasi. API ini hanya membawa amplop tersegel; isinya dienkripsi di sisi Anda.

Apakah saya memerlukan akun AeroNyx untuk menggunakan API ini?

Tidak. Pasangan kunci Ed25519 adalah identitasnya. Buat secara lokal dan tandatangani setiap permintaan dengannya; kunci publik adalah alamat chat Anda.

Apakah saya menandatangani /messages/ atau path lengkap?

Path lengkap sebagaimana dikirim, dimulai dengan /api/relay/developer/v1, ditambah string kueri jika ada. Menandatangani hanya /messages/ menghasilkan 401. Vektor uji autentikasi di halaman ini menunjukkan masukan yang tepat.

Mengapa saya mendapat 401?

Setiap kegagalan autentikasi mengembalikan 401 yang sama. Penyebab yang umum adalah jam yang selisihnya lebih dari 60 detik, nonce yang dipakai ulang, path atau isi permintaan yang ditandatangani berbeda dari yang dikirim, atau metode HTTP huruf kecil pada data yang ditandatangani. Telusuri daftar periksa pada Pemecahan masalah 401 dan bandingkan digest Anda dengan vektor uji.

Mengapa API mengembalikan 202 tetapi pesan tidak pernah muncul di AeroNyx App?

Relay menerima payload Base64 yang valid apa pun, tetapi App hanya menampilkan amplop tersegel yang dapat diverifikasi dan didekripsinya. Periksa payload_b64 Anda terhadap vektor emas di halaman ini, pastikan receiver_pubkey pada amplop adalah penerima, dan jaga agar stempel waktu amplop berada dalam selisih 300 detik dari waktu saat ini. Mengirim teks biasa yang disandikan sebagai Base64 diterima tetapi tidak pernah ditampilkan.

Mengapa API mengembalikan invalid_request?

Penyebab yang paling umum adalah mengirim ke kunci publik Anda sendiri, isi permintaan yang tidak memuat tepat lima kolom yang didokumentasikan, message_id yang bukan UUID, atau discriminant di luar rentang 1-255. Lihat tabel kesalahan pada Batas dan kesalahan.

Dapatkah AeroNyx membaca pesan yang dikirim melalui API ini?

Tidak. Amplop dienkripsi dengan kunci yang diturunkan dari identitas pengirim dan penerima, yang tidak pernah dipegang AeroNyx. Layanan dapat melihat metadata pengiriman seperti kunci publik, waktu, ukuran, dan alamat IP.

Berapa lama pesan yang belum terkirim disimpan?

Hingga 72 jam. Konfirmasikan setiap pesan hanya setelah Anda mendekripsi dan menyimpannya; pesan yang belum dikonfirmasi dapat dikirim ulang.

Bahasa pemrograman apa yang dapat saya gunakan?

Bahasa apa pun yang memiliki Ed25519, X25519, HKDF-SHA256, dan XChaCha20-Poly1305, termasuk binding libsodium untuk sebagian besar bahasa. Halaman ini menyertakan implementasi referensi Python yang lengkap dan mulai cepat yang dapat dijalankan.

Apakah AeroNyx Chat API bersifat real-time?

Pengiriman ya: penerima yang App-nya terhubung langsung mendapat pesan Anda. Penerimaan melalui HTTPS menggunakan polling, jadi Anda melihat pesan baru pada panggilan GET /inbox/ berikutnya. Untuk menerima secara real-time, hubungkan identitas yang sama ke WebSocket Chat Relay di wss://api.aeronyx.network/ws/relay/; keduanya membaca antrean yang sama.

Apakah pengguna AeroNyx mendapat notifikasi untuk pesan yang saya kirim?

Saat ini tidak. Pesan yang dikirim melalui API ini tidak memicu notifikasi push, sehingga pengguna yang App-nya tertutup akan melihat pesan Anda saat membukanya berikutnya. Pengguna yang App-nya terhubung langsung melihatnya.

<!-- faq:end -->