AeroNyx Chat API 連携ガイド

約17分で読めます

HTTPS で AeroNyx のエンドツーエンド暗号化メッセージを送受信します。リクエスト署名、実行可能なクイックスタート、封印済みエンベロープの形式、エラーコード、テストベクターを解説します。

AeroNyx Chat API を使うと、サーバー、ボット、アプリケーションから、通常の HTTPS を介して AeroNyx ユーザーとのエンドツーエンド暗号化された 1 対 1 のメッセージを 送受信できます。AeroNyx Privacy Network、VPN、オニオンルーティング、匿名メールボックスは 使用しません。

送信はリアルタイムです。受信者の App が接続中であれば、メッセージはすぐに届きます。この API での受信はポーリングです。GET /inbox/ はその時点で待機している内容を返すだけで、リクエストを保持し続けることはありません。リアルタイムに受信するには、同じ ID を Chat Relay の WebSocket(wss://api.aeronyx.network/ws/relay/)に接続してください。詳しくは下の「リアルタイム配信」を参照してください。

ベース URL:https://api.aeronyx.network/api/relay/developer/v1

仕組み

  1. ID を作成します。 Ed25519 の鍵ペアがチャット ID になります。32 バイトの 公開鍵を 64 文字の小文字 16 進数で表したものが、チャットアドレスです。 AeroNyx アカウントは不要です。
  2. すべてのリクエストに署名します。 各呼び出しには、鍵で署名した Authorization ヘッダーを付けます(認証)。
  3. メッセージを封印します。 メッセージの暗号化と署名は自分側で行い、 生成されたエンベロープだけを送信します(メッセージエンベロープ)。
  4. POST /messages/ で送信します。
  5. GET /inbox/ で受信し、各メッセージを復号して保存したら、 POST /acks/ で確認応答を返します。
エンドポイント認証用途
GET /capabilities/不要サービスの制限と対応機能
POST /messages/必須封印したメッセージの送信
GET /inbox/必須未確認応答のメッセージの取得
POST /acks/必須保存済みメッセージの確認応答

クイックスタート

このスクリプトは、新しい ID から別の新しい ID へメッセージを送信し、読み戻し、 復号して、確認応答を返します。下記の リファレンス実装を aeronyx_chat.py として保存し、pip install pynacl requests で依存関係を インストールしてから実行してください。

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

認証

GET /capabilities/ を除くすべてのエンドポイントで、次のヘッダーが必要です。

text
Authorization: AeroNyx-Chat-V1 <public-key-hex>:<unix-seconds>:<nonce-hex>:<signature-hex>
  • public-key-hex は、64 文字のチャットアドレスです。
  • unix-seconds は、サーバーの時計との差が 60 秒以内である必要があります。
  • nonce-hex は、16 バイトの乱数を 32 文字の小文字 16 進数で表したものです。 各 nonce は 1 回しか使えません。
  • signature-hex は、次の 32 バイトのダイジェストに対する Ed25519 署名を 128 文字の 16 進数で表したものです。
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 は、送信されるとおりのリクエストパス全体です。 /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 に署名します。

認証のテストベクター

API を呼び出す前に、署名コードの確認に使ってください。この鍵はテスト専用です。 実際のメッセージには絶対に使用しないでください。

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

Ed25519 署名は決定的なので、同じ入力からは必ずこの signature_hex と同じ値が得られます。 body_sha256_hex が異なる場合は、ボディのバイト列が異なっています。

401 のトラブルシューティング

API は、認証に失敗したすべての場合に同じ 401 を返します。そのため、 次の項目を順番に確認してください。

  1. 時計:unix-seconds が実際の時刻との差 60 秒以内であること。
  2. nonce がリクエストごとに新しく、16 バイトで、32 文字の小文字 16 進数で 表されていること。
  3. 署名するパスに /api/relay/developer/v1 とクエリ文字列が含まれていること。
  4. 署名するメソッドが大文字で、リクエストと一致していること。
  5. 署名したボディのバイト列が、送信するバイト列と同じであること。署名後に JSON を 再シリアライズしないでください。
  6. ダイジェストと署名が上記のテストベクターと一致すること。

エンドポイント

サービス情報

GET /capabilities/ は認証不要で、現在の制限と対応機能を返します。

メッセージの送信

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

5 つのフィールドはすべて必須で、それ以外のフィールドは使用できません。

  • receiver_pubkey は、受信者のチャットアドレスです。自分自身のアドレスとは異なる必要があります。
  • message_id は UUID です。メッセージごとに新しいランダムな UUID を使い、再試行に備えて ボディをそのまま保持してください。同じ message_id を異なるフィールドで送信すると、 409 idempotency_conflict が返されます。
  • discriminant は、AeroNyx App のユーザーに表示されるべき 1 対 1 のメッセージでは 11 です。
  • sent_at は Unix 秒で、サーバー時刻との差が 300 秒以内である必要があります。
  • payload_b64 は、メッセージエンベロープで説明する 封印済みエンベロープです。

リレーが確認するのは、payload_b64 が 1 バイトから 191 KiB の有効な Base64 であることだけです。 エンベロープとその他のデータを区別できないため、有効なエンベロープではないペイロードもここでは 受理され、受信者の App によって何も通知されずに破棄されます。エンベロープは ゴールデンベクターでテストしてください。

成功したリクエストは HTTP 202 を返します。

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

accepted は、リレーが暗号文を永続的に保存したことを意味します。delivered_live が true なのは、リレーがメッセージをライブ配信にエラーなく渡したという意味にすぎません。受信者の接続が 1 つもなくても true になるため、相手がオンラインかどうかの判断には使えません。どちらも、受信者がメッセージを復号した、または表示したことを意味しません。それを示すのは受信箱に届く配信レシートです(「受信箱のイベント」を参照)。

受信箱の取得

GET /inbox/?limit=50

未確認応答のイベントを最大 100 件返します。タイムアウトや再起動の後に、同じイベントが 再び現れることがあります。

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

has_more が true の場合は、保存済みのメッセージに確認応答を返してから、再度取得してください。

受信箱のイベント

受信箱には、新しいメッセージだけでなく、リレーがあなたの ID 宛てにキューに入れたすべての項目が入ります。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 は msg_id と同じ)。編集されたメッセージの ID
message_revoke送信者がメッセージを取り消した。取り消されたメッセージの ID
message_reactiontarget_msg_id へのリアクションが追加または削除された。リアクションの ID

