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: 1 addition & 1 deletion xchat/cryptography-primer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ If anything in the signed material changes, verification fails. Only someone wit

### In your app

The Chat XDK signs when you encrypt outbound messages and verifies when you decrypt inbound ones—**if** you provide the sender’s public key material (from the public-key APIs). Results typically include a **verified** flag; you can treat unverified messages as suspicious or configure the SDK to reject them. Details are in the [Chat XDK](/xchat/xchat-xdk) reference.
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.

### Signed state changes (action signatures)

Expand Down
82 changes: 50 additions & 32 deletions xchat/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ Load private keys with **Juicebox** (PIN + `juicebox_config` from the API) or a
chat := chatxdk.New()
defer chat.Close()

if err := chat.ImportKeys(os.Getenv("PRIVATE_KEYS_B64")); err != nil {
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")
Expand Down Expand Up @@ -408,10 +412,14 @@ The response returns the canonical conversation id (`data.conversation_id`—the
}

// Omit conversationId for a new 1:1; pass the id to rotate later
const prepared = chat.prepareConversationKeyChange('YOUR_USER_ID', signingKeyVersion, [
await publicKeyInput('YOUR_USER_ID'),
await publicKeyInput('RECIPIENT_USER_ID'),
]);
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) => ({
Expand Down Expand Up @@ -439,11 +447,9 @@ The response returns the canonical conversation id (`data.conversation_id`—the
```rust
// 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(
&sender_id,
&signing_key_version,
&public_key_inputs,
None, // new 1:1; pass Some(id) to rotate later
ConversationKeyChangeParams::new(&sender_id, &signing_key_version, public_key_inputs),
)?;
let participant_keys: Vec<_> = prepared
.participant_keys
Expand Down Expand Up @@ -484,18 +490,23 @@ The response returns the canonical conversation id (`data.conversation_id`—the
.json()?;
// Canonical id for later requests
let conversation_id = resp["data"]["conversation_id"].as_str().unwrap().to_string();
let conv_key = prepared.conversation_key;
// 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;
```
</Tab>
<Tab title="Go">
```go
// KeyVersion comes from the public_key_version field on each record
prepared, err := chat.PrepareConversationKeyChange(myUserID, signingKeyVersion,
[]chatxdk.PublicKeyInput{
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},
}, "") // empty id for a new 1:1; pass the id to rotate later
},
// 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{
Expand Down Expand Up @@ -528,17 +539,21 @@ The response returns the canonical conversation id (`data.conversation_id`—the
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 is base64(raw key) — pass to EncryptMessage
// prepared.ConversationKey feeds EncryptMessage
_ = resp
```
</Tab>
<Tab title="C#">
```csharp
// KeyVersion comes from the public_key_version field on each record
var prepared = chat.PrepareConversationKeyChange(myUserId, signingKeyVersion, 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 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 {
Expand Down Expand Up @@ -577,9 +592,12 @@ The response returns the canonical conversation id (`data.conversation_id`—the
PublicKeyInput theirs = new PublicKeyInput();
theirs.userId = recipientId; theirs.publicKey = theirIdentityPubB64; theirs.keyVersion = theirKeyVersion;

PreparedConversationChange prepared = chat.prepareConversationKeyChange(
myUserId, signingKeyVersion, List.of(mine, theirs),
null); // null for a new 1:1; pass the id to rotate later
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) {
Expand Down Expand Up @@ -612,7 +630,7 @@ The response returns the canonical conversation id (`data.conversation_id`—the
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.getConversationKey();
byte[] convKey = prepared.conversationKey;
String convKeyVersion = prepared.conversationKeyVersion;
```
</Tab>
Expand All @@ -622,7 +640,7 @@ The response returns the canonical conversation id (`data.conversation_id`—the

## 5. Send a message

Encrypt with the **raw** conversation key (in Go, pass that key as **base64**). On the send request, map:
Encrypt with the **raw** conversation key bytes. On the send request, map:

| Chat XDK field | Request body field |
|:---------------|:-------------------|
Expand Down Expand Up @@ -663,15 +681,15 @@ Use a **hyphenated** conversation id in the URL path when the API requires it (`
import { randomUUID } from 'crypto';

const messageId = randomUUID();
const payload = chat.encryptMessage(
const payload = chat.encryptMessage({
messageId,
'YOUR_USER_ID',
'CONVERSATION_ID',
convKey,
'Hello!',
convKeyVersion,
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,
Expand All @@ -688,7 +706,7 @@ Use a **hyphenated** conversation id in the URL path when the API requires it (`
&message_id,
&sender_id,
&conversation_id,
&conv_key,
conv_key,
"Hello!",
&conv_key_version,
&signing_key_version,
Expand All @@ -712,7 +730,7 @@ Use a **hyphenated** conversation id in the URL path when the API requires it (`
MessageID: messageID,
SenderID: senderID,
ConversationID: conversationID,
ConversationKey: convKeyB64,
ConversationKey: convKey,
Text: "Hello!",
ConversationKeyVersion: convKeyVersion,
SigningKeyVersion: signingKeyVersion,
Expand Down
36 changes: 19 additions & 17 deletions xchat/groups.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,28 @@ The `title` and `avatar_url` you pass to `prepare_group_create` are signed and e
</Tab>
<Tab title="TypeScript">
```typescript
const prepared = chat.prepareGroupCreate(
myUserId, signingKeyVersion, memberPublicKeys,
groupId, // g-prefixed id from POST /2/chat/conversations/group/initialize
memberIds, adminIds, 'Project team',
);
const prepared = chat.prepareGroupCreate({
senderId: myUserId, signingKeyVersion, publicKeys: memberPublicKeys,
conversationId: groupId, // g-prefixed id from POST /2/chat/conversations/group/initialize
memberIds, adminIds, title: 'Project team',
});
// prepared.actionSignatures has two entries — send both
```
</Tab>
<Tab title="Rust">
```rust
let prepared = chat.prepare_group_create(
&sender_id, &signing_key_version, &member_public_keys,
&group_id, &member_ids, &admin_ids,
Some("Project team"), None, None,
)?;
let mut params = GroupCreateParams::new(
&sender_id, &signing_key_version, member_public_keys,
&group_id, member_ids, admin_ids,
);
params.title = Some("Project team".into());
let prepared = chat.prepare_group_create(params)?;
// prepared.action_signatures has two entries — send both
```
</Tab>
<Tab title="Go">
```go
prepared, err := chat.PrepareGroupCreate(chatxdk.PrepareGroupCreateParams{
prepared, err := chat.PrepareGroupCreate(chatxdk.GroupCreateParams{
SenderID: myUserID, SigningKeyVersion: signingKeyVersion,
PublicKeys: memberPublicKeys, ConversationID: groupID,
MemberIDs: memberIDs, AdminIDs: adminIDs, Title: "Project team",
Expand All @@ -82,7 +83,7 @@ The `title` and `avatar_url` you pass to `prepare_group_create` are signed and e
</Tab>
<Tab title="C#">
```csharp
var prepared = chat.PrepareGroupCreate(new PrepareGroupCreateParams {
var prepared = chat.PrepareGroupCreate(new GroupCreateParams {
SenderId = myUserId, SigningKeyVersion = signingKeyVersion,
PublicKeys = memberPublicKeys, ConversationId = groupId,
MemberIds = memberIds, AdminIds = adminIds, Title = "Project team",
Expand All @@ -92,7 +93,7 @@ The `title` and `avatar_url` you pass to `prepare_group_create` are signed and e
</Tab>
<Tab title="Java">
```java
PrepareGroupCreateParams params = new PrepareGroupCreateParams();
GroupCreateParams params = new GroupCreateParams();
params.senderId = myUserId;
params.signingKeyVersion = signingKeyVersion;
params.publicKeys = memberPublicKeys;
Expand Down Expand Up @@ -138,14 +139,15 @@ Whether a given field is stored encrypted is decided by the client that writes i
</Tab>
<Tab title="Rust">
```rust
let group_name = chat.decrypt(&conversation_group_name_b64, &raw_conv_key)?;
let encrypted_name = chat.encrypt("Project team", &raw_conv_key)?;
// conv_key: &XChatConversationKey from extract_conversation_keys / decrypt_conversation_key
let group_name = chat.decrypt(&conversation_group_name_b64, &conv_key)?;
let encrypted_name = chat.encrypt("Project team", &conv_key)?;
```
</Tab>
<Tab title="Go">
```go
groupName, err := chat.Decrypt(conversationGroupNameB64, convKeyB64)
encryptedName, err := chat.Encrypt("Project team", convKeyB64)
groupName, err := chat.Decrypt(conversationGroupNameB64, rawConvKey)
encryptedName, err := chat.Encrypt("Project team", rawConvKey)
_ = groupName
_ = encryptedName
```
Expand Down
53 changes: 36 additions & 17 deletions xchat/media.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ flowchart LR
let _mime = detect_mime_type(&plaintext);
let dims = detect_image_dimensions(&plaintext);
let (width, height) = dims.map(|d| (d.width as i64, d.height as i64)).unwrap_or((0, 0));
let encrypted_blob = chat.encrypt_stream(&plaintext, &raw_conv_key)?;
// conv_key: &XChatConversationKey from extract_conversation_keys / decrypt_conversation_key
let encrypted_blob = chat.encrypt_stream(&plaintext, &conv_key)?;
```
</Tab>
<Tab title="Go">
Expand All @@ -71,10 +72,9 @@ flowchart LR
mime, _ := chatxdk.DetectMimeType(plaintext)
dims, _ := chatxdk.DetectImageDimensions(plaintext)
_ = mime
ptB64, _ := chatxdk.BytesToBase64(plaintext)
encryptedB64, err := chat.EncryptStream(ptB64, convKeyB64)
encrypted, err := chat.EncryptStream(plaintext, rawConvKey)
_ = dims
_ = encryptedB64
_ = encrypted
```
</Tab>
<Tab title="C#">
Expand All @@ -84,8 +84,8 @@ flowchart LR
byte[] plaintext = await File.ReadAllBytesAsync("photo.jpg");
string? mime = ChatXdkUtilities.DetectMimeType(plaintext);
var dims = ChatXdkUtilities.DetectImageDimensions(plaintext);
int width = dims?.Width ?? 0;
int height = dims?.Height ?? 0;
int width = (int)(dims?.Width ?? 0);
int height = (int)(dims?.Height ?? 0);
byte[] encryptedBlob = chat.EncryptStream(plaintext, rawConvKey);
```
</Tab>
Expand All @@ -97,8 +97,8 @@ flowchart LR
byte[] plaintext = Files.readAllBytes(Path.of("photo.jpg"));
String mime = ChatXdkUtilities.detectMimeType(plaintext);
ImageDimensions dims = ChatXdkUtilities.detectImageDimensions(plaintext);
int width = dims != null ? dims.width : 0;
int height = dims != null ? dims.height : 0;
int width = dims != null ? (int) dims.width : 0;
int height = dims != null ? (int) dims.height : 0;
byte[] encryptedBlob = chat.encryptStream(plaintext, rawConvKey);
```
</Tab>
Expand Down Expand Up @@ -161,24 +161,23 @@ Encrypt with a media attachment, then POST the send-message body (same field map
<Tab title="TypeScript">
```typescript
const messageId = crypto.randomUUID();
const payload = chat.encryptMessage(
const payload = chat.encryptMessage({
messageId,
senderId,
conversationId,
rawConvKey,
caption || '',
conversationKey: rawConvKey,
text: caption || '',
conversationKeyVersion,
signingKeyVersion,
null,
[{
attachments: [{
attachmentType: 'media',
mediaHashKey: mediaHashKey,
width,
height,
filesizeBytes: plaintext.byteLength,
filename: 'photo.jpg',
}],
);
});
await client.chat.sendMessage(conversationId.replace(/:/g, '-'), {
message_id: messageId,
encoded_message_create_event: payload.encryptedContent,
Expand Down Expand Up @@ -206,7 +205,7 @@ Encrypt with a media attachment, then POST the send-message body (same field map
```go
payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
MessageID: messageID, SenderID: senderID, ConversationID: conversationID,
ConversationKey: convKeyB64, Text: caption,
ConversationKey: rawConvKey, Text: caption,
ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion,
Attachments: []chatxdk.AttachmentDescriptor{{
AttachmentType: "media",
Expand Down Expand Up @@ -259,6 +258,25 @@ Encrypt with a media attachment, then POST the send-message body (same field map

Path: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/download-chat-media). Response body is ciphertext. On inbound messages, read `media_hash_key` from decrypted attachments / `media_hashes`.

**Pick the key by the event's key version.** Each decrypted message event carries the `keyVersion` (JS; `key_version` in the other bindings) its content was encrypted under. Decrypt an attachment with the conversation key for **that** version—`conversationKeys.keys[event.keyVersion]`—not the latest. After a key rotation (for example a member add), the latest key cannot decrypt media attached to older messages.

<Tabs>
<Tab title="Python">
```python
keys = result["conversation_keys"]["keys"]
key_for_media = keys[event["key_version"]] # not the latest version
plaintext = chat.decrypt_stream(encrypted_blob, key_for_media)
```
</Tab>
<Tab title="TypeScript">
```typescript
const keys = result.conversationKeys.keys;
const keyForMedia = keys[event.keyVersion]; // not the latest version
const plaintext = chat.decryptStream(encryptedBlob, keyForMedia);
```
</Tab>
</Tabs>

<Tabs>
<Tab title="Python">
```python
Expand Down Expand Up @@ -296,7 +314,8 @@ Path: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/downl
.header("Authorization", &auth)
.send()?
.bytes()?;
let plaintext = chat.decrypt_stream(&encrypted_blob, &raw_conv_key)?;
// conv_key: &XChatConversationKey from extract_conversation_keys / decrypt_conversation_key
let plaintext = chat.decrypt_stream(&encrypted_blob, &conv_key)?;
```
</Tab>
<Tab title="Go">
Expand All @@ -306,7 +325,7 @@ Path: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/downl
req, _ := http.NewRequest(http.MethodGet, url, nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := http.DefaultClient.Do(req)
// read body → BytesToBase64 → chat.DecryptStream(encryptedB64, convKeyB64)
// read body into []byte → chat.DecryptStream(encryptedBlob, rawConvKey)
_ = resp
_ = err
```
Expand Down
Loading