Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions xchat/cryptography-primer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ If anything in the signed material changes, verification fails. Only someone wit

The Chat XDK signs when you encrypt outbound messages and verifies when you decrypt inbound ones against the sender’s public key material (from the public-key APIs). Verification is **mandatory by default**: the SDK rejects unverified signed events unless you explicitly disable the check (not recommended). Details are in the [Chat XDK](/xchat/xchat-xdk) reference.

Signatures also cover quoted content. A reply embeds the raw **signed** original message it quotes; when the Chat XDK decrypts the reply, it verifies that embedded original and compares the quote against it, reporting the outcome as `reply_preview_validation` (`Valid` / `Invalid`). An `Invalid` outcome means the quote does not match the signed original—treat the quoted material as untrusted, even though the reply itself is verified separately—so no participant can attribute fabricated words to another.

### Signed state changes (action signatures)

Messages are not the only signed material. Every call that changes conversation state—adding or rotating conversation keys, creating a group, adding members—must carry one or more **action signatures**: the sender signs a payload describing exactly what the change does (for a key change, that payload includes the new conversation key itself), and the API rejects the request if the signatures are missing or malformed.
Expand Down
372 changes: 203 additions & 169 deletions xchat/getting-started.mdx

Large diffs are not rendered by default.

35 changes: 17 additions & 18 deletions xchat/groups.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Crypto is still: **Chat XDK** for keys and payloads; **X API** to create the gro

1. Mint the group id with `POST /2/chat/conversations/group/initialize` — the response's `data.conversation_id` is the g-prefixed id you use everywhere below.
2. Load each member's identity public key and `public_key_version` (`GET` public-key routes under **Encryption keys**; [`GET /2/users/public_keys`](/x-api/users/get-public-keys-for-multiple-users) fetches several users in one request). Verify each record with `verify_key_binding` before using it (see the warning in [Getting Started](/xchat/getting-started#4-set-up-conversation-keys)).
3. Run **`prepare_group_create`** once, with **all** members (including yourself), the g-prefixed id, and the member/admin id lists. One call generates the conversation key, wraps it for every member, and signs the create — it returns **two** action signatures (the conversation-key change and the group create).
3. Run **`prepare_group_create`** once, with **all** members (including yourself), the g-prefixed id, and the member/admin id lists. One call generates the conversation key, wraps it for every member, and signs the create with the session identity from `set_identity` — it returns **two** action signatures (the conversation-key change and the group create).
4. `POST /2/chat/conversations/group` with the group members/admins, `conversation_key_version`, `conversation_participant_keys` (SDK **`encrypted_key`** → API **`encrypted_conversation_key`**), and **both** `action_signatures`. Validation failures come back as stable, human-readable messages, for example `"Too many members: adding these members would exceed the allowed group size."` or `"Cannot add all members: one or more of the requested members cannot be added to this conversation."`.
5. Keep the **raw** conversation key and **version** for encrypt/decrypt.

