| 구성 요소 | 역할 |
|---|---|
| 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 초기화
이 단계는 이미 보유한 키를 로드합니다—이 신원이 이전에 최초 설정을 완료한 경우 사용하세요:- 보안 키 백업: 공개 키 레코드의
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단계에서 기존 키를 로드했다면 이 단계를 건너뛰세요. 그렇지 않은 경우, 새 신원에 대한 일회성 설정은 세 가지 작업을 수행합니다:- 키페어 생성 —
generate_keypairs가 신원 및 서명 키페어를 생성합니다. - 공개 키 등록 — 다른 사람이 사용자에게 암호화하고 서명을 검증할 수 있도록 등록 페이로드를 공개 키 추가 엔드포인트에 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. 대화 키 설정
사용자 ID, 서명 키 버전, 그리고 모든 참가자의 신원 공개 키와 함께 **prepare_conversation_key_change**를 호출합니다. 한 번의 호출로 새로운 대화 키가 생성되고, 각 참가자에 대해 암호화되며, 변경 사항이 서명됩니다. 결과를 대화 키 추가 엔드포인트(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)로 확인하세요—공개 키 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(
"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. 수신 및 복호화
실시간 트래픽에는 웹훅 또는 활동 스트림을, 이력 조회에는 대화 이벤트의 페이지 처리를 사용하세요.- 실시간 페이로드 필드:
encoded_event, 선택적conversation_key_change_event - 이력:
GET /2/chat/conversations/{id}/events— 모든 이벤트에 대해 **decrypt_events**와meta.conversation_key_events를 함께 사용하는 것이 좋음 - 서명 검증을 위해 발신자의 공개 키를 복호화에 전달합니다 (API 필드를
SigningKeyEntry로 매핑; Chat XDK 참조) - JavaScript는 camelCase 이벤트 유형(
message)을 사용합니다; 다른 언어는"Message"와 JSON의 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로 실시간 전달을 중복 제거하세요- 페이지네이션이 완료될 때까지 이벤트 이력을 페이지 처리하여 키 변경 메타데이터를 놓치지 마세요
- 프로덕션에서 패스코드, 개인 키, 메시지 평문을 로그에 남기지 마세요
- 웹 앱에서는 OAuth 토큰(및 키 백업 realm 토큰 발급)을 서버에 유지하세요; 개인 키는 클라이언트 Chat XDK에만 보관하는 것이 좋습니다
다음 단계
Chat XDK
모든 언어 바인딩의 메서드 및 타입
미디어
암호화된 이미지 및 파일 첨부
그룹
다자간 대화 및 메타데이터
실시간 이벤트
웹훅 및 활동 전달