| コンポーネント | 役割 |
|---|---|
| Chat XDK | 暗号化、復号、署名、および秘密鍵の保管(セキュアキーバックアップまたは鍵ブロブ) |
| X API | 公開鍵、会話鍵、メッセージ、イベント——Python や TypeScript の XDK 経由、あるいは HTTPS とユーザーアクセストークンで呼び出し |
前提条件
- 開発者アカウント と OAuth 2.0 に対応するように構成された App
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" } # pin a release tag in production, e.g. tag = "vX.Y.Z"
reqwest = { version = "0.12", features = ["blocking", "json"] }
serde_json = "1"
base64 = "0.22"
# 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>
<!-- Use the latest version from https://central.sonatype.com/artifact/com.x/chatxdk -->
<version>x.y.z</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 を初期化する
このステップは既に所有している鍵をロードします——このアイデンティティで初回セットアップを完了済みの場合に使用します。- セキュアキーバックアップ: public-key レコードから取得した
juicebox_configで SDK を構築し、unlockにパスコードを渡して秘密鍵を復元します(たとえば新しいデバイスで)。 - 鍵ブロブ: 以前
export_keysでエクスポートしたブロブをimport_keysに渡し、登録済みの鍵バージョンも一緒に渡します(Rust と Go ではこの派生をimport_keys_with_version/ImportKeysWithVersionと呼びます)。
public_key_version を渡して set_identity(user_id, signing_key_version) を一度呼び出します。これによりセッションのアイデンティティが保存され、以降のすべての encrypt および prepare 呼び出しはこのアイデンティティとして署名するため、呼び出しごとに送信者 ID や署名鍵バージョンを渡す必要はありません。
初回セットアップの場合は? 同じように 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)
# Or load a key blob instead of secure key backup:
# chat.import_keys(blob, version=signing_key_version)
chat.set_identity("YOUR_USER_ID", 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.setIdentity('YOUR_USER_ID', signingKeyVersion);
use base64::{engine::general_purpose::STANDARD as B64, Engine};
use chat_xdk_core::ChatCore;
let 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_with_version(&blob, &signing_key_version)?;
chat.set_identity("YOUR_USER_ID", &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)
}
signingKeyVersion := os.Getenv("SIGNING_KEY_VERSION")
if signingKeyVersion == "" {
signingKeyVersion = "1"
}
if err := chat.ImportKeysWithVersion(blob, signingKeyVersion); err != nil {
log.Fatal(err)
}
if err := chat.SetIdentity(myUserID, signingKeyVersion); err != nil {
log.Fatal(err)
}
using ChatXdk;
using var chat = new Chat();
var signingKeyVersion = Environment.GetEnvironmentVariable("SIGNING_KEY_VERSION") ?? "1";
chat.ImportKeys(Convert.FromBase64String(
Environment.GetEnvironmentVariable("PRIVATE_KEYS_B64")!), signingKeyVersion);
chat.SetIdentity(myUserId, 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")), signingKeyVersion);
chat.setIdentity(myUserId, signingKeyVersion);
}
export_keys / import_keys)がよく使われます。クライアントアプリではセキュアキーバックアップ(パスコードを使う setup / unlock)がよく使われます。両方のパスについては Chat XDK リファレンスを参照してください。
自前の鍵を持ち込みたい場合は?
import_keys は Chat XDK の export_keys が生成した不透明なブロブのみを受け付けます——これは完全な鍵状態のバージョン管理された内部シリアライズであり、生の鍵や PEM エンコードされた P-256 鍵ではありません。このブロブを自分で構築することはできません:ステップ 3 の generate_keypairs を通じて鍵を生成し、一度ブロブをエクスポートして、base64 エンコードして保存してください。手作りしたり改変したブロブはインポートに失敗します。3. 鍵を作成して登録する(初回セットアップ)
ステップ 2 で既存の鍵をロードした場合は、このステップをスキップしてください。それ以外の場合、新しいアイデンティティの一度きりのセットアップでは次の 3 つを行います。- 鍵ペアを作成する —
generate_keypairsがアイデンティティ鍵ペアと署名鍵ペアを生成します。 - 秘密鍵を保管する — パスコードを使う
setupはセキュアキーバックアップに書き込みます(クライアント)。またはexport_keysが安全に保管するための鍵ブロブを返します(サーバーやボット)。 - 公開鍵を登録する — 他者があなたに暗号化したり、あなたの署名を検証したりできるように、登録ペイロードを add-public-key エンドポイントに POST します。
set_identity を呼び出し、このセッションが新しいアイデンティティとして署名するようにします。
すぐに実行できる、各バインディング用の一度きりの登録スクリプトが
chat-xdk/examples にあります(Python、TypeScript、Go、Rust、C#、Java)。新しいアイデンティティをオンボードするだけであれば、下記のフローを手作業で実装するのではなく、これらを使ってください。- 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_identity("YOUR_USER_ID", str(registration.version or "1"))
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.setIdentity('YOUR_USER_ID', String(registration.version ?? '1'));
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
let key_version = registration.version.clone().unwrap_or_else(|| "1".into());
chat.set_identity(&user_id, &key_version);
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()
privateKeys, _ := chat.ExportKeys() // store securely
_ = privateKeys
keyVersion := "1"
if registration.Version != nil {
keyVersion = *registration.Version
}
if err := chat.SetIdentity(userID, keyVersion); err != nil {
log.Fatal(err)
}
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.SetIdentity(userId, registration.Version ?? "1");
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.setIdentity(myUserId, registration.version != null ? registration.version : "1");
セキュアキーバックアップには強力なパスコードを使用してください。パスコードを失うか、保護されていない鍵ブロブを失うと、過去のメッセージを復号できなくなる可能性があります。
4. 会話鍵をセットアップする
すべての参加者のアイデンティティ公開鍵を渡してprepare_conversation_key_change を呼び出します。送信者のアイデンティティはステップ 2 で設定したセッションから取得されます。この 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 の各フィールドを渡してください——差し替えられたアイデンティティ鍵が会話鍵を受け取れないようにします。- 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(
[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({
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(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{
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
_ = resp
convKey := prepared.ConversationKey
convKeyVersion := prepared.ConversationKeyVersion
// KeyVersion comes from the public_key_version field on each record
var prepared = chat.PrepareConversationKeyChange(new ConversationKeyChangeParams(new[] {
new PublicKeyInput { UserId = myUserId, PublicKey = myPk, KeyVersion = myVer },
new PublicKeyInput { UserId = recipientId, PublicKey = theirPk, KeyVersion = theirVer },
})); // ConversationId null for a new 1:1; set it 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;
// conversationId stays null for a new 1:1; set it to rotate later
PreparedConversationChange prepared =
chat.prepareConversationKeyChange(new ConversationKeyChangeParams(List.of(mine, theirs)));
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. メッセージを送信する
ステップ 4 の生の会話鍵で暗号化します。SDK がメッセージ ID(UUID)を生成し、それを署名済みイベントに埋め込み、ペイロード上で返します——自分で生成することは決してありません。送信リクエストでは次のようにマップしてください。| Chat XDK フィールド | リクエストボディフィールド |
|---|---|
encrypted_content / encryptedContent / EncryptedContent | encoded_message_create_event |
encoded_event_signature / encodedEventSignature / EncodedEventSignature | encoded_message_event_signature |
ペイロードの message_id / messageId / MessageId | message_id |
: → -)は URL パスでそれを使用します。SDK 自体は柔軟です:encrypt_message と encrypt_reply は、保持している任意の形式で ID を受け付けます——イベントからの A:B、リスティングや URL パスからの A-B(順不同)、または単に受信者のユーザー ID——署名前に正規化します。グループ ID(g プレフィックス付き)はそのまま渡ります。
- Python
- TypeScript
- Rust
- Go
- C#
- Java
from xdk.chat.models import SendMessageRequest
# Sender identity resolves from set_identity (step 2)
payload = chat.encrypt_message(
"CONVERSATION_ID",
"Hello!",
conversation_key=conv_key,
conversation_key_version=conv_key_version,
)
client.chat.send_message(
"RECIPIENT_USER_ID",
SendMessageRequest(
message_id=payload.message_id, # SDK-generated, embedded in the signed event
encoded_message_create_event=payload.encrypted_content,
encoded_message_event_signature=payload.encoded_event_signature,
),
)
// Sender identity resolves from setIdentity (step 2)
const payload = chat.encryptMessage({
conversationId: 'CONVERSATION_ID',
text: 'Hello!',
conversationKey: convKey,
conversationKeyVersion: convKeyVersion,
});
await client.chat.sendMessage('RECIPIENT_USER_ID', {
message_id: payload.messageId, // SDK-generated, embedded in the signed event
encoded_message_create_event: payload.encryptedContent,
encoded_message_event_signature: payload.encodedEventSignature,
});
use chat_xdk_core::EncryptMessageParams;
// Sender identity resolves from set_identity (step 2)
let payload = chat.encrypt_message(
EncryptMessageParams::new(&conversation_id, "Hello!")
.with_conversation_key(conv_key, &conv_key_version),
)?;
let body = serde_json::json!({
// SDK-generated, embedded in the signed event
"message_id": payload.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()?;
// Sender identity resolves from SetIdentity (step 2)
payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
ConversationID: conversationID,
Text: "Hello!",
ConversationKey: convKey,
ConversationKeyVersion: convKeyVersion,
})
if err != nil {
log.Fatal(err)
}
body, _ := json.Marshal(map[string]string{
// SDK-generated, embedded in the signed event
"message_id": payload.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
// Sender identity resolves from SetIdentity (step 2)
var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hello!") {
ConversationKey = convKey,
ConversationKeyVersion = convKeyVersion,
});
var sendJson = System.Text.Json.JsonSerializer.Serialize(new Dictionary<string, string> {
// SDK-generated, embedded in the signed event
["message_id"] = payload.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();
// Sender identity resolves from setIdentity (step 2)
EncryptMessageParams params = new EncryptMessageParams(conversationId, "Hello!");
params.conversationKey = convKey;
params.conversationKeyVersion = convKeyVersion;
SendPayload payload = chat.encryptMessage(params);
String pathId = conversationId.replace(':', '-');
String sendJson = new ObjectMapper().writeValueAsString(Map.of(
// SDK-generated, embedded in the signed event
"message_id", payload.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());
このフローではステップ 4 で作成したばかりなので、スニペットでは会話鍵を明示的に渡しています。鍵キャッシュがオンで、
decrypt_events パスが会話の鍵を検証した後(ステップ 6)は、encrypt_message(conversation_id, text) だけで十分です——SDK が最新の検証済み鍵を自動で埋めます。リトライでは同じ暗号化ペイロードを再送信すべきなので、ID が二度発行されることはありません。6. 受信と復号
ライブトラフィックには Webhook かアクティビティストリームを使い、履歴には会話 events をページングします。- ライブペイロードフィールド:
encoded_event、オプションでconversation_key_change_event - 履歴:
GET /2/chat/conversations/{id}/events— すべてのイベントにmeta.conversation_key_eventsを加えてdecrypt_eventsを使うのが推奨 - 復号には、SDK が各メッセージを誰が書いたかを検証できるように、送信者の署名鍵が必要です。これらは他の参加者の 公開 鍵です——ステップ 4 で使ったのと同じ public-keys エンドポイントから取得し、フィールドを
SigningKeyEntryにマップします(下のスニペットにマッピングが含まれています) - 呼び出しごとに署名鍵(および
decrypt_eventの場合は会話鍵)を渡すか、あるいは 2 つのオプションのセッションストアを一度セットして短い呼び出し形式を使用できます。下のスニペットはストアを使用しています:set_signing_keys(entries)は参加者の鍵を保持し、set_cache_keys(true)(既定でオフ)は各会話の最新の署名検証済み鍵を保持するので、後の呼び出しは鍵引数を省略できます。どちらのスタイルでも検証は同じです - JavaScript は camelCase のイベントタイプ(
message)を使いますが、他の言語は"Message"と JSON のスネークケースフィールドを使います
- Python
- TypeScript
- Rust
- Go
- C#
- Java
# Once per process: fill the signing-key store and enable the key cache
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
]
chat.set_signing_keys(
signing_keys_for("YOUR_USER_ID") + signing_keys_for("RECIPIENT_USER_ID")
)
chat.set_cache_keys(True)
# Initial load or pagination: batch decrypt. Conversation keys are
# extracted from the KeyChange events in the batch; per-event failures
# are collected in result["errors"], never raised.
result = chat.decrypt_events(all_events_b64)
for dm in result["messages"]:
event = dm["event"]
if event["type"] == "Message" and event["content"]["content_type"] == "Text":
print(event["sender_id"], event["content"]["text"], event["verified"])
# Live traffic: one event at a time
def handle_payload(payload: dict):
if payload.get("conversation_key_change_event"):
# A rotation enters the key cache only after its signature
# verifies, which is what decrypt_events does
chat.decrypt_events([payload["conversation_key_change_event"]])
event = chat.decrypt_event(payload["encoded_event"]) # raises on failure
if event["type"] == "Message" and event["content"]["content_type"] == "Text":
print(event["sender_id"], event["content"]["text"], event["verified"])
// Once per process: fill the signing-key store and enable the key cache
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,
}));
}
chat.setSigningKeys([
...(await signingKeysFor('YOUR_USER_ID')),
...(await signingKeysFor('RECIPIENT_USER_ID')),
]);
chat.setCacheKeys(true);
// Initial load or pagination: batch decrypt. Conversation keys are
// extracted from the KeyChange events in the batch; per-event failures
// are collected in result.errors, never thrown.
const result = chat.decryptEvents(allEventsB64);
for (const dm of result.messages) {
if (dm.event.type === 'message' && dm.event.content?.contentType === 'text') {
console.log(dm.event.senderId, dm.event.content.text, dm.event.verified);
}
}
// Live traffic: one event at a time
function handlePayload(payload: {
encoded_event: string;
conversation_key_change_event?: string;
}) {
if (payload.conversation_key_change_event) {
// A rotation enters the key cache only after its signature
// verifies, which is what decryptEvents does
chat.decryptEvents([payload.conversation_key_change_event]);
}
const event = chat.decryptEvent(payload.encoded_event); // throws on failure
if (event.type === 'message' && event.content?.contentType === 'text') {
console.log(event.senderId, event.content.text, event.verified);
}
}
// Once per instance: fill the signing-key store (Vec<SigningKeyEntry>
// from GET /2/users/{id}/public_keys) and enable the key cache
chat.set_signing_keys(participant_signing_keys);
chat.set_cache_keys(true);
// Initial load: batch decrypt — per-event failures land in result.errors
let result = chat.decrypt_events(&all_events_b64, &[]);
// Live traffic: a rotation enters the key cache only after its
// signature verifies, which is what decrypt_events does
if let Some(kc) = key_change_b64.as_deref() {
chat.decrypt_events(&[kc], &[]);
}
let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?;
// Once per instance: fill the signing-key store ([]SigningKeyEntry
// from GET /2/users/{id}/public_keys) and enable the key cache
if err := chat.SetSigningKeys(participantSigningKeys); err != nil {
log.Fatal(err)
}
chat.SetCacheKeys(true)
// Initial load: batch decrypt — per-event failures land in result.Errors
result, err := chat.DecryptEvents(allEventsB64, nil)
if err != nil {
log.Fatal(err)
}
for _, dm := range result.Messages {
if dm.Event.Type == "Message" {
fmt.Println(dm.Event.AsMessage().Text())
}
}
// Live traffic: a rotation enters the key cache only after its
// signature verifies, which is what DecryptEvents does
if keyChange != "" {
chat.DecryptEvents([]string{keyChange}, nil)
}
event, err := chat.DecryptEvent(encodedEvent, nil, nil)
if err == nil && event.Type == "Message" {
fmt.Println(event.AsMessage().Text())
}
// Once per instance: fill the signing-key store (SigningKeyEntry list
// from GET /2/users/{id}/public_keys) and enable the key cache
chat.SetSigningKeys(participantSigningKeys);
chat.SetCacheKeys(true);
// Initial load: batch decrypt — per-event failures land in result.Errors
var result = chat.DecryptEvents(allEventsB64);
foreach (var dm in result.Messages)
{
if (dm.Event.GetProperty("type").GetString() == "Message")
Console.WriteLine(dm.Event.GetProperty("content").GetProperty("text").GetString());
}
// Live traffic: a rotation enters the key cache only after its
// signature verifies, which is what DecryptEvents does
if (!string.IsNullOrEmpty(keyChangeB64))
chat.DecryptEvents(new[] { keyChangeB64 });
var evt = chat.DecryptEvent(encodedEvent); // throws on failure
if (evt.GetProperty("type").GetString() == "Message")
Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString());
// Once per instance: fill the signing-key store (SigningKeyEntry list
// from GET /2/users/{id}/public_keys) and enable the key cache
chat.setSigningKeys(participantSigningKeys);
chat.setCacheKeys(true);
// Initial load: batch decrypt — per-event failures land in result.errors
DecryptEventsResult result = chat.decryptEvents(allEventsB64, null);
for (DecryptedMessage dm : result.messages) {
if ("Message".equals(dm.event.path("type").asText())) {
System.out.println(dm.event.path("content").path("text").asText());
}
}
// Live traffic: a rotation enters the key cache only after its
// signature verifies, which is what decryptEvents does
if (keyChangeB64 != null && !keyChangeB64.isEmpty()) {
chat.decryptEvents(List.of(keyChangeB64), null);
}
JsonNode evt = chat.decryptEvent(encodedEvent, (Map<String, byte[]>) null, null);
if ("Message".equals(evt.path("type").asText())) {
System.out.println(evt.path("content").path("text").asText());
}
サーバーレスまたは複数インスタンス構成の場合は? 署名鍵ストアと鍵キャッシュは SDK インスタンスのメモリ内に存在します。それが機能しない状況——ある呼び出しが復号し、別の呼び出しが送信する——では、代わりに鍵を明示的に渡してください:
decrypt_events(events, signing_keys)、decrypt_event(event_b64, conversation_keys, signing_keys)、および encrypt メソッドの conversation_key/conversation_key_version オーバーライド。decrypt_events が返す conversation_keys を自分で永続化して渡し直してください。ベストプラクティス
- 署名鍵ストアを最新に保つ:送信者が新しい鍵バージョンを登録した場合は完全な参加者セットで
set_signing_keysを呼び直し、署名検証失敗時にも更新してください - ライブ配信を
event_uuidで重複排除してください