Expand All @@ -38,8 +38,9 @@ Crypto is still: **Chat XDK** for keys and payloads; **X API** to create the gro
<Tabs>
<Tab title="Python">
```python
# chat has keys loaded and set_identity called (see Getting Started)
prepared = chat.prepare_group_create(
"YOUR_USER_ID", signing_key_version, member_public_keys,
member_public_keys,
group_id, # g-prefixed id from POST /2/chat/conversations/group/initialize
member_ids, admin_ids, title="Project team",
)
Expand All @@ -50,8 +51,9 @@ Crypto is still: **Chat XDK** for keys and payloads; **X API** to create the gro
</Tab>
<Tab title="TypeScript">
```typescript
// chat has keys loaded and setIdentity called (see Getting Started)
const prepared = chat.prepareGroupCreate({
senderId: myUserId, signingKeyVersion, publicKeys: memberPublicKeys,
publicKeys: memberPublicKeys,
conversationId: groupId, // g-prefixed id from POST /2/chat/conversations/group/initialize
memberIds, adminIds, title: 'Project team',
});
Expand All @@ -60,9 +62,9 @@ Crypto is still: **Chat XDK** for keys and payloads; **X API** to create the gro
</Tab>
<Tab title="Rust">
```rust
// chat has keys loaded and set_identity called (see Getting Started)
let mut params = GroupCreateParams::new(
&sender_id, &signing_key_version, member_public_keys,
&group_id, member_ids, admin_ids,
member_public_keys, &group_id, member_ids, admin_ids,
);
params.title = Some("Project team".into());
let prepared = chat.prepare_group_create(params)?;
Expand All @@ -71,8 +73,8 @@ Crypto is still: **Chat XDK** for keys and payloads; **X API** to create the gro
</Tab>
<Tab title="Go">
```go
// chat has keys loaded and SetIdentity called (see Getting Started)
prepared, err := chat.PrepareGroupCreate(chatxdk.GroupCreateParams{
SenderID: myUserID, SigningKeyVersion: signingKeyVersion,
PublicKeys: memberPublicKeys, ConversationID: groupID,
MemberIDs: memberIDs, AdminIDs: adminIDs, Title: "Project team",
})
Expand All @@ -83,23 +85,20 @@ Crypto is still: **Chat XDK** for keys and payloads; **X API** to create the gro
</Tab>
<Tab title="C#">
```csharp
var prepared = chat.PrepareGroupCreate(new GroupCreateParams {
SenderId = myUserId, SigningKeyVersion = signingKeyVersion,
PublicKeys = memberPublicKeys, ConversationId = groupId,
MemberIds = memberIds, AdminIds = adminIds, Title = "Project team",
});
// chat has keys loaded and SetIdentity called (see Getting Started)
var prepared = chat.PrepareGroupCreate(
new GroupCreateParams(memberPublicKeys, groupId, memberIds, adminIds)
{
Title = "Project team",
});
// prepared.ActionSignatures has two entries — send both
```
</Tab>
<Tab title="Java">
```java
GroupCreateParams params = new GroupCreateParams();
params.senderId = myUserId;
params.signingKeyVersion = signingKeyVersion;
params.publicKeys = memberPublicKeys;
params.conversationId = groupId;
params.memberIds = memberIds;
params.adminIds = adminIds;
// chat has keys loaded and setIdentity called (see Getting Started)
GroupCreateParams params =
new GroupCreateParams(memberPublicKeys, groupId, memberIds, adminIds);
params.title = "Project team";
PreparedConversationChange prepared = chat.prepareGroupCreate(params);
// prepared.actionSignatures has two entries — send both
Expand Down
94 changes: 52 additions & 42 deletions xchat/media.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -122,23 +122,19 @@ Use the request bodies on the OpenAPI pages under **API reference → Media**. P

## Send with an attachment

Encrypt with a media attachment, then POST the send-message body (same field mapping as [Getting Started](/xchat/getting-started#5-send-a-message)).
Encrypt with a media attachment, then POST the send-message body (same field mapping as [Getting Started](/xchat/getting-started#5-send-a-message)). The SDK generates the `message_id` and returns it on the payload—send that value, and reuse the same payload on retries so an id is never minted twice.

<Tabs>
<Tab title="Python">
```python
import uuid
from xdk.chat.models import SendMessageRequest

message_id = str(uuid.uuid4())
# chat has keys loaded and set_identity called (see Getting Started)
payload = chat.encrypt_message(
message_id,
sender_id,
conversation_id,
raw_conv_key,
caption or "",
conversation_key_version,
signing_key_version,
conversation_key=raw_conv_key,
conversation_key_version=conversation_key_version,
attachments=[{
"attachment_type": "media",
"media_hash_key": media_hash_key,
Expand All @@ -151,7 +147,7 @@ Encrypt with a media attachment, then POST the send-message body (same field map
client.chat.send_message(
conversation_id.replace(":", "-"),
SendMessageRequest(
message_id=message_id,
message_id=payload.message_id, # generated by the SDK
encoded_message_create_event=payload.encrypted_content,
encoded_message_event_signature=payload.encoded_event_signature,
),
Expand All @@ -160,37 +156,47 @@ Encrypt with a media attachment, then POST the send-message body (same field map
</Tab>
<Tab title="TypeScript">
```typescript
const messageId = crypto.randomUUID();
// chat has keys loaded and setIdentity called (see Getting Started)
const payload = chat.encryptMessage({
messageId,
senderId,
conversationId,
conversationKey: rawConvKey,
text: caption || '',
conversationKey: rawConvKey,
conversationKeyVersion,
signingKeyVersion,
attachments: [{
attachmentType: 'media',
mediaHashKey: mediaHashKey,
attachment_type: 'media',
media_hash_key: mediaHashKey,
width,
height,
filesizeBytes: plaintext.byteLength,
filesize_bytes: plaintext.byteLength,
filename: 'photo.jpg',
}],
});
await client.chat.sendMessage(conversationId.replace(/:/g, '-'), {
message_id: messageId,
message_id: payload.messageId, // generated by the SDK
encoded_message_create_event: payload.encryptedContent,
encoded_message_event_signature: payload.encodedEventSignature,
});
```
</Tab>
<Tab title="Rust">
```rust
// Set attachments on EncryptMessageParams per chat_xdk_core AttachmentDescriptor::Media
let payload = chat.encrypt_message(params_with_media_attachment)?;
use chat_xdk_core::{AttachmentDescriptor, EncryptMessageParams};

// chat has keys loaded and set_identity called (see Getting Started)
let mut params = EncryptMessageParams::new(&conversation_id, caption)
.with_conversation_key(conv_key.to_bytes(), &conversation_key_version);
params.attachments = Some(vec![AttachmentDescriptor::Media {
media_hash_key: media_hash_key.clone(),
width,
height,
filesize_bytes: plaintext.len() as i64,
filename: "photo.jpg".into(),
media_type: None,
duration_millis: None,
}]);
let payload = chat.encrypt_message(params)?;
let body = serde_json::json!({
"message_id": message_id,
"message_id": payload.message_id, // generated by the SDK
"encoded_message_create_event": payload.encrypted_content,
"encoded_message_event_signature": payload.encoded_event_signature,
});
Expand All @@ -203,10 +209,12 @@ Encrypt with a media attachment, then POST the send-message body (same field map
</Tab>
<Tab title="Go">
```go
// chat has keys loaded and SetIdentity called (see Getting Started)
payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
MessageID: messageID, SenderID: senderID, ConversationID: conversationID,
ConversationKey: rawConvKey, Text: caption,
ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion,
ConversationID: conversationID,
Text: caption,
ConversationKey: rawConvKey,
ConversationKeyVersion: conversationKeyVersion,
Attachments: []chatxdk.AttachmentDescriptor{{
AttachmentType: "media",
MediaHashKey: mediaHashKey,
Expand All @@ -216,42 +224,44 @@ Encrypt with a media attachment, then POST the send-message body (same field map
Filename: "photo.jpg",
}},
})
// POST payload.EncryptedContent / EncodedEventSignature to /2/chat/conversations/{id}/messages
// POST payload.MessageID (generated by the SDK), payload.EncryptedContent,
// and payload.EncodedEventSignature to /2/chat/conversations/{id}/messages
```
</Tab>
<Tab title="C#">
```csharp
var payload = chat.EncryptMessage(new EncryptMessageParams {
MessageId = messageId,
SenderId = senderId,
ConversationId = conversationId,
// chat has keys loaded and SetIdentity called (see Getting Started)
var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, caption ?? "")
{
ConversationKey = rawConvKey,
Text = caption ?? "",
ConversationKeyVersion = conversationKeyVersion,
SigningKeyVersion = signingKeyVersion,
// Attachments = media descriptor with MediaHashKey, Width, Height,
// FilesizeBytes, and Filename (as in the Go tab above)
Attachments = new[]
{
AttachmentDescriptor.Media(mediaHashKey, width, height, plaintext.Length, "photo.jpg"),
},
});
// POST EncryptedContent / EncodedEventSignature as for text messages
// POST payload.MessageId (generated by the SDK), payload.EncryptedContent,
// and payload.EncodedEventSignature as for text messages
```
</Tab>
<Tab title="Java">
```java
EncryptMessageParams params = new EncryptMessageParams();
params.messageId = messageId;
params.senderId = senderId;
params.conversationId = conversationId;
// chat has keys loaded and setIdentity called (see Getting Started)
EncryptMessageParams params =
new EncryptMessageParams(conversationId, caption != null ? caption : "");
params.conversationKey = rawConvKey;
params.text = caption != null ? caption : "";
params.conversationKeyVersion = conversationKeyVersion;
params.signingKeyVersion = signingKeyVersion;
// params.attachments — media type with mediaHashKey, width, height, filename
params.attachments = List.of(AttachmentDescriptor.media(
mediaHashKey, width, height, plaintext.length, "photo.jpg", null, null));
SendPayload payload = chat.encryptMessage(params);
// POST to /2/chat/conversations/{id}/messages
// POST payload.messageId (generated by the SDK), payload.encryptedContent,
// and payload.encodedEventSignature to /2/chat/conversations/{id}/messages
```
</Tab>
</Tabs>

The conversation key pair can be omitted entirely: with `set_cache_keys(true)` enabled, `encrypt_message` resolves the key and version from the conversation's latest verified key change (see [Getting Started](/xchat/getting-started)).

---

## Download and decrypt
Expand Down
Loading