دليل تكامل واجهة AeroNyx Chat API
أرسل رسائل AeroNyx المشفّرة من طرف إلى طرف واستقبلها عبر HTTPS: توقيع الطلبات، وبداية سريعة قابلة للتشغيل، وصيغة المغلف المختوم، ورموز الأخطاء، ومتجهات الاختبار.
تتيح AeroNyx Chat API لخادم أو روبوت أو تطبيق إرسال رسائل فردية مشفّرة من طرف إلى طرف إلى مستخدمي AeroNyx واستقبالها منهم عبر HTTPS العادي. وهي لا تستخدم شبكة الخصوصية AeroNyx ولا VPN ولا التوجيه البصلي ولا صندوق البريد المجهول.
الإرسال فوري: إذا كان App المستلم متصلًا، تصل رسالتك إليه على الفور. أما الاستلام عبر هذه الواجهة فيعمل بالاستطلاع: يعيد GET /inbox/ ما ينتظر الآن فقط ولا يُبقي الطلب مفتوحًا أبدًا. للاستلام في الوقت الفعلي، صِل الهوية نفسها بـ WebSocket الخاص بـ Chat Relay (wss://api.aeronyx.network/ws/relay/). راجع قسم «التسليم في الوقت الفعلي» أدناه.
عنوان URL الأساسي: https://api.aeronyx.network/api/relay/developer/v1
آلية العمل
- أنشئ هوية. زوج مفاتيح Ed25519 هو هوية الدردشة الخاصة بك. المفتاح العام ذو الـ 32 بايتًا، المكتوب في صورة 64 حرفًا سداسيًا عشريًا بأحرف صغيرة، هو عنوان الدردشة الخاص بك. لا حاجة إلى حساب AeroNyx.
- وقّع كل طلب. يحمل كل استدعاء ترويسة
Authorizationموقَّعة بمفتاحك (المصادقة). - اختم كل رسالة. شفّر الرسالة ووقّعها من جانبك، وأرسل المغلف الناتج فقط (مغلف الرسالة).
- أرسلها باستخدام
POST /messages/. - استقبل باستخدام
GET /inbox/، ثم فُكّ تشفير كل رسالة واحفظها، ثم أكّد استلامها باستخدامPOST /acks/.
| نقطة النهاية | المصادقة | الغرض |
|---|---|---|
GET /capabilities/ | لا توجد | حدود الخدمة والميزات المدعومة |
POST /messages/ | مطلوبة | إرسال رسالة مختومة |
GET /inbox/ | مطلوبة | جلب الرسائل غير المؤكَّد استلامها |
POST /acks/ | مطلوبة | تأكيد استلام الرسائل التي حفظتها |
البدء السريع
يرسل هذا السكربت رسالة من هوية جديدة إلى هوية أخرى، ثم يقرؤها ويفك تشفيرها ويؤكد
استلامها. احفظ التنفيذ المرجعي الوارد أدناه باسم
aeronyx_chat.py، وثبّت الاعتماديات بالأمر pip install pynacl requests،
ثم شغّله.
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())
المصادقة
تتطلب كل نقطة نهاية، باستثناء GET /capabilities/، هذه الترويسة:
Authorization: AeroNyx-Chat-V1 <public-key-hex>:<unix-seconds>:<nonce-hex>:<signature-hex>
public-key-hexهو عنوان الدردشة الخاص بك المكوَّن من 64 حرفًا.- يجب أن يكون
unix-secondsضمن 60 ثانية من ساعة الخادم. nonce-hexهو 16 بايتًا عشوائيًا مكتوبة في صورة 32 حرفًا سداسيًا عشريًا بأحرف صغيرة. لا يمكن استخدام كل nonce إلا مرة واحدة.signature-hexهو توقيع Ed25519، مكتوب في صورة 128 حرفًا سداسيًا عشريًا، على هذا الملخص ذي الـ 32 بايتًا:
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 هو مسار الطلب الكامل كما أُرسل، بما في ذلك البادئة
/api/relay/developer/v1 وأي سلسلة استعلام. على سبيل المثال:
| الطلب | PATH_WITH_QUERY المطلوب توقيعه |
|---|---|
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 بايتات جسم الطلب تمامًا كما أُرسلت. لذلك فإن مسافات
JSON الفارغة وترتيب المفاتيح مهمّان: سلسِل البيانات مرة واحدة، ووقّع تلك البايتات،
وأرسل البايتات نفسها. أما الطلب الذي لا جسم له فيوقّع قيمة SHA-256 للسلسلة الفارغة.
متجه اختبار المصادقة
استخدمه للتحقق من شيفرة التوقيع لديك قبل استدعاء الواجهة. هذا المفتاح مخصص للاختبار فقط؛ لا تستخدمه أبدًا مع رسائل حقيقية.
{
"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"
}
توقيعات Ed25519 حتمية، لذا يجب أن تعطي المدخلات نفسها قيمة signature_hex هذه
بالضبط. وإذا اختلفت قيمة body_sha256_hex لديك، فهذا يعني أن بايتات جسم الطلب
لديك مختلفة.
استكشاف أخطاء 401 وإصلاحها
ترد الواجهة على كل إخفاق في المصادقة بالرمز 401 نفسه، لذا تحقق مما يلي بالترتيب:
- الساعة: أن يكون
unix-secondsضمن 60 ثانية من الوقت الفعلي. - أن تكون قيمة nonce جديدة في كل طلب، وبطول 16 بايتًا، ومكتوبة في صورة 32 حرفًا سداسيًا عشريًا بأحرف صغيرة.
- أن يتضمن المسار الموقَّع
/api/relay/developer/v1وسلسلة الاستعلام. - أن تكون الطريقة الموقَّعة بأحرف كبيرة وأن تطابق الطلب.
- أن تكون بايتات الجسم الموقَّعة هي البايتات التي ترسلها. لا تعد تسلسل JSON بعد التوقيع.
- أن يطابق الملخص والتوقيع لديك متجه الاختبار الوارد أعلاه.
نقاط النهاية
معلومات الخدمة
لا تحتاج GET /capabilities/ إلى مصادقة، وهي تعيد الحدود الحالية والميزات
المدعومة.
إرسال رسالة
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>"
}
جميع الحقول الخمسة مطلوبة ولا يُسمح بأي حقول أخرى.
receiver_pubkeyهو عنوان الدردشة الخاص بالمستلم. ويجب أن يختلف عن عنوانك.message_idهو UUID. استخدم UUID عشوائيًا جديدًا لكل رسالة، واحتفظ بالجسم نفسه تمامًا لإعادة المحاولة. يؤدي إرسالmessage_idنفسه مع حقول مختلفة إلى إعادة409 idempotency_conflict.discriminantهو11للرسالة الفردية التي ينبغي أن يراها مستخدم AeroNyx App.sent_atهو وقت Unix بالثواني، ويجب أن يكون ضمن 300 ثانية من وقت الخادم.payload_b64هو المغلف المختوم الموصوف في مغلف الرسالة.
لا يتحقق المرحِّل إلا من أن payload_b64 نص Base64 صالح حجمه من 1 بايت إلى
191 KiB. ولا يستطيع التمييز بين المغلف وبيانات أخرى، لذا فإن الحمولة التي ليست
مغلفًا صالحًا تُقبل هنا ثم يُسقطها تطبيق المستلم بصمت. اختبر مغلفك بالمقارنة مع
المتجه الذهبي.
عند نجاح الطلب يعيد HTTP 202:
{
"message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
"accepted": true,
"delivered_live": false
}
تعني accepted أن المرحِّل خزّن النص المشفّر تخزينًا دائمًا. وتكون delivered_live بقيمة true عندما يسلّم المرحِّل الرسالة إلى التسليم المباشر دون خطأ. وتبقى true حتى عندما لا يملك المستلم أي اتصال مفتوح، لذلك لا تخبرك بما إذا كان أحد متصلًا. لا يعني أيٌّ من الحقلين أن المستلم فكّ تشفير الرسالة أو عرضها؛ الذي يدل على ذلك هو إيصال التسليم في صندوق الوارد لديك (راجع «الأحداث في صندوق الوارد»).
جلب صندوق الوارد
GET /inbox/?limit=50
يعيد ما يصل إلى 100 حدث غير مؤكَّد الاستلام. وقد يظهر الحدث نفسه مرة أخرى بعد انتهاء مهلة أو إعادة تشغيل.
{
"messages": [{
"sender_pubkey": "<hex>",
"discriminant": 11,
"payload_b64": "<ciphertext>",
"timestamp": 1780000000,
"msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"
}],
"has_more": false
}
إذا كانت قيمة has_more هي true، فأكّد استلام ما حفظته ثم اجلب الرسائل مرة أخرى.
الأحداث في صندوق الوارد
يحتوي صندوق الوارد على كل ما وضعه المرحِّل في قائمة الانتظار لهويتك، لا على الرسائل الجديدة فقط. العنصر الذي لا يحتوي على control_type رسالة: افتح payload_b64 كما هو موضح في «مغلف الرسالة». أما العنصر الذي يحتوي على control_type فهو حدث يخص رسالة ما. تحقّق من control_type قبل فك تشفير أي شيء: الإيصالات لا تحتوي على payload_b64.
control_type | المعنى | msg_id |
|---|---|---|
message_receipt | فكّ App المستلم تشفير رسالة أرسلتها وخزّنها. | قيمة message_id لرسالتك |
message_read | قرأ المستلم رسالتك. يُرسل فقط إذا كانت إيصالات القراءة مفعّلة لديه. | قيمة message_id لرسالتك |
message_edit | عدّل المرسل رسالة (target_msg_id هو المعرّف نفسه). | معرّف الرسالة المعدّلة |
message_revoke | سحب المرسل رسالة. | معرّف الرسالة المسحوبة |
message_reaction | أُضيف تفاعل على target_msg_id أو أُزيل. | معرّف التفاعل |
التعديلات وعمليات السحب والتفاعلات موقّعة (payload_sig)، وصيغها موضحة في تكامل العملاء مع مُرحِّل الدردشة في AeroNyx. تأكيد معرّف ما يزيل كل عناصر قائمة الانتظار التي تحمله، لذلك عالج صفحة كاملة قبل التأكيد. أكّد أيضًا أنواع الأحداث التي لا تعالجها، وإلا فستُعاد مرة أخرى.
تأكيد استلام الرسائل
POST /acks/
{
"message_ids": ["018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"]
}
لا تؤكد استلام رسالة إلا بعد أن تتحقق منها وتفك تشفيرها وتكتبها في تخزين دائم.
وتُستبعد المعرّفات غير المعروفة من قائمة acknowledged في الاستجابة.
نموذج التسليم
- التسليم مرة واحدة على الأقل. أزل التكرار باستخدام
control_typeمعmsg_idمعًا: يحمل الإيصال معرّف الرسالة التي يؤكدها. - يُحتفظ بالنص المشفّر غير المسلَّم لمدة 72 ساعة. لا تتعامل مع الخدمة على أنها سجل دائم.
- أعد محاولة الإرسال الفاشل بالجسم نفسه تمامًا ومع nonce مصادقة جديد.
التسليم في الوقت الفعلي
الإرسال
يسلّم POST /messages/ رسالتك إلى كل اتصال يفتحه المستلم، ويحتفظ بها في الوقت نفسه في قائمة الانتظار حتى يؤكدها App المستلم. المستلم الذي يكون App لديه متصلًا يتلقاها فورًا؛ أما غير المتصل فيتلقاها عند اتصال App في المرة التالية، خلال 72 ساعة.
الرسائل المرسلة عبر هذه الواجهة لا تُطلق إشعارًا فوريًا (push). المستلم الذي يكون App لديه مغلقًا لا يُنبَّه، وسيرى الرسالة عندما يفتح App في المرة التالية.
الاستلام بالاستطلاع
يعود GET /inbox/ فورًا، سواء كان هناك ما ينتظر أم لا. لا تصل إليك الرسالة إلا في استدعائك التالي، لذا فالتأخير الذي تراه هو فاصل الاستطلاع لديك. للمحادثات، استلم عبر WebSocket بدلًا من ذلك.
الاستلام في الوقت الفعلي
صِل الهوية نفسها بـ WebSocket الخاص بـ Chat Relay. لا حاجة إلى حساب أو تسجيل.
- افتح
wss://api.aeronyx.network/ws/relay/وسجّل الدخول بمفتاح Ed25519 نفسه، كما هو موضح في جزء تسجيل الدخول من تكامل العملاء مع مُرحِّل الدردشة في AeroNyx. - بعد تسجيل الدخول، يعيد المرحِّل أولًا إرسال كل ما لا يزال في قائمة الانتظار لك (
from_offlineبقيمةtrue)، ثم يرسل كل عنصر جديد فور وصوله. تصل الرسائل كإطاراتrelay_envelopeبالحقول نفسها التي تحملها رسالة صندوق الوارد؛ أما الإيصالات والتعديلات وعمليات السحب والتفاعلات فتصل كإطارات يكونtypeفيها هوcontrol_typeالوارد في الجدول أعلاه. - بعد أن تخزّن عنصرًا، أكّده:
{"type": "relay_offline_ack", "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"}
يقرأ WebSocket وGET /inbox/ قائمة الانتظار نفسها: العنصر الذي تؤكده في أحدهما يختفي من الآخر، وPOST /acks/ يعمل مع كليهما. يمكنك مواصلة الإرسال عبر HTTPS بينما تستلم عبر WebSocket.
الحدود والأخطاء
| الحد | القيمة |
|---|---|
| الحمولة المختومة | 191 KiB (يظل طلب JSON بترميز Base64 أقل من حد الطلب) |
| جسم الطلب | 256 KiB |
| صفحة صندوق الوارد ودفعة التأكيد | 100 عنصر |
| مدة الاحتفاظ دون اتصال | 72 ساعة |
| الحالة | المعنى |
|---|---|
400 invalid_json | الجسم ليس كائن JSON. |
400 invalid_request | لا يحتوي الجسم على الحقول الخمسة بالضبط، أو أن receiver_pubkey ليس 64 حرفًا سداسيًا عشريًا بأحرف صغيرة أو يساوي عنوانك، أو أن discriminant خارج النطاق 1-255، أو أن message_id ليس UUID، أو أن payload_b64 ليس Base64 صالحًا. |
400 invalid_payload | الحمولة بعد فك ترميزها فارغة أو أكبر من 191 KiB. |
400 message_expired | يختلف sent_at عن وقت الخادم بأكثر من 300 ثانية. |
401 | المصادقة مفقودة أو منتهية الصلاحية أو غير صالحة أو معاد استخدامها. راجع استكشاف أخطاء 401 وإصلاحها. |
403 | رفضت سياسة المستلم هذا المرسِل. |
409 | تعارض في idempotency، أو أن طلبًا مطابقًا لا يزال قيد المعالجة. |
429 | تم بلوغ حد معدل المرسِل: 50 طلبًا في الثانية و100,000 طلب في اليوم بالتوقيت العالمي المنسق لكل هوية. انتظر المدة المذكورة في ترويسة Retry-After. |
503 | التسليم الدائم أو المصادقة غير متاح مؤقتًا. أعد المحاولة بالجسم نفسه تمامًا مع nonce جديد. |
ما لا يغطيه هذا الدليل
يغطي هذا الدليل الرسائل الفردية عبر HTTPS. لأي شيء آخر، اربط الهوية نفسها بـ WebSocket لمُرحِّل الدردشة وراجع تكامل العملاء مع مُرحِّل الدردشة في AeroNyx:
- رسائل المجموعات وحالة الحضور ومؤشرات الكتابة.
- المرفقات المشفّرة، بما فيها الرسائل الصوتية وملفات الفيديو.
- المكالمات الصوتية والمرئية. إطارات التشوير مدرجة في تلك المقالة، لكن دليل تكامل المكالمات الكامل (رمز الوسائط وتوقيع الدعوة وتشفير المكالمة) لم يُنشر بعد.
الخصوصية والرؤية
محتوى الرسالة مشفّر من طرف إلى طرف: فهو يُختم من جانبك بمفتاح مشتق من هويتَي المرسِل والمستلم، لا تحتفظ AeroNyx به أبدًا، وتخزّن الواجهة النص المشفّر فقط وتمرّره. ولتسليم الرسائل تستطيع الخدمة رؤية المفتاحين العامين للمرسِل والمستلم، ووقت الرسالة وحجمها، وحالة التسليم، وعنوان IP لكل طلب. ولا يخفي HTTPS هذه البيانات الوصفية عن الخدمة.
مغلف الرسالة
تستخدم الرسائل التي ينبغي أن يراها مستخدم AeroNyx App القيمة discriminant: 11
ومغلفًا مختومًا من طرف إلى طرف في payload_b64. ولا يرى المرحِّل المفتاح ولا النص
الأصلي أبدًا. جميع الأعداد الصحيحة بترتيب البايتات الأصغر أولًا (little-endian).
1. المفتاح المشترك
يشتق الطرفان المفتاح نفسه ذا الـ 32 بايتًا من هويتَي الدردشة Ed25519 الخاصتين بهما:
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)
مع libsodium يكون ذلك عبر crypto_sign_ed25519_sk_to_curve25519 و
crypto_sign_ed25519_pk_to_curve25519 وcrypto_scalarmult، يليها HKDF. يستخدم
المرسِلون دائمًا مفتاح HKDF. أما إصدارات التطبيق القديمة جدًا فكانت ترسل باستخدام
raw مباشرةً، لذا يجوز للمستلم أن يجرّب raw إذا فشل فك التشفير بمفتاح HKDF.
2. النص الأصلي
الرسالة النصية هي نصها بترميز UTF-8. أما الرسائل الواردة من التطبيق التي تحمل
مرفقات أو ردودًا أو إعادة توجيه فهي كائن JSON يحتوي على
"type": "aeronyx_message" وحقل text؛ اعرض text إذا كنت لا تدعم بقية
المحتوى.
3. التشفير
XChaCha20-Poly1305 مع nonce عشوائي جديد بطول 24 بايتًا ودون بيانات مصاحبة
(associated data). وciphertext أدناه هو ناتج التشفير شاملًا وسمه (tag) البالغ
16 بايتًا.
4. التوقيع
يوقّع المرسِل هذه البايتات الـ 121 بواسطة 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. المغلف
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 هو Base64 القياسي لهذه البايتات. لاحظ أن content_type بايت واحد
داخل البيانات الموقَّعة لكنه 4 بايتات في المغلف.
الاستقبال
حلّل المغلف، وتحقق من أن receiver_pubkey هو هويتك، وتحقق من التوقيع مقابل
sender_pubkey الوارد في المغلف، ثم فُكّ التشفير بالمفتاح المشتق من ذلك المرسِل.
أسقط الرسالة إذا فشلت أي خطوة. ويُسقط التطبيق الرسائل الحية التي يبعد timestamp
فيها عن ساعته بأكثر من 300 ثانية.
التنفيذ المرجعي (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"),
}
المتجه الذهبي
مفاتيح وnonce ومعرّف ووقت ثابتة، للاختبار فقط. لا تستخدم هذه المفاتيح أبدًا.
{
"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))) قيمة payload_b64 هذه بالضبط، ويجب أن تعيد
open_(bob, payload_b64) النص الأصلي. جرى التحقق من هذا المتجه مقابل اتفاق المفاتيح
والموقِّع والتشفير الخاصة بتطبيق AeroNyx نفسه، ومقابل مفكّك المغلفات الصارم في العقدة.
الأسئلة الشائعة
ما هي AeroNyx Chat API؟
هي واجهة HTTPS ذات إصدارات لتبادل رسائل فردية مشفّرة من طرف إلى طرف مع مستخدمي AeroNyx من خادم أو روبوت أو تطبيق. وتضم أربع نقاط نهاية: القدرات والإرسال وصندوق الوارد وتأكيد الاستلام. وهي تنقل المغلفات المختومة فقط؛ إذ يُشفَّر المحتوى من جانبك.
هل أحتاج إلى حساب AeroNyx لاستخدام الواجهة؟
لا. زوج مفاتيح Ed25519 هو الهوية. أنشئه محليًا ووقّع به كل طلب؛ والمفتاح العام هو عنوان الدردشة الخاص بك.
هل أوقّع /messages/ أم المسار الكامل؟
المسار الكامل كما أُرسل، بدءًا من /api/relay/developer/v1، مع سلسلة الاستعلام إن وُجدت. يؤدي توقيع /messages/ وحده إلى 401. ويوضح متجه اختبار المصادقة في هذه الصفحة المدخلات بالتحديد.
لماذا أتلقى 401؟
يعيد كل إخفاق في المصادقة الرمز 401 نفسه. والأسباب المعتادة هي ساعة منحرفة بأكثر من 60 ثانية، أو إعادة استخدام nonce، أو مسار أو جسم موقَّع يختلف عمّا أُرسل، أو طريقة HTTP بأحرف صغيرة في البيانات الموقَّعة. اتبع قائمة التحقق ضمن استكشاف أخطاء 401 وإصلاحها وقارن الملخص لديك بمتجه الاختبار.
لماذا تعيد الواجهة 202 لكن الرسالة لا تظهر أبدًا في AeroNyx App؟
يقبل المرحِّل أي حمولة Base64 صالحة، لكن التطبيق لا يعرض إلا مغلفًا مختومًا يستطيع التحقق منه وفك تشفيره. قارن payload_b64 لديك بالمتجه الذهبي في هذه الصفحة، وتأكد من أن receiver_pubkey في المغلف هو المستلم، وأبقِ الطابع الزمني للمغلف ضمن 300 ثانية من الوقت الحالي. إرسال نص عادي مرمَّز بـ Base64 يُقبل لكنه لا يُعرض أبدًا.
لماذا تعيد الواجهة invalid_request؟
أكثر الأسباب شيوعًا هي الإرسال إلى مفتاحك العام، أو جسم لا يحتوي على الحقول الخمسة الموثّقة بالضبط، أو message_id ليس UUID، أو discriminant خارج النطاق 1-255. راجع جدول الأخطاء ضمن الحدود والأخطاء.
هل تستطيع AeroNyx قراءة الرسائل المرسلة عبر هذه الواجهة؟
لا. تُشفَّر المغلفات بمفتاح مشتق من هويتَي المرسِل والمستلم، ولا تحتفظ AeroNyx به أبدًا. تستطيع الخدمة رؤية البيانات الوصفية للتسليم مثل المفاتيح العامة والأوقات والأحجام وعناوين IP.
إلى متى يُحتفظ بالرسائل غير المسلَّمة؟
حتى 72 ساعة. لا تؤكد استلام كل رسالة إلا بعد فك تشفيرها وحفظها؛ إذ يمكن تسليم الرسائل غير المؤكَّد استلامها مرة أخرى.
ما لغات البرمجة التي يمكنني استخدامها؟
أي لغة تدعم Ed25519 وX25519 وHKDF-SHA256 وXChaCha20-Poly1305، ويشمل ذلك روابط libsodium لمعظم اللغات. تتضمن هذه الصفحة تنفيذًا مرجعيًا كاملًا بلغة Python وبدءًا سريعًا قابلًا للتشغيل.
هل واجهة AeroNyx Chat API تعمل في الوقت الفعلي؟
الإرسال نعم: المستلم الذي يكون App لديه متصلًا يتلقى رسالتك فورًا. أما الاستلام عبر HTTPS فيعمل بالاستطلاع، لذا ترى الرسائل الجديدة عند استدعائك التالي لـ GET /inbox/. للاستلام في الوقت الفعلي، صِل الهوية نفسها بـ WebSocket الخاص بـ Chat Relay على wss://api.aeronyx.network/ws/relay/؛ فهو يقرأ قائمة الانتظار نفسها.
هل يتلقى مستخدم AeroNyx إشعارًا برسالة أرسلتُها؟
ليس حاليًا. الرسائل المرسلة عبر هذه الواجهة لا تُطلق إشعارًا فوريًا، لذا يرى المستخدم الذي يكون App لديه مغلقًا رسالتك عندما يفتحه في المرة التالية. أما المستخدم الذي يكون App لديه متصلًا فيراها فورًا.
<!-- faq:end -->