編集、取り消し、リアクションには署名(payload_sig)が付いています。形式は AeroNyx Chat Relay クライアント統合 を参照してください。ある ID に確認応答すると、その ID を持つキュー内の項目がすべて削除されるため、1 ページ分を処理し終えてから確認応答してください。処理しないイベントの種類にも確認応答してください。そうしないと再び返されます。

メッセージの確認応答

POST /acks/

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

確認応答は、メッセージを検証し、復号し、永続ストレージに書き込んだ後にのみ返してください。 不明な ID は、レスポンスの acknowledged リストに含まれません。

配信モデル

  • 配信は at least once(少なくとも 1 回)です。control_type と msg_id の組み合わせで重複を排除してください。レシートは、確認対象のメッセージの ID を持っています。
  • 未配信の暗号文は 72 時間保持されます。このサービスを恒久的な履歴として扱わないでください。
  • 送信に失敗した場合は、まったく同じボディと新しい認証 nonce で再試行してください。

リアルタイム配信

送信

POST /messages/ は、受信者が開いているすべての接続にメッセージを渡し、同時に、受信者の App が確認応答するまでキューに保持します。受信者の App が接続中ならすぐに届き、オフラインなら次に App が接続したときに届きます(72 時間以内)。

この API で送信したメッセージは、プッシュ通知をトリガーしません。App を閉じている受信者には通知されず、次に App を開いたときにメッセージが表示されます。

ポーリングによる受信

GET /inbox/ は、待機中の項目があるかどうかに関係なく、すぐに返ります。メッセージは次に呼び出したときに初めて受け取れるので、体感する遅延はポーリング間隔そのものです。会話用途では WebSocket で受信してください。

リアルタイムの受信

同じ ID を Chat Relay の WebSocket に接続します。アカウントや登録は不要です。

  1. wss://api.aeronyx.network/ws/relay/ を開き、同じ Ed25519 鍵でログインします。手順は AeroNyx Chat Relay クライアント統合 のログインの節を参照してください。
  2. ログイン後、リレーはまずキューに残っている項目をすべて再送し(from_offline が true)、その後は新しい項目が届くたびにすぐ送ります。メッセージは、受信箱のメッセージと同じフィールドを持つ relay_envelope フレームで届きます。レシート、編集、取り消し、リアクションは、type が上の表の control_type であるフレームで届きます。
  3. 項目を保存したら、確認応答します。
json
{"type": "relay_offline_ack", "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"}

WebSocket と GET /inbox/ は同じキューを読みます。一方で確認応答した項目はもう一方からも消え、POST /acks/ はどちらにも使えます。受信を WebSocket で行いながら、送信は HTTPS のまま続けられます。

制限とエラー

制限値
封印済みペイロード191 KiB(Base64 化した JSON リクエストはリクエスト上限を下回ります)
リクエストボディ256 KiB
受信箱のページと確認応答のバッチ100 件
オフライン保持期間72 時間
ステータス意味
400 invalid_jsonボディが JSON オブジェクトではありません。
400 invalid_requestボディに 5 つのフィールドがちょうど含まれていない、receiver_pubkey が 64 文字の小文字 16 進数ではないか自分のアドレスと同じ、discriminant が 1-255 の範囲外、message_id が UUID ではない、または payload_b64 が有効な Base64 ではありません。
400 invalid_payloadデコードしたペイロードが空であるか、191 KiB を超えています。
400 message_expiredsent_at がサーバー時刻と 300 秒を超えて異なります。
401認証がない、期限切れ、無効、またはリプレイです。401 のトラブルシューティングを参照してください。
403受信者のポリシーにより、この送信者は拒否されました。
409冪等性の競合、または同一のリクエストがまだ処理中です。
429送信者のレート制限に達しました。1 つの ID につき 1 秒あたり 50 リクエスト、UTC の 1 日あたり 100,000 リクエストです。Retry-After ヘッダーに示された時間だけ待ってください。
503永続的な配信または認証が一時的に利用できません。新しい nonce で、まったく同じボディを再試行してください。

このガイドの対象外

このガイドが扱うのは、HTTPS での 1 対 1 メッセージです。それ以外は、同じ ID でチャットリレーの WebSocket に接続し、AeroNyx Chat Relay クライアント統合 を参照してください。

  • グループメッセージ、プレゼンス、入力中表示。
  • 暗号化された添付ファイル(音声メッセージや動画ファイルを含む)。
  • 音声・ビデオ通話。シグナリングフレームはその記事に一覧がありますが、通話連携の完全なガイド(メディアトークン、招待署名、通話の暗号化)はまだ公開されていません。

プライバシーと可視性

メッセージの内容はエンドツーエンドで暗号化されます。送信者と受信者の ID から導出した鍵で、 自分側で封印されます。この鍵を AeroNyx が保持することはなく、API は暗号文の保存と 転送のみを行います。メッセージを配信するため、サービスは送信者と受信者の公開鍵、メッセージの時刻、 サイズ、配信状態、各リクエストの IP アドレスを確認できます。HTTPS では、 このメタデータはサービスに対して隠されません。

メッセージエンベロープ

AeroNyx App のユーザーに表示されるべきメッセージは、discriminant: 11 と、payload_b64 内の エンドツーエンドで封印されたエンベロープを使用します。リレーが鍵や平文を見ることは ありません。整数はすべてリトルエンディアンです。

1. 共有鍵

双方が、Ed25519 のチャット ID から同じ 32 バイトの鍵を導出します。

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)

libsodium では、crypto_sign_ed25519_sk_to_curve25519、 crypto_sign_ed25519_pk_to_curve25519、crypto_scalarmult を使い、続いて HKDF を適用します。送信者は常に HKDF 鍵を使用します。非常に古い App のバージョンは raw を 直接使って送信していたため、受信者は HKDF 鍵で復号に失敗した場合に raw を試してもかまいません。

2. 平文

テキストメッセージは、その UTF-8 テキストです。添付ファイル、返信、転送を含む App からの メッセージは、"type": "aeronyx_message" と text フィールドを持つ JSON オブジェクトです。 それ以外に対応しない場合は、text を表示してください。

3. 暗号化

XChaCha20-Poly1305 を、新しくランダムに生成した 24 バイトの nonce で使用し、関連データは 使用しません。以下の ciphertext は、16 バイトのタグを含む暗号の出力です。

4. 署名

送信者は、次の 121 バイトに 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. エンベロープ

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 は、これらのバイト列の標準 Base64 です。content_type は、署名対象のデータ内では 1 バイトですが、エンベロープ内では 4 バイトである点に注意してください。

受信

エンベロープを解析し、receiver_pubkey が自分の ID であることを確認し、エンベロープの sender_pubkey に対して署名を検証してから、その送信者から導出した鍵で復号します。 いずれかの手順が失敗した場合は、メッセージを破棄してください。App は、timestamp が 自身の時計と 300 秒を超えて異なるライブメッセージを破棄します。

リファレンス実装(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"),
    }

ゴールデンベクター

鍵、nonce、ID、時刻を固定したもので、テスト専用です。これらの鍵は絶対に使用しないでください。

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))) は、この payload_b64 とまったく同じ値を生成する必要があり、 open_(bob, payload_b64) は平文を返す必要があります。このベクターは、 AeroNyx App 自身の鍵合意、署名、暗号、およびノードの厳格なエンベロープデコーダーに対して 検証済みです。

<!-- faq:start -->

よくある質問

AeroNyx Chat API とは何ですか?

サーバー、ボット、アプリケーションから、AeroNyx ユーザーとエンドツーエンド暗号化された 1 対 1 のメッセージをやり取りするための、バージョン付きの HTTPS API です。エンドポイントは、capabilities、送信、受信箱、確認応答の 4 つです。扱うのは封印されたエンベロープだけで、内容は自分側で暗号化されます。

API を使うのに AeroNyx アカウントは必要ですか?

いいえ。Ed25519 の鍵ペアが ID になります。ローカルで生成し、それですべてのリクエストに署名してください。公開鍵がチャットアドレスです。

/messages/ とフルパスのどちらに署名しますか?

送信されるとおりのフルパスです。/api/relay/developer/v1 で始まり、クエリ文字列がある場合はそれも含めます。/messages/ だけに署名すると 401 になります。このページの認証のテストベクターに、正確な入力が示されています。

なぜ 401 が返されるのですか?

認証に失敗すると、どの場合も同じ 401 が返されます。よくある原因は、時計が 60 秒を超えてずれている、nonce を再利用している、署名したパスやボディが送信した内容と異なる、署名データの HTTP メソッドが小文字になっている、などです。「401 のトラブルシューティング」のチェックリストに沿って確認し、ダイジェストをテストベクターと比較してください。

API は 202 を返すのに、メッセージが AeroNyx App に表示されないのはなぜですか?

リレーは有効な Base64 のペイロードであれば何でも受理しますが、App が表示するのは、検証と復号ができる封印済みエンベロープだけです。payload_b64 をこのページのゴールデンベクターと照合し、エンベロープの receiver_pubkey が受信者であること、エンベロープのタイムスタンプが現在時刻との差 300 秒以内であることを確認してください。プレーンテキストを Base64 にエンコードして送信しても、受理はされますが表示されることはありません。

なぜ API は invalid_request を返すのですか?

最もよくある原因は、自分自身の公開鍵への送信、ドキュメントに記載された 5 つのフィールドがちょうど含まれていないボディ、UUID ではない message_id、1-255 の範囲外の discriminant です。「制限とエラー」のエラー表を参照してください。

この API で送信したメッセージを AeroNyx は読めますか?

いいえ。エンベロープは、送信者と受信者の ID から導出した鍵で暗号化されており、この鍵を AeroNyx が保持することはありません。サービスが確認できるのは、公開鍵、時刻、サイズ、IP アドレスなどの配信メタデータです。

未配信のメッセージはどのくらい保持されますか?

最長 72 時間です。確認応答は、各メッセージを復号して保存した後にのみ返してください。確認応答のないメッセージは、再び配信されることがあります。

どのプログラミング言語を使えますか?

Ed25519、X25519、HKDF-SHA256、XChaCha20-Poly1305 に対応した言語であれば使えます。ほとんどの言語向けの libsodium バインディングが該当します。このページには、完全な Python のリファレンス実装と、実行可能なクイックスタートが含まれています。

AeroNyx Chat API はリアルタイムですか?

送信はリアルタイムです。受信者の App が接続中であれば、メッセージはすぐに届きます。HTTPS での受信はポーリングなので、新しいメッセージは次に GET /inbox/ を呼び出したときに見えます。リアルタイムに受信するには、同じ ID を Chat Relay の WebSocket(wss://api.aeronyx.network/ws/relay/)に接続してください。同じキューを読みます。

送ったメッセージで AeroNyx ユーザーに通知は届きますか?

現時点では届きません。この API で送信したメッセージはプッシュ通知をトリガーしないため、App を閉じているユーザーには次に App を開いたときに表示されます。App が接続中のユーザーにはすぐに表示されます。

<!-- faq:end -->