| コンポーネント | 役割 |
|---|---|
| Chat XDK | 暗号化、復号、署名、および秘密鍵の保存(安全な鍵バックアップまたは鍵 blob) |
| X API | 公開鍵、会話鍵、メッセージ、イベント — Python または TypeScript XDK 経由、または HTTPS とユーザーアクセストークン経由 |
前提条件
- 開発者アカウント と OAuth 2.0 用に構成されたアプリ
dm.read、dm.write、tweet.read、users.readを持つユーザーアクセストークン
1. 依存関係をインストールする
- Python
- TypeScript
- Rust
- Go
- C#
- Java
pip install chatxdk xdk
chatxdk です。chat_xdk としてインポートします。Python 3.10+ が必要です。npm install @xdevplatform/chat-xdk @xdevplatform/xdk
npm install juicebox-sdk # optional peer dependency — required for setup()/unlock() secure key backup
@xdevplatform/chat-xdk に同梱されています。ビルド手順は不要です。Node.js 18+ が必要です。[dependencies]
# chat-xdk-core is not yet on crates.io — use the git dependency
chat-xdk-core = { git = "https://github.com/xdevplatform/chat-xdk", tag = "v0.2.1" }
reqwest = { version = "0.12", features = ["blocking", "json"] }
serde_json = "1"
base64 = "0.22"
uuid = { version = "1", features = ["v4"] }
# Required until thrift 0.24 is released on crates.io
[patch.crates-io]
thrift = { git = "https://github.com/apache/thrift.git", rev = "deb36fa409849de45973b04ffc3ce49d277ca90a" }
go get github.com/xdevplatform/chat-xdk/go/chatxdk
dotnet add package XDevPlatform.ChatXdk
<dependency>
<groupId>com.x</groupId>
<artifactId>chatxdk</artifactId>
<version>0.2.1</version>
</dependency>
jna.library.path の設定は不要です。com.x.chatxdk からインポートします。JDK 17+ が必要です。- Python
- TypeScript
- Rust
- Go
- C#
- Java
from xdk import Client
client = Client(access_token="YOUR_OAUTH2_USER_TOKEN")
import { Client } from '@xdevplatform/xdk';
const client = new Client({ accessToken: 'YOUR_OAUTH2_USER_TOKEN' });
let access_token = std::env::var("X_ACCESS_TOKEN")?;
let http = reqwest::blocking::Client::new();
let auth = format!("Bearer {access_token}");
accessToken := os.Getenv("X_ACCESS_TOKEN")
httpClient := &http.Client{Timeout: 30 * time.Second}
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("X_ACCESS_TOKEN"));
String accessToken = System.getenv("X_ACCESS_TOKEN");
HttpClient http = HttpClient.newHttpClient();
2. 既存の鍵で Chat XDK を初期化する
このステップでは、すでに持っている鍵をロードします — この identity が過去に初回セットアップを完了している場合に使用してください:- 安全な鍵バックアップ: 公開鍵レコードの
juicebox_configで SDK を構築し、パスコードでunlockして秘密鍵を復元します(たとえば新しいデバイスで)。 - 鍵 blob: 以前に
export_keysでエクスポートした blob をimport_keysに渡します。
public_key_version)を設定します。
初めてセットアップする場合は? 同じ方法で SDK を構築しますが、unlock / import_keys はスキップし、ステップ 3 に進んで鍵の作成、バックアップ、登録を行ってください。
- Python
- TypeScript
- Rust
- Go
- C#
- Java
import json
from chat_xdk import Chat
resp = client.chat.get_user_public_keys(
"YOUR_USER_ID",
public_key_fields=[
"public_key_version", "public_key", "signing_public_key",
"identity_public_key_signature", "juicebox_config",
],
)
record = resp.data[0]
signing_key_version = str(record["public_key_version"])
chat = Chat(json.dumps(record["juicebox_config"]))
chat.unlock("YOUR_PASSCODE") # recovers keys stored by setup() during first-time setup (step 3)
chat.set_key_version(signing_key_version)
import { createChat } from '@xdevplatform/chat-xdk';
const resp = await client.chat.getUserPublicKeys('YOUR_USER_ID', {
publicKeyFields: [
'public_key_version', 'public_key', 'signing_public_key',
'identity_public_key_signature', 'juicebox_config',
],
});
const record = resp.data[0];
const signingKeyVersion = String(record.public_key_version);
const chat = await createChat({
juiceboxConfig: JSON.stringify(record.juicebox_config),
getAuthToken: async (realmId) => getRealmTokenFromYourBackend(realmId),
});
await chat.unlock('YOUR_PASSCODE');
chat.setKeyVersion(signingKeyVersion);
use base64::{engine::general_purpose::STANDARD as B64, Engine};
use chat_xdk_core::ChatCore;
let mut chat = ChatCore::new();
let blob = B64.decode(std::env::var("PRIVATE_KEYS_B64")?)?;
let signing_key_version = std::env::var("SIGNING_KEY_VERSION").unwrap_or_else(|_| "1".into());
chat.import_keys(&blob)?;
chat.set_key_version(&signing_key_version);
import "github.com/xdevplatform/chat-xdk/go/chatxdk"
chat := chatxdk.New()
defer chat.Close()
blob, err := chatxdk.Base64ToBytes(os.Getenv("PRIVATE_KEYS_B64"))
if err != nil {
log.Fatal(err)
}
if err := chat.ImportKeys(blob); err != nil {
log.Fatal(err)
}
signingKeyVersion := os.Getenv("SIGNING_KEY_VERSION")
if signingKeyVersion == "" {
signingKeyVersion = "1"
}
chat.SetKeyVersion(signingKeyVersion)
using ChatXdk;
using var chat = new Chat();
var signingKeyVersion = Environment.GetEnvironmentVariable("SIGNING_KEY_VERSION") ?? "1";
chat.ImportKeys(Convert.FromBase64String(
Environment.GetEnvironmentVariable("PRIVATE_KEYS_B64")!));
chat.SetKeyVersion(signingKeyVersion);
import com.x.chatxdk.Chat;
String signingKeyVersion = Optional.ofNullable(System.getenv("SIGNING_KEY_VERSION")).orElse("1");
try (Chat chat = new Chat()) {
chat.importKeys(Base64.getDecoder().decode(System.getenv("PRIVATE_KEYS_B64")));
chat.setKeyVersion(signingKeyVersion);
}
export_keys / import_keys)を使用します。クライアントアプリでは通常安全な鍵バックアップ(パスコードを使った setup / unlock)を使用します。両方のパスについては Chat XDK リファレンスを参照してください。
独自の鍵を持ち込みますか?
import_keys は、Chat XDK の export_keys が生成する不透明な blob のみを受け付けます — これは鍵の完全な状態をバージョン付きで非公開にシリアライズしたものであり、未加工または PEM エンコードされた P-256 鍵ではありません。この blob を自分で構築することはできません: generate_keypairs(ステップ 3)で鍵を生成し、blob を一度エクスポートして、base64 エンコードで保存してください。手作りまたは改変された blob はインポートに失敗します。3. 鍵を作成して登録する(初回セットアップ)
ステップ 2 で既存の鍵をロードした場合は、このステップをスキップしてください。それ以外の場合、新しい identity の 1 回限りのセットアップでは 3 つのことを行います:- キーペアを作成する —
generate_keypairsが identity と署名のキーペアを生成します。 - 公開鍵を登録する — 他のユーザーがあなた宛てに暗号化し、あなたの署名を検証できるよう、登録ペイロードを add-public-key エンドポイントに POST します。
- 秘密鍵を保存する — パスコードで
setupすると安全な鍵バックアップに書き込まれます(クライアント)。またはexport_keysが返す鍵 blob を安全に保存します(サーバーとボット)。
- Python
- TypeScript
- Rust
- Go
- C#
- Java
from xdk.chat.models import AddUserPublicKeyRequest
registration = chat.generate_keypairs()
pk = registration.public_key
client.chat.add_user_public_key(
"YOUR_USER_ID",
AddUserPublicKeyRequest(
public_key={
"identity_public_key_signature": pk.identity_public_key_signature,
"public_key": pk.public_key,
"public_key_fingerprint": pk.public_key_fingerprint,
"registration_method": pk.registration_method,
"signing_public_key": pk.signing_public_key,
"signing_public_key_signature": pk.signing_public_key_signature,
},
version=registration.version,
generate_version=registration.generate_version,
),
)
chat.setup("YOUR_PASSCODE")
chat.set_key_version(str(registration.version or signing_key_version))
const registration = chat.generateKeypairs();
const pk = registration.publicKey;
await client.chat.addUserPublicKey('YOUR_USER_ID', {
public_key: {
identity_public_key_signature: pk.identityPublicKeySignature,
public_key: pk.publicKey,
public_key_fingerprint: pk.publicKeyFingerprint,
registration_method: pk.registrationMethod,
signing_public_key: pk.signingPublicKey,
signing_public_key_signature: pk.signingPublicKeySignature,
},
version: registration.version,
generate_version: registration.generateVersion,
});
await chat.setup('YOUR_PASSCODE');
chat.setKeyVersion(String(registration.version ?? signingKeyVersion));
let registration = chat.generate_keypairs()?;
let body = serde_json::to_value(®istration)?;
let resp = http
.post(format!("https://api.x.com/2/users/{user_id}/public_keys"))
.header("Authorization", &auth)
.json(&body)
.send()?;
if !resp.status().is_success() {
anyhow::bail!("register keys: {}", resp.text()?);
}
let _blob = chat.export_keys()?; // store securely
registration, err := chat.GenerateKeypairs()
if err != nil {
log.Fatal(err)
}
regJSON, _ := json.Marshal(registration)
req, _ := http.NewRequest(http.MethodPost,
"https://api.x.com/2/users/"+userID+"/public_keys",
bytes.NewReader(regJSON))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
privateKeysB64, _ := chat.ExportKeys() // store securely
_ = privateKeysB64
chat.SetKeyVersion(signingKeyVersion)
var registration = chat.GenerateKeypairs();
var regJson = System.Text.Json.JsonSerializer.Serialize(registration);
using var content = new StringContent(regJson, Encoding.UTF8, "application/json");
using var regResp = await http.PostAsync(
$"https://api.x.com/2/users/{Uri.EscapeDataString(userId)}/public_keys", content);
regResp.EnsureSuccessStatusCode();
var blob = chat.ExportKeys(); // store securely
chat.SetKeyVersion(signingKeyVersion);
var registration = chat.generateKeypairs();
String regJson = new ObjectMapper().writeValueAsString(registration);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.x.com/2/users/" + userId + "/public_keys"))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(regJson))
.build();
HttpResponse<String> regResp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (regResp.statusCode() >= 300) {
throw new RuntimeException("register keys: " + regResp.body());
}
byte[] blob = chat.exportKeys(); // store securely
chat.setKeyVersion(signingKeyVersion);
安全な鍵バックアップには強力なパスコードを使用してください。パスコードを紛失したり、保護されていない鍵 blob を失うと、過去のメッセージの復号ができなくなる可能性があります。
4. 会話鍵をセットアップする
prepare_conversation_key_change を、あなたのユーザー ID、署名鍵のバージョン、およびすべての参加者の identity 公開鍵とともに呼び出します。1 回の呼び出しで新しい会話鍵を生成し、各参加者向けに暗号化し、変更に署名します。結果を add conversation keys エンドポイント(POST /2/chat/conversations/{id}/keys)に POST します — ボディには conversation_key_version、conversation_participant_keys(SDK の encrypted_key → API の encrypted_conversation_key)、および action_signatures が必要です(必須;API は署名がないと呼び出しを拒否します)。送信用に未加工の会話鍵を保持します。
レスポンスは、正規の会話 ID(data.conversation_id — 1:1 の場合はハイフンで結合されたペア、グループの場合は g プレフィックス付き ID)と、鍵変更の data.sequence_id を返します。以降のリクエストでは、クライアント側で再構築するのではなく、返された ID を使用してください。同じ呼び出しは後で鍵をローテーションするためにも使えます: 既存の会話 ID を prepare_conversation_key_change に渡し、新しい鍵バージョンで POST します。会話鍵が漏洩したと疑われる場合はローテーションしてください — ローテーションは将来のメッセージのみを保護します;以前の鍵バージョンで暗号化されたメッセージは、そのバージョンを保持している人にとっては引き続き読み取り可能です。
ラップする前に取得した鍵を検証してください。
prepare_conversation_key_change は、渡された任意の公開鍵に対して新しい会話鍵を暗号化します。各取得済みレコードに対してまず verify_key_binding(identity, signing, signature) でチェックし(public-keys API のレコードの public_key、signing_public_key、identity_public_key_signature フィールドを渡します)、置き換えられた identity 鍵が会話鍵を受け取れないようにしてください。- Python
- TypeScript
- Rust
- Go
- C#
- Java
def public_key_input(user_id: str) -> dict:
r = client.chat.get_user_public_keys(
user_id, public_key_fields=["public_key_version", "public_key"]
).data[0]
return {"user_id": user_id, "public_key": r["public_key"], "key_version": r["public_key_version"]}
prepared = chat.prepare_conversation_key_change(
"YOUR_USER_ID",
signing_key_version,
[public_key_input("YOUR_USER_ID"), public_key_input("RECIPIENT_USER_ID")],
# conversation_id=None for a new 1:1; pass the id to rotate later
)
resp = client.chat.add_conversation_keys(
"RECIPIENT_USER_ID",
{
"conversation_key_version": prepared["conversation_key_version"],
"conversation_participant_keys": [
{
"user_id": pk["user_id"],
"encrypted_conversation_key": pk["encrypted_key"],
"public_key_version": pk["public_key_version"],
}
for pk in prepared["participant_keys"]
],
"action_signatures": [
{
"message_id": sig["message_id"],
"encoded_message_event_detail": sig["encoded_message_event_detail"],
"message_event_signature": {
"signature": sig["signature"],
"public_key_version": sig["public_key_version"],
"signature_version": sig["signature_version"],
},
}
for sig in prepared["action_signatures"]
],
},
)
conversation_id = resp.data["conversation_id"] # canonical id for later requests
sequence_id = resp.data["sequence_id"]
conv_key = prepared["conversation_key"]
conv_key_version = prepared["conversation_key_version"]
async function publicKeyInput(userId: string) {
const r = (await client.chat.getUserPublicKeys(userId, {
publicKeyFields: ['public_key_version', 'public_key'],
})).data[0];
return { userId, publicKey: r.public_key, keyVersion: r.public_key_version };
}
// Omit conversationId for a new 1:1; pass the id to rotate later
const prepared = chat.prepareConversationKeyChange({
senderId: 'YOUR_USER_ID',
signingKeyVersion,
publicKeys: [
await publicKeyInput('YOUR_USER_ID'),
await publicKeyInput('RECIPIENT_USER_ID'),
],
});
const resp = await client.chat.addConversationKeys('RECIPIENT_USER_ID', {
conversation_key_version: prepared.conversationKeyVersion,
conversation_participant_keys: prepared.participantKeys.map((pk) => ({
user_id: pk.userId,
encrypted_conversation_key: pk.encryptedKey,
public_key_version: pk.publicKeyVersion,
})),
action_signatures: prepared.actionSignatures.map((sig) => ({
message_id: sig.messageId,
encoded_message_event_detail: sig.encodedMessageEventDetail,
message_event_signature: {
signature: sig.signature,
public_key_version: sig.publicKeyVersion,
signature_version: sig.signatureVersion,
},
})),
});
const conversationId = resp.data.conversation_id; // canonical id for later requests
const sequenceId = resp.data.sequence_id;
const convKey = prepared.conversationKey;
const convKeyVersion = prepared.conversationKeyVersion;
// public_key_inputs: Vec<PublicKeyInput> from GET public keys
// (user_id, public_key, key_version ← public_key_version)
// new 1:1; set params.conversation_id = Some(id) to rotate later
let prepared = chat.prepare_conversation_key_change(
ConversationKeyChangeParams::new(&sender_id, &signing_key_version, public_key_inputs),
)?;
let participant_keys: Vec<_> = prepared
.participant_keys
.iter()
.map(|pk| {
serde_json::json!({
"user_id": pk.user_id,
"encrypted_conversation_key": pk.encrypted_key,
"public_key_version": pk.public_key_version,
})
})
.collect();
let action_signatures: Vec<_> = prepared
.action_signatures
.iter()
.map(|sig| {
serde_json::json!({
"message_id": sig.message_id,
"encoded_message_event_detail": sig.encoded_message_event_detail,
"message_event_signature": {
"signature": sig.signature,
"public_key_version": sig.public_key_version,
"signature_version": sig.signature_version,
},
})
})
.collect();
let body = serde_json::json!({
"conversation_key_version": prepared.conversation_key_version,
"conversation_participant_keys": participant_keys,
"action_signatures": action_signatures,
});
let resp: serde_json::Value = http
.post(format!("https://api.x.com/2/chat/conversations/{recipient_id}/keys"))
.header("Authorization", &auth)
.json(&body)
.send()?
.json()?;
// Canonical id for later requests
let conversation_id = resp["data"]["conversation_id"].as_str().unwrap().to_string();
// conversation_key is Option<XChatConversationKey>; encrypt_message wants owned bytes
let conv_key = prepared.conversation_key.expect("key present").to_bytes();
let conv_key_version = prepared.conversation_key_version;
// KeyVersion comes from the public_key_version field on each record
prepared, err := chat.PrepareConversationKeyChange(chatxdk.ConversationKeyChangeParams{
SenderID: myUserID,
SigningKeyVersion: signingKeyVersion,
PublicKeys: []chatxdk.PublicKeyInput{
{UserID: myUserID, PublicKey: myIdentityPubB64, KeyVersion: myKeyVersion},
{UserID: recipientID, PublicKey: theirIdentityPubB64, KeyVersion: theirKeyVersion},
},
// ConversationID empty for a new 1:1; pass the id to rotate later
})
var parts []map[string]string
for _, pk := range prepared.ParticipantKeys {
parts = append(parts, map[string]string{
"user_id": pk.UserID,
"encrypted_conversation_key": pk.EncryptedKey,
"public_key_version": pk.PublicKeyVersion,
})
}
var sigs []map[string]any
for _, sig := range prepared.ActionSignatures {
sigs = append(sigs, map[string]any{
"message_id": sig.MessageID,
"encoded_message_event_detail": sig.EncodedMessageEventDetail,
"message_event_signature": map[string]string{
"signature": sig.Signature,
"public_key_version": sig.PublicKeyVersion,
"signature_version": sig.SignatureVersion,
},
})
}
body, _ := json.Marshal(map[string]any{
"conversation_key_version": prepared.ConversationKeyVersion,
"conversation_participant_keys": parts,
"action_signatures": sigs,
})
req, _ := http.NewRequest(http.MethodPost,
"https://api.x.com/2/chat/conversations/"+recipientID+"/keys",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
// Response data.conversation_id is the canonical id for later requests
// prepared.ConversationKey feeds EncryptMessage
_ = resp
// KeyVersion comes from the public_key_version field on each record
var prepared = chat.PrepareConversationKeyChange(new ConversationKeyChangeParams {
SenderId = myUserId,
SigningKeyVersion = signingKeyVersion,
PublicKeys = new[] {
new PublicKeyInput { UserId = myUserId, PublicKey = myPk, KeyVersion = myVer },
new PublicKeyInput { UserId = recipientId, PublicKey = theirPk, KeyVersion = theirVer },
},
}); // ConversationId null for a new 1:1; pass the id to rotate later
var keysBody = new {
conversation_key_version = prepared.ConversationKeyVersion,
conversation_participant_keys = prepared.ParticipantKeys.Select(pk => new {
user_id = pk.UserId,
encrypted_conversation_key = pk.EncryptedKey,
public_key_version = pk.PublicKeyVersion,
}),
action_signatures = prepared.ActionSignatures.Select(sig => new {
message_id = sig.MessageId,
encoded_message_event_detail = sig.EncodedMessageEventDetail,
message_event_signature = new {
signature = sig.Signature,
public_key_version = sig.PublicKeyVersion,
signature_version = sig.SignatureVersion,
},
}),
};
var json = System.Text.Json.JsonSerializer.Serialize(keysBody);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
using var resp = await http.PostAsync(
$"https://api.x.com/2/chat/conversations/{Uri.EscapeDataString(recipientId)}/keys",
content);
resp.EnsureSuccessStatusCode();
var data = System.Text.Json.JsonDocument.Parse(await resp.Content.ReadAsStringAsync())
.RootElement.GetProperty("data");
string conversationId = data.GetProperty("conversation_id").GetString()!; // canonical id
byte[] convKey = prepared.ConversationKey!;
string convKeyVersion = prepared.ConversationKeyVersion;
// keyVersion comes from the public_key_version field on each record
PublicKeyInput mine = new PublicKeyInput();
mine.userId = myUserId; mine.publicKey = myIdentityPubB64; mine.keyVersion = myKeyVersion;
PublicKeyInput theirs = new PublicKeyInput();
theirs.userId = recipientId; theirs.publicKey = theirIdentityPubB64; theirs.keyVersion = theirKeyVersion;
ConversationKeyChangeParams keyParams = new ConversationKeyChangeParams();
keyParams.senderId = myUserId;
keyParams.signingKeyVersion = signingKeyVersion;
keyParams.publicKeys = List.of(mine, theirs);
// keyParams.conversationId null for a new 1:1; set the id to rotate later
PreparedConversationChange prepared = chat.prepareConversationKeyChange(keyParams);
List<Map<String, String>> parts = new ArrayList<>();
for (var pk : prepared.participantKeys) {
parts.add(Map.of(
"user_id", pk.userId,
"encrypted_conversation_key", pk.encryptedKey,
"public_key_version", pk.publicKeyVersion));
}
List<Map<String, Object>> sigs = new ArrayList<>();
for (var sig : prepared.actionSignatures) {
sigs.add(Map.of(
"message_id", sig.messageId,
"encoded_message_event_detail", sig.encodedMessageEventDetail,
"message_event_signature", Map.of(
"signature", sig.signature,
"public_key_version", sig.publicKeyVersion,
"signature_version", sig.signatureVersion)));
}
ObjectMapper mapper = new ObjectMapper();
String body = mapper.writeValueAsString(Map.of(
"conversation_key_version", prepared.conversationKeyVersion,
"conversation_participant_keys", parts,
"action_signatures", sigs));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.x.com/2/chat/conversations/" + recipientId + "/keys"))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
JsonNode data = mapper.readTree(resp.body()).path("data");
String conversationId = data.path("conversation_id").asText(); // canonical id
byte[] convKey = prepared.conversationKey;
String convKeyVersion = prepared.conversationKeyVersion;
5. メッセージを送信する
未加工の会話鍵バイトで暗号化します。送信リクエストでは、次のようにマップします:| Chat XDK フィールド | リクエストボディフィールド |
|---|---|
encrypted_content / encryptedContent / EncryptedContent | encoded_message_create_event |
encoded_event_signature / encodedEventSignature / EncodedEventSignature | encoded_message_event_signature |
| 生成した ID | message_id |
: → -)。SDK 自体は柔軟です: encrypt_message と encrypt_reply は、保持している任意の形式で ID を受け付けます — イベントからの A:B、リストや URL パスからの A-B(どちらの順序でも可)、あるいは受信者のユーザー ID だけでも構いません — そして署名する前に正規化します。グループ ID(g プレフィックス付き)はそのまま渡されます。
- Python
- TypeScript
- Rust
- Go
- C#
- Java
import uuid
from xdk.chat.models import SendMessageRequest
message_id = str(uuid.uuid4())
payload = chat.encrypt_message(
message_id,
"YOUR_USER_ID",
"CONVERSATION_ID",
conv_key,
"Hello!",
conv_key_version,
signing_key_version,
)
client.chat.send_message(
"RECIPIENT_USER_ID",
SendMessageRequest(
message_id=message_id,
encoded_message_create_event=payload.encrypted_content,
encoded_message_event_signature=payload.encoded_event_signature,
),
)
import { randomUUID } from 'crypto';
const messageId = randomUUID();
const payload = chat.encryptMessage({
messageId,
senderId: 'YOUR_USER_ID',
conversationId: 'CONVERSATION_ID',
conversationKey: convKey,
text: 'Hello!',
conversationKeyVersion: convKeyVersion,
signingKeyVersion,
});
await client.chat.sendMessage('RECIPIENT_USER_ID', {
message_id: messageId,
encoded_message_create_event: payload.encryptedContent,
encoded_message_event_signature: payload.encodedEventSignature,
});
use chat_xdk_core::EncryptMessageParams;
let message_id = uuid::Uuid::new_v4().to_string();
let payload = chat.encrypt_message(EncryptMessageParams::new(
&message_id,
&sender_id,
&conversation_id,
conv_key,
"Hello!",
&conv_key_version,
&signing_key_version,
))?;
let body = serde_json::json!({
"message_id": message_id,
"encoded_message_create_event": payload.encrypted_content,
"encoded_message_event_signature": payload.encoded_event_signature,
});
let path_id = conversation_id.replace(':', "-");
http.post(format!("https://api.x.com/2/chat/conversations/{path_id}/messages"))
.header("Authorization", &auth)
.json(&body)
.send()?;
messageID := uuid.NewString()
payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
MessageID: messageID,
SenderID: senderID,
ConversationID: conversationID,
ConversationKey: convKey,
Text: "Hello!",
ConversationKeyVersion: convKeyVersion,
SigningKeyVersion: signingKeyVersion,
})
body, _ := json.Marshal(map[string]string{
"message_id": messageID,
"encoded_message_create_event": payload.EncryptedContent,
"encoded_message_event_signature": payload.EncodedEventSignature,
})
pathID := strings.ReplaceAll(conversationID, ":", "-")
req, _ := http.NewRequest(http.MethodPost,
"https://api.x.com/2/chat/conversations/"+pathID+"/messages",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
_ = resp
var messageId = Guid.NewGuid().ToString();
var payload = chat.EncryptMessage(new EncryptMessageParams {
MessageId = messageId,
SenderId = senderId,
ConversationId = conversationId,
ConversationKey = convKey,
Text = "Hello!",
ConversationKeyVersion = convKeyVersion,
SigningKeyVersion = signingKeyVersion,
});
var sendJson = System.Text.Json.JsonSerializer.Serialize(new Dictionary<string, string> {
["message_id"] = messageId,
["encoded_message_create_event"] = payload.EncryptedContent,
["encoded_message_event_signature"] = payload.EncodedEventSignature,
});
using var content = new StringContent(sendJson, Encoding.UTF8, "application/json");
var pathId = conversationId.Replace(':', '-');
using var resp = await http.PostAsync(
$"https://api.x.com/2/chat/conversations/{Uri.EscapeDataString(pathId)}/messages",
content);
resp.EnsureSuccessStatusCode();
EncryptMessageParams params = new EncryptMessageParams();
params.messageId = UUID.randomUUID().toString();
params.senderId = senderId;
params.conversationId = conversationId;
params.conversationKey = convKey;
params.text = "Hello!";
params.conversationKeyVersion = convKeyVersion;
params.signingKeyVersion = signingKeyVersion;
SendPayload payload = chat.encryptMessage(params);
String pathId = conversationId.replace(':', '-');
String sendJson = new ObjectMapper().writeValueAsString(Map.of(
"message_id", params.messageId,
"encoded_message_create_event", payload.encryptedContent,
"encoded_message_event_signature", payload.encodedEventSignature));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.x.com/2/chat/conversations/" + pathId + "/messages"))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(sendJson))
.build();
http.send(req, HttpResponse.BodyHandlers.ofString());
6. 受信して復号する
ライブトラフィックには Webhook またはアクティビティストリーム を使用します。履歴には会話イベントをページングします。- ライブペイロードのフィールド:
encoded_event、オプションのconversation_key_change_event - 履歴:
GET /2/chat/conversations/{id}/events— 全イベントに対するdecrypt_eventsに加えてmeta.conversation_key_eventsを優先 - 署名検証のために送信者の公開鍵を decrypt に渡します(API フィールドを
SigningKeyEntryにマップします;Chat XDK を参照) - JavaScript は camelCase のイベントタイプ(
message)を使用します;他の言語は JSON で"Message"と snake_case フィールドを使用します
- Python
- TypeScript
- Rust
- Go
- C#
- Java
conversation_keys = {} # conversation_id -> { version: key_bytes }
def signing_keys_for(user_id: str) -> list[dict]:
resp = client.chat.get_user_public_keys(
user_id,
public_key_fields=[
"public_key_version", "public_key", "signing_public_key", "identity_public_key_signature",
],
)
return [
{
"user_id": user_id,
"public_key_version": r["public_key_version"],
"public_key": r["signing_public_key"],
"identity_public_key": r["public_key"],
"identity_public_key_signature": r["identity_public_key_signature"],
}
for r in resp.data
]
def handle_payload(payload: dict):
cid = payload["conversation_id"]
if payload.get("conversation_key_change_event"):
conversation_keys[cid] = chat.extract_conversation_keys(
[payload["conversation_key_change_event"]]
)["keys"]
event = chat.decrypt_event(
payload["encoded_event"],
conversation_keys.get(cid, {}),
signing_keys_for(payload["sender_id"]),
)
if event.get("type") == "Message" and event.get("content", {}).get("content_type") == "Text":
print(event["sender_id"], event["content"]["text"], event.get("verified"))
const conversationKeys = new Map<string, Record<string, Uint8Array>>();
async function signingKeysFor(userId: string) {
const resp = await client.chat.getUserPublicKeys(userId, {
publicKeyFields: [
'public_key_version', 'public_key', 'signing_public_key', 'identity_public_key_signature',
],
});
return resp.data.map((r: {
public_key_version: string;
public_key: string;
signing_public_key: string;
identity_public_key_signature: string;
}) => ({
userId,
publicKeyVersion: r.public_key_version,
publicKey: r.signing_public_key,
identityPublicKey: r.public_key,
identityPublicKeySignature: r.identity_public_key_signature,
}));
}
async function handlePayload(payload: {
conversation_id: string;
encoded_event: string;
sender_id: string;
conversation_key_change_event?: string;
}) {
const cid = payload.conversation_id;
if (payload.conversation_key_change_event) {
conversationKeys.set(
cid,
chat.extractConversationKeys([payload.conversation_key_change_event]).keys,
);
}
const event = chat.decryptEvent(
payload.encoded_event,
conversationKeys.get(cid) ?? {},
await signingKeysFor(payload.sender_id),
);
if (event.type === 'message' && event.content?.contentType === 'text') {
console.log(event.senderId, event.content.text, event.verified);
}
}
// Build Vec<SigningKeyEntry> from GET /2/users/{id}/public_keys
// (public_key_version, public_key, signing_public_key, identity_public_key_signature)
if let Some(kc) = key_change_b64.as_deref() {
let extracted = chat.extract_conversation_keys(&[kc]);
conv_keys.extend(extracted.keys);
}
let event = chat.decrypt_event(&encoded_event, &conv_keys, &sender_signing_keys)?;
if keyChange != "" {
extracted, _ := chat.ExtractConversationKeys([]string{keyChange})
for v, k := range extracted.Keys {
convKeys[v] = k
}
}
event, err := chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys)
if err == nil && event.Type == "Message" {
fmt.Println(event.AsMessage().Text())
}
if (!string.IsNullOrEmpty(keyChangeB64))
{
var extracted = chat.ExtractConversationKeys(new[] { keyChangeB64 });
foreach (var kv in extracted.Keys)
convKeys[kv.Key] = kv.Value;
}
var evt = chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys);
if (evt.GetProperty("type").GetString() == "Message")
Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString());
if (keyChangeB64 != null && !keyChangeB64.isEmpty()) {
var extracted = chat.extractConversationKeys(List.of(keyChangeB64));
convKeys.putAll(extracted.keys);
}
JsonNode evt = chat.decryptEvent(encodedEvent, convKeys, senderSigningKeys);
if ("Message".equals(evt.path("type").asText())) {
System.out.println(evt.path("content").path("text").asText());
}
ベストプラクティス
- 未加工の会話鍵と送信者の公開鍵をキャッシュし、署名検証が失敗した場合は更新する
event_uuidでライブ配信の重複を排除する- ページネーションが完了するまでイベント履歴をページングし、鍵変更のメタデータを見逃さないようにする
- 本番環境でパスコード、秘密鍵、メッセージ平文をログに残さない
- Web アプリでは、OAuth トークン(および鍵バックアップ realm トークン発行)をサーバー側で保持し、秘密鍵はクライアントの Chat XDK 内でのみ保持することが望ましい
次のステップ
Chat XDK
すべての言語バインディングのメソッドと型
Media
暗号化された画像とファイル添付
Groups
複数参加者の会話とメタデータ
Real-time events
Webhook とアクティビティ配信