diff --git a/xchat/cryptography-primer.mdx b/xchat/cryptography-primer.mdx
index f98c7b364..bcfe4a4ad 100644
--- a/xchat/cryptography-primer.mdx
+++ b/xchat/cryptography-primer.mdx
@@ -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.
diff --git a/xchat/getting-started.mdx b/xchat/getting-started.mdx
index 145ece8ed..bcfb7f426 100644
--- a/xchat/getting-started.mdx
+++ b/xchat/getting-started.mdx
@@ -45,11 +45,10 @@ X Chat apps use two pieces together:
```toml
[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" }
+ chat-xdk-core = { git = "https://github.com/xdevplatform/chat-xdk", tag = "v0.4.0" }
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]
@@ -75,7 +74,7 @@ X Chat apps use two pieces together:
com.x
chatxdk
- 0.2.1
+ 0.4.0
```
@@ -136,9 +135,9 @@ Create an API client with your **user** OAuth 2.0 access token:
This step **loads keys you already have**—use it when this identity completed first-time setup before:
- **Secure key backup:** construct the SDK with the `juicebox_config` from your public-key record, then `unlock` with your passcode to recover the private keys (for example, on a new device).
-- **Key blob:** `import_keys` with a blob you previously exported via `export_keys`.
+- **Key blob:** `import_keys` with a blob you previously exported via `export_keys`, passing the registered key version alongside it (Rust and Go name this variant `import_keys_with_version` / `ImportKeysWithVersion`).
-Then set your registered public-key version (`public_key_version` on your record).
+Then call **`set_identity(user_id, signing_key_version)`** once, with your user ID and your record's `public_key_version`. This stores the session identity: every later encrypt and prepare call signs as this identity, so you never pass a sender ID or signing key version per call.
**Setting up for the first time?** Construct the SDK the same way but skip `unlock`/`import_keys`, and continue to [step 3](#3-create-and-register-keys-first-time-setup) to create, back up, and register your keys.
@@ -160,7 +159,9 @@ Then set your registered public-key version (`public_key_version` on your record
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)
+ # 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)
```
@@ -181,7 +182,7 @@ Then set your registered public-key version (`public_key_version` on your record
getAuthToken: async (realmId) => getRealmTokenFromYourBackend(realmId),
});
await chat.unlock('YOUR_PASSCODE');
- chat.setKeyVersion(signingKeyVersion);
+ chat.setIdentity('YOUR_USER_ID', signingKeyVersion);
```
@@ -189,11 +190,11 @@ Then set your registered public-key version (`public_key_version` on your record
use base64::{engine::general_purpose::STANDARD as B64, Engine};
use chat_xdk_core::ChatCore;
- let mut chat = ChatCore::new();
+ 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(&blob)?;
- chat.set_key_version(&signing_key_version);
+ chat.import_keys_with_version(&blob, &signing_key_version)?;
+ chat.set_identity("YOUR_USER_ID", &signing_key_version);
```
@@ -207,14 +208,16 @@ Then set your registered public-key version (`public_key_version` on your record
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)
+ if err := chat.ImportKeysWithVersion(blob, signingKeyVersion); err != nil {
+ log.Fatal(err)
+ }
+ if err := chat.SetIdentity(myUserID, signingKeyVersion); err != nil {
+ log.Fatal(err)
+ }
```
@@ -224,8 +227,8 @@ Then set your registered public-key version (`public_key_version` on your record
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);
+ Environment.GetEnvironmentVariable("PRIVATE_KEYS_B64")!), signingKeyVersion);
+ chat.SetIdentity(myUserId, signingKeyVersion);
```
@@ -234,8 +237,8 @@ Then set your registered public-key version (`public_key_version` on your record
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);
+ chat.importKeys(Base64.getDecoder().decode(System.getenv("PRIVATE_KEYS_B64")), signingKeyVersion);
+ chat.setIdentity(myUserId, signingKeyVersion);
}
```
@@ -257,6 +260,8 @@ Skip this step if you loaded existing keys in [step 2](#2-initialize-the-chat-xd
2. **Store the private keys** — `setup` with a passcode writes them to secure key backup (clients), or `export_keys` returns a key blob for you to store securely (servers and bots).
3. **Register the public keys** — POST the registration payload to the add-public-key endpoint so others can encrypt to you and verify your signatures.
+Finish by calling `set_identity` with the registration's key version, so this session signs as the new identity.
+
Ready-to-run one-time registration scripts for every binding live under [`chat-xdk/examples`](https://github.com/xdevplatform/chat-xdk/tree/main/examples) (Python, TypeScript, Go, Rust, C#, and Java). Use them instead of hand-rolling the flow below when you just need to onboard a new identity.
@@ -284,7 +289,7 @@ Ready-to-run one-time registration scripts for every binding live under [`chat-x
),
)
chat.setup("YOUR_PASSCODE")
- chat.set_key_version(str(registration.version or signing_key_version))
+ chat.set_identity("YOUR_USER_ID", str(registration.version or "1"))
```
@@ -304,7 +309,7 @@ Ready-to-run one-time registration scripts for every binding live under [`chat-x
generate_version: registration.generateVersion,
});
await chat.setup('YOUR_PASSCODE');
- chat.setKeyVersion(String(registration.version ?? signingKeyVersion));
+ chat.setIdentity('YOUR_USER_ID', String(registration.version ?? '1'));
```
@@ -320,6 +325,8 @@ Ready-to-run one-time registration scripts for every binding live under [`chat-x
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);
```
@@ -339,9 +346,15 @@ Ready-to-run one-time registration scripts for every binding live under [`chat-x
log.Fatal(err)
}
resp.Body.Close()
- privateKeysB64, _ := chat.ExportKeys() // store securely
- _ = privateKeysB64
- chat.SetKeyVersion(signingKeyVersion)
+ 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)
+ }
```
@@ -353,7 +366,7 @@ Ready-to-run one-time registration scripts for every binding live under [`chat-x
$"https://api.x.com/2/users/{Uri.EscapeDataString(userId)}/public_keys", content);
regResp.EnsureSuccessStatusCode();
var blob = chat.ExportKeys(); // store securely
- chat.SetKeyVersion(signingKeyVersion);
+ chat.SetIdentity(userId, registration.Version ?? "1");
```
@@ -371,7 +384,7 @@ Ready-to-run one-time registration scripts for every binding live under [`chat-x
throw new RuntimeException("register keys: " + regResp.body());
}
byte[] blob = chat.exportKeys(); // store securely
- chat.setKeyVersion(signingKeyVersion);
+ chat.setIdentity(myUserId, registration.version != null ? registration.version : "1");
```
@@ -384,7 +397,7 @@ Use a strong passcode for secure key backup. Losing the passcode or an unprotect
## 4. Set up conversation keys
-Call **`prepare_conversation_key_change`** with your user ID, your signing key version, and every participant's identity public key. One call generates a fresh conversation key, encrypts it for each participant, and signs the change. POST the result to the **add conversation keys** endpoint (`POST /2/chat/conversations/{id}/keys`)—the body needs `conversation_key_version`, `conversation_participant_keys` (SDK `encrypted_key` → API `encrypted_conversation_key`), and **`action_signatures`** (required; the API rejects the call without them). Keep the **raw** conversation key for sending.
+Call **`prepare_conversation_key_change`** with every participant's identity public key; the sender identity comes from the session you set in step 2. One call generates a fresh conversation key, encrypts it for each participant, and signs the change. POST the result to the **add conversation keys** endpoint (`POST /2/chat/conversations/{id}/keys`)—the body needs `conversation_key_version`, `conversation_participant_keys` (SDK `encrypted_key` → API `encrypted_conversation_key`), and **`action_signatures`** (required; the API rejects the call without them). Keep the **raw** conversation key for sending.
The response returns the canonical conversation id (`data.conversation_id`—the hyphen-joined pair for a 1:1, or the g-prefixed id for a group) and the key change's `data.sequence_id`. Use that returned id for subsequent requests instead of reconstructing it client-side. The same call also **rotates** keys later: pass the existing conversation id to `prepare_conversation_key_change` and POST with the newer key version. Rotate when you suspect the conversation key was exposed—rotation protects **future** messages only; messages encrypted under earlier key versions stay readable to anyone who holds those versions.
@@ -402,8 +415,6 @@ The response returns the canonical conversation id (`data.conversation_id`—the
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
)
@@ -450,8 +461,6 @@ 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({
- senderId: 'YOUR_USER_ID',
- signingKeyVersion,
publicKeys: [
await publicKeyInput('YOUR_USER_ID'),
await publicKeyInput('RECIPIENT_USER_ID'),
@@ -484,9 +493,9 @@ The response returns the canonical conversation id (`data.conversation_id`—the
```rust
// public_key_inputs: Vec 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
+ // 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),
+ ConversationKeyChangeParams::new(public_key_inputs),
)?;
let participant_keys: Vec<_> = prepared
.participant_keys
@@ -536,8 +545,6 @@ The response returns the canonical conversation id (`data.conversation_id`—the
```go
// 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},
@@ -576,21 +583,18 @@ 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 feeds EncryptMessage
_ = resp
+ convKey := prepared.ConversationKey
+ convKeyVersion := prepared.ConversationKeyVersion
```
```csharp
// 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 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 {
@@ -629,12 +633,9 @@ The response returns the canonical conversation id (`data.conversation_id`—the
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);
+ // conversationId stays null for a new 1:1; set it to rotate later
+ PreparedConversationChange prepared =
+ chat.prepareConversationKeyChange(new ConversationKeyChangeParams(List.of(mine, theirs)));
List
+### A reply carries `reply_preview_validation: "Invalid"`
+
+Decrypted replies may carry `reply_preview_validation` (`"Valid"` / `"Invalid"`; JavaScript uses `'valid'` / `'invalid'`). `Invalid` means the quoted preview inside the message does not match the signed original event it embeds—treat the quote as untrusted and render the quoted content only from the validated original. The message itself is verified separately and is still authentic; nothing throws for an invalid preview.
+
### Old events permanently fail verification
Errors like `signature missing or no matching signing key` or an ECDSA mismatch on **old** events are permanent. Signatures are immutable and verified by rebuilding the signed payload from the event itself, so an event that was signed over different bytes (or never signed) fails on every future load—no retry, key refresh, or API call can heal it. Treat these events as tombstones, not retryable errors. Rotating the conversation key starts a clean, verifiable history from that point forward; new messages are unaffected.
@@ -184,8 +189,8 @@ These mistakes are specific to X Chat encryption (not general HTTP errors):
|:------|:----|
| Wrong key bytes | Pass the **raw** conversation key bytes into the Chat XDK, not the encrypted key string from the API |
| Wrong JSON field names | Map `encrypted_content` → `encoded_message_create_event` and `encoded_event_signature` → `encoded_message_event_signature` |
-| Missing message id | Generate `message_id` yourself and send the same value in the request body |
-| Version mismatch | Align `conversation_key_version` with the key you use; align signing key version with `set_key_version` / your public-key record |
+| Wrong message id | Send the `message_id` from the returned payload—the SDK generates it and embeds it in the signed event, so any other value fails. On retries, reuse the same encrypted payload so the id is never minted twice |
+| Version mismatch | Align `conversation_key_version` with the key you use; align the signing key version passed to `set_identity` with your public-key record |
| Path id form | URL paths still need the hyphenated conversation id (`:` → `-`), but for signing the SDK accepts any form: `A:B`, `A-B` (either order), or the bare recipient user id—all canonicalize to the same signed bytes |
### API returns 400 for a state-changing call
@@ -210,5 +215,5 @@ When investigating crypto failures:
- Log conversation ids, event ids, and key **versions** only
- Do **not** log plaintext, passcodes, private keys, or full key blobs
-- Confirm `set_key_version` matches the `public_key_version` on your public-key record
+- Confirm the signing-key version passed to `set_identity` matches the `public_key_version` on your public-key record
- For incomplete history, page **all** event pages so key-change metadata is not skipped before decrypting
diff --git a/xchat/xchat-xdk.mdx b/xchat/xchat-xdk.mdx
index 9518bfa35..dae926384 100644
--- a/xchat/xchat-xdk.mdx
+++ b/xchat/xchat-xdk.mdx
@@ -32,7 +32,7 @@ App walkthrough: [Getting Started](/xchat/getting-started). Sample bots: [chat-x
[dependencies]
# chat-xdk-core is not yet on crates.io — use the git dependency.
# It exports both ChatCore and the async secure-key-backup Chat type.
- chat-xdk-core = { git = "https://github.com/xdevplatform/chat-xdk", tag = "v0.2.1" }
+ chat-xdk-core = { git = "https://github.com/xdevplatform/chat-xdk", tag = "v0.4.0" }
# Required until thrift 0.24 is released on crates.io
[patch.crates-io]
@@ -58,7 +58,7 @@ App walkthrough: [Getting Started](/xchat/getting-started). Sample bots: [chat-x
com.x
chatxdk
- 0.2.1
+ 0.4.0
```
@@ -70,31 +70,37 @@ App walkthrough: [Getting Started](/xchat/getting-started). Sample bots: [chat-x
## Quick start
-Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send body to [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) as in [Getting Started](/xchat/getting-started).
+Load keys, set your identity once, decrypt a backlog, decrypt one live event, encrypt a message. Wire the send body to [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) as in [Getting Started](/xchat/getting-started).
+
+The snippets use the two **optional** session stores for the shortest call forms: `set_signing_keys` holds the other participants' public keys (fetched from the [public-keys endpoint](/x-api/chat/get-user-public-keys)) so decrypt calls can verify senders without a per-call argument, and `set_cache_keys(true)` lets the SDK remember each conversation's verified key so encrypt calls need only the conversation id and text. Skip either and pass the same values per call instead—both styles verify identically; see [Decrypt](#decrypt).
```python
from chat_xdk import Chat
- chat = Chat(juicebox_config_json) # or Chat() + import_keys(blob)
+ chat = Chat(juicebox_config_json) # or Chat() + import_keys(blob, version)
chat.unlock("YOUR_PASSCODE")
- chat.set_key_version(signing_key_version)
- result = chat.decrypt_events(raw_events, signing_keys)
+ # Session defaults: identity for signing, stored signing keys for
+ # verification, opt-in cache for conversation keys
+ chat.set_identity(my_user_id, signing_key_version)
+ chat.set_signing_keys(signing_keys) # all participants
+ chat.set_cache_keys(True)
+
+ # Batch-decrypt the backlog; senders verify against the stored keys
+ result = chat.decrypt_events(raw_events)
for dm in result["messages"]:
ev = dm["event"]
- if ev.get("type") == "Message":
- print(ev.get("sender_id"), ev.get("content", {}).get("text"))
+ if ev["type"] == "Message":
+ print(ev["sender_id"], ev["content"]["text"])
- cached = result["conversation_keys"]["keys"]
- event = chat.decrypt_event(one_event_b64, cached, sender_signing_keys)
+ # Decrypt one live event with the cached conversation key
+ event = chat.decrypt_event(one_event_b64)
- raw_key = cached[result["conversation_keys"]["latest_version"]]
- payload = chat.encrypt_message(
- message_id, sender_id, conversation_id, raw_key, "Hi!",
- conversation_key_version, signing_key_version,
- )
+ # Encrypt and sign as the session identity, under the cached key
+ payload = chat.encrypt_message(event["conversation_id"], "Hi!")
+ message_id = payload.message_id # SDK-generated — send as message_id
```
@@ -106,38 +112,53 @@ Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send
getAuthToken: async (realmId) => getRealmToken(realmId),
});
await chat.unlock('YOUR_PASSCODE');
- chat.setKeyVersion(signingKeyVersion);
- const result = chat.decryptEvents(rawEvents, signingKeys);
+ // Session defaults: identity for signing, stored signing keys for
+ // verification, opt-in cache for conversation keys
+ chat.setIdentity(myUserId, signingKeyVersion);
+ chat.setSigningKeys(signingKeys); // all participants
+ chat.setCacheKeys(true);
+
+ // Batch-decrypt the backlog; senders verify against the stored keys
+ const result = chat.decryptEvents(rawEvents);
for (const dm of result.messages) {
if (dm.event.type === 'message') {
console.log(dm.event.senderId, dm.event.content?.text);
}
}
- const cached = result.conversationKeys.keys;
- const event = chat.decryptEvent(oneEventB64, cached, senderSigningKeys);
+ // Decrypt one live event with the cached conversation key
+ const event = chat.decryptEvent(oneEventB64);
- const rawKey = cached[result.conversationKeys.latestVersion!];
- const payload = chat.encryptMessage({
- messageId, senderId, conversationId, conversationKey: rawKey, text: 'Hi!',
- conversationKeyVersion, signingKeyVersion,
- });
+ // Encrypt and sign as the session identity, under the cached key
+ const payload = chat.encryptMessage({ conversationId: event.conversationId!, text: 'Hi!' });
+ const messageId = payload.messageId; // SDK-generated — send as message_id
```
```rust
- // ChatCore + import_keys, or chat_xdk_core::Chat + unlock().await
- let result = chat.decrypt_events(&raw_events, &signing_keys);
- let cached = &result.conversation_keys.keys;
- let event = chat.decrypt_event(one_event_b64, cached, &sender_signing_keys)?;
- // cached values are XChatConversationKey; encrypt_message wants owned bytes
- let latest = result.conversation_keys.latest_version.as_deref().unwrap_or_default();
- let conv_key = cached[latest].to_bytes();
- let payload = chat.encrypt_message(EncryptMessageParams::new(
- &message_id, &sender_id, &conversation_id, conv_key, "Hi!",
- &conversation_key_version, &signing_key_version,
- ))?;
+ // chat_xdk_core::Chat + unlock(b"…").await, or ChatCore + import_keys_with_version
+
+ // Session defaults: identity for signing, stored signing keys for
+ // verification, opt-in cache for conversation keys
+ chat.set_identity(my_user_id, signing_key_version);
+ chat.set_signing_keys(signing_keys); // all participants
+ chat.set_cache_keys(true);
+
+ // Batch-decrypt the backlog; senders verify against the stored keys
+ let result = chat.decrypt_events(&raw_events, &[]);
+ for dm in &result.messages {
+ if let Event::Message(msg) = &dm.event {
+ println!("{}: {}", msg.meta.sender_id.as_deref().unwrap_or("?"), msg.text().unwrap_or(""));
+ }
+ }
+
+ // Decrypt one live event with the cached conversation key
+ let event = chat.decrypt_event(one_event_b64, &Default::default(), &[])?;
+
+ // Encrypt and sign as the session identity, under the cached key
+ let payload = chat.encrypt_message(EncryptMessageParams::new(conversation_id, "Hi!"))?;
+ let message_id = payload.message_id; // SDK-generated — send as message_id
```
@@ -145,58 +166,90 @@ Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send
chat := chatxdk.New()
defer chat.Close()
blob, _ := chatxdk.Base64ToBytes(privateKeysB64)
- _ = chat.ImportKeys(blob)
- chat.SetKeyVersion(signingKeyVersion)
+ _ = chat.ImportKeysWithVersion(blob, signingKeyVersion)
+
+ // Session defaults: identity for signing, stored signing keys for
+ // verification, opt-in cache for conversation keys
+ chat.SetIdentity(myUserID, signingKeyVersion)
+ _ = chat.SetSigningKeys(signingKeys) // all participants
+ chat.SetCacheKeys(true)
+
+ // Batch-decrypt the backlog; senders verify against the stored keys
+ result, err := chat.DecryptEvents(rawEvents, nil)
+ for _, dm := range result.Messages {
+ if dm.Event.Type == "Message" {
+ fmt.Println(dm.Event.AsMessage().Text())
+ }
+ }
- result, err := chat.DecryptEvents(rawEvents, signingKeys)
- cached := result.ConversationKeys.Keys
- event, err := chat.DecryptEvent(oneEventB64, cached, senderSigningKeys)
- rawKey := cached[*result.ConversationKeys.LatestVersion]
+ // Decrypt one live event with the cached conversation key
+ event, err := chat.DecryptEvent(oneEventB64, nil, nil)
+ msg := event.AsMessage() // nil unless event.Type == "Message"
+
+ // Encrypt and sign as the session identity, under the cached key
payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
- MessageID: messageID, SenderID: senderID, ConversationID: conversationID,
- ConversationKey: rawKey, Text: "Hi!",
- ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion,
+ ConversationID: *msg.ConversationID,
+ Text: "Hi!",
})
- _ = event
- _ = payload
+ messageID := payload.MessageID // SDK-generated — send as message_id
+ _ = messageID
_ = err
```
```csharp
using var chat = new Chat();
- chat.ImportKeys(privateKeyBytes);
- chat.SetKeyVersion(signingKeyVersion);
+ chat.ImportKeys(privateKeyBytes, signingKeyVersion);
+
+ // Session defaults: identity for signing, stored signing keys for
+ // verification, opt-in cache for conversation keys
+ chat.SetIdentity(myUserId, signingKeyVersion);
+ chat.SetSigningKeys(signingKeys); // all participants
+ chat.SetCacheKeys(true);
+
+ // Batch-decrypt the backlog; senders verify against the stored keys
+ var result = chat.DecryptEvents(rawEvents);
+ foreach (var dm in result.Messages)
+ {
+ if (dm.Event.GetProperty("type").GetString() == "Message")
+ Console.WriteLine(dm.Event.GetProperty("content").GetProperty("text").GetString());
+ }
- var result = chat.DecryptEvents(rawEvents, signingKeys);
- var cached = result.ConversationKeys.Keys;
- var evt = chat.DecryptEvent(oneEventB64, cached, senderSigningKeys);
- var payload = chat.EncryptMessage(new EncryptMessageParams {
- MessageId = messageId, SenderId = senderId, ConversationId = conversationId,
- ConversationKey = rawKey, Text = "Hi!",
- ConversationKeyVersion = conversationKeyVersion, SigningKeyVersion = signingKeyVersion,
- });
+ // Decrypt one live event with the cached conversation key
+ var evt = chat.DecryptEvent(oneEventB64);
+ var conversationId = evt.GetProperty("conversation_id").GetString()!;
+
+ // Encrypt and sign as the session identity, under the cached key
+ var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hi!"));
+ var messageId = payload.MessageId; // SDK-generated — send as message_id
```
```java
try (Chat chat = new Chat()) {
- chat.importKeys(privateKeyBytes);
- chat.setKeyVersion(signingKeyVersion);
-
- DecryptEventsResult result = chat.decryptEvents(rawEvents, signingKeys);
- Map cached = result.conversationKeys.keys;
- JsonNode event = chat.decryptEvent(oneEventB64, cached, senderSigningKeys);
-
- EncryptMessageParams params = new EncryptMessageParams();
- params.messageId = messageId;
- params.senderId = senderId;
- params.conversationId = conversationId;
- params.conversationKey = rawKey;
- params.text = "Hi!";
- params.conversationKeyVersion = conversationKeyVersion;
- params.signingKeyVersion = signingKeyVersion;
- SendPayload payload = chat.encryptMessage(params);
+ chat.importKeys(privateKeyBytes, signingKeyVersion);
+
+ // Session defaults: identity for signing, stored signing keys for
+ // verification, opt-in cache for conversation keys
+ chat.setIdentity(myUserId, signingKeyVersion);
+ chat.setSigningKeys(signingKeys); // all participants
+ chat.setCacheKeys(true);
+
+ // Batch-decrypt the backlog; senders verify against the stored keys
+ DecryptEventsResult result = chat.decryptEvents(rawEvents, null);
+ for (DecryptedMessage dm : result.messages) {
+ if ("Message".equals(dm.event.path("type").asText())) {
+ System.out.println(dm.event.path("content").path("text").asText());
+ }
+ }
+
+ // Decrypt one live event with the cached conversation key
+ JsonNode event = chat.decryptEvent(oneEventB64, (Map) null, null);
+ String conversationId = event.path("conversation_id").asText();
+
+ // Encrypt and sign as the session identity, under the cached key
+ SendPayload payload = chat.encryptMessage(new EncryptMessageParams(conversationId, "Hi!"));
+ String messageId = payload.messageId; // SDK-generated — send as message_id
}
```
@@ -206,7 +259,9 @@ Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send
## Lifecycle and keys
-Construct the SDK, store private keys (passcode-protected secure key backup or a local key blob), register **public** keys with the Chat API, and set your registered **public-key version** after unlock or import. Call `generate_keypairs` once per device/app identity; post the registration payload to the public-keys endpoint. Use `setup` / `unlock` (and related passcode helpers) for secure key backup on every binding. `export_keys` / `import_keys` (raw key-blob persistence for bots and servers) are available on the **native bindings only**—Python, Go, .NET, JVM, and Rust. The JS/WASM binding does not expose raw key export or import: in a browser any script that reaches the instance could exfiltrate the identity, so JS keeps keys inside secure key backup. A JS server that wants to avoid a backup-realm round-trip per request should reuse one unlocked `Chat` instance across requests, or run a native binding where key blobs are supported.
+Construct the SDK, store private keys (passcode-protected secure key backup or a local key blob), register **public** keys with the Chat API, and call **`set_identity(user_id, signing_key_version)`** after unlock or import—it sets the sender and signing-key version every signed action defaults to, so the encrypt and prepare methods work without per-call identity arguments. Call `generate_keypairs` once per device/app identity; post the registration payload to the public-keys endpoint. Use `setup` / `unlock` (and related passcode helpers) for secure key backup on every binding. `export_keys` / `import_keys` (raw key-blob persistence for bots and servers) are available on the **native bindings only**—Python, Go, .NET, JVM, and Rust. The JS/WASM binding does not expose raw key export or import: in a browser any script that reaches the instance could exfiltrate the identity, so JS keeps keys inside secure key backup. A JS server that wants to avoid a backup-realm round-trip per request should reuse one unlocked `Chat` instance across requests, or run a native binding where key blobs are supported.
+
+The SDK also needs the version the X API reports for your registered public key, so key-change entries targeting other versions are skipped. `set_identity` records it together with the user id; `import_keys` accepts it directly as an optional argument (Rust and Go use `import_keys_with_version` / `ImportKeysWithVersion`).
@@ -217,13 +272,13 @@ Construct the SDK, store private keys (passcode-protected secure key backup or a
chat = Chat(juicebox_config_json)
chat.setup("YOUR_PASSCODE") # first time — generates keypairs
# chat.unlock("YOUR_PASSCODE") # later sessions
- chat.set_key_version(version) # from add-public-key / get-public-keys response
+ chat.set_identity(user_id, version) # version from add-public-key / get-public-keys response
reg = chat.get_public_keys() # or registration fields from generate_keypairs
# Key blob (server / bot)
chat2 = Chat()
- chat2.import_keys(secret_blob)
- chat2.set_key_version(version)
+ chat2.import_keys(secret_blob, version)
+ chat2.set_identity(user_id, version)
blob = chat2.export_keys() # treat as a password
```
@@ -237,7 +292,7 @@ Construct the SDK, store private keys (passcode-protected secure key backup or a
});
await chat.setup('YOUR_PASSCODE');
// await chat.unlock('YOUR_PASSCODE');
- chat.setKeyVersion(version);
+ chat.setIdentity(userId, version);
const publics = chat.getPublicKeys();
// JS/WASM stores keys only through secure key backup — there is no raw key
@@ -247,12 +302,12 @@ Construct the SDK, store private keys (passcode-protected secure key backup or a
```rust
// chat_xdk_core::Chat — async secure key backup unlock, or ChatCore + import_keys
- chat.setup("YOUR_PASSCODE").await?;
- // chat.unlock("YOUR_PASSCODE").await?;
- chat.set_key_version(&version);
+ chat.setup(b"YOUR_PASSCODE").await?;
+ // chat.unlock(b"YOUR_PASSCODE").await?;
+ chat.set_identity(user_id, version);
let publics = chat.get_public_keys()?;
let blob = chat.export_keys()?;
- chat.import_keys(&blob)?;
+ chat.import_keys_with_version(&blob, version)?;
```
@@ -262,10 +317,10 @@ Construct the SDK, store private keys (passcode-protected secure key backup or a
// Prefer ImportKeys for servers; secure key backup unlock where supported
keyBlob, _ := chatxdk.Base64ToBytes(privateKeysB64)
- if err := chat.ImportKeys(keyBlob); err != nil {
+ if err := chat.ImportKeysWithVersion(keyBlob, version); err != nil {
log.Fatal(err)
}
- chat.SetKeyVersion(version)
+ chat.SetIdentity(userID, version)
publics, err := chat.GetPublicKeys()
blob, err := chat.ExportKeys()
_ = publics
@@ -276,9 +331,9 @@ Construct the SDK, store private keys (passcode-protected secure key backup or a
```csharp
using var chat = new Chat();
- chat.ImportKeys(privateKeyBytes);
+ chat.ImportKeys(privateKeyBytes, version);
// or secure key backup setup / unlock when config is available
- chat.SetKeyVersion(version);
+ chat.SetIdentity(userId, version);
var publics = chat.GetPublicKeys();
var blob = chat.ExportKeys();
```
@@ -286,8 +341,8 @@ Construct the SDK, store private keys (passcode-protected secure key backup or a
```java
try (Chat chat = new Chat()) {
- chat.importKeys(privateKeyBytes);
- chat.setKeyVersion(version);
+ chat.importKeys(privateKeyBytes, version);
+ chat.setIdentity(userId, version);
var publics = chat.getPublicKeys();
byte[] blob = chat.exportKeys();
}
@@ -303,7 +358,7 @@ Optional: signature verification is **on by default** (`reject_unverified = true
## Conversation keys
-Three **prepare** methods each make one call do everything a key change needs: generate a fresh conversation key, encrypt it for every participant (from the public keys you pass), and sign the change. All return the same **`PreparedConversationChange`** shape, ready to POST—rename SDK field `encrypted_key` to **`encrypted_conversation_key`** in `conversation_participant_keys`, and map the action signatures into the required **`action_signatures`** body field.
+Three **prepare** methods each make one call do everything a key change needs: generate a fresh conversation key, encrypt it for every participant (from the public keys you pass), and sign the change. The sender identity and signing-key version come from the session (`set_identity`); set `sender_id` / `signing_key_version` on the params to override. All return the same **`PreparedConversationChange`** shape, ready to POST—rename SDK field `encrypted_key` to **`encrypted_conversation_key`** in `conversation_participant_keys`, and map the action signatures into the required **`action_signatures`** body field.
| Scenario | Method | Action signatures returned |
|:---------|:-------|:---------------------------|
@@ -327,7 +382,7 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys,
# {"user_id": "1215441834412953600", "public_key": "BASE64_IDENTITY_PUBLIC_KEY", "key_version": "1733889755256"},
# {"user_id": "1843439638876491776", "public_key": "BASE64_IDENTITY_PUBLIC_KEY", "key_version": "1766181805686"},
# ]
- prepared = chat.prepare_conversation_key_change(my_user_id, signing_key_version, participants)
+ prepared = chat.prepare_conversation_key_change(participants)
# prepared["conversation_key"] — raw bytes for encrypt_message
# prepared["participant_keys"] — per-user wraps; rename encrypted_key → encrypted_conversation_key on POST
# prepared["action_signatures"] — required on the POST body
@@ -342,9 +397,7 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys,
```typescript
- const prepared = chat.prepareConversationKeyChange({
- senderId: myUserId, signingKeyVersion, publicKeys: participants,
- });
+ const prepared = chat.prepareConversationKeyChange({ publicKeys: participants });
// prepared.conversationKey — Uint8Array for encryptMessage
// prepared.participantKeys / prepared.actionSignatures — POST body fields
@@ -357,7 +410,7 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys,
```rust
let prepared = chat.prepare_conversation_key_change(
- ConversationKeyChangeParams::new(&my_user_id, &signing_key_version, participants),
+ ConversationKeyChangeParams::new(participants),
)?;
let extracted = chat.extract_conversation_keys(&key_change_blobs);
let latest = extracted.latest_version.as_deref().unwrap_or_default();
@@ -368,7 +421,7 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys,
```go
prepared, err := chat.PrepareConversationKeyChange(chatxdk.ConversationKeyChangeParams{
- SenderID: myUserID, SigningKeyVersion: signingKeyVersion, PublicKeys: participants,
+ PublicKeys: participants,
})
// prepared.ConversationKey feeds EncryptMessage
// prepared.ParticipantKeys / prepared.ActionSignatures — POST body fields
@@ -382,9 +435,7 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys,
```csharp
- var prepared = chat.PrepareConversationKeyChange(new ConversationKeyChangeParams {
- SenderId = myUserId, SigningKeyVersion = signingKeyVersion, PublicKeys = participants,
- });
+ var prepared = chat.PrepareConversationKeyChange(new ConversationKeyChangeParams(participants));
var extracted = chat.ExtractConversationKeys(keyChangeBlobs);
var raw = extracted.Keys[extracted.LatestVersion];
var one = chat.DecryptConversationKey(encryptedBlob);
@@ -392,11 +443,8 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys,
```java
- ConversationKeyChangeParams keyParams = new ConversationKeyChangeParams();
- keyParams.senderId = myUserId;
- keyParams.signingKeyVersion = signingKeyVersion;
- keyParams.publicKeys = participants;
- PreparedConversationChange prepared = chat.prepareConversationKeyChange(keyParams);
+ PreparedConversationChange prepared =
+ chat.prepareConversationKeyChange(new ConversationKeyChangeParams(participants));
ConversationKeyBundle extracted = chat.extractConversationKeys(keyChangeBlobs);
byte[] raw = extracted.keys.get(extracted.latestVersion);
byte[] one = chat.decryptConversationKey(encryptedBlob);
@@ -410,9 +458,18 @@ For group create and member adds, pass the params each method needs (member/admi
## Decrypt
-**`decrypt_events`** is for history and backlog: it pulls conversation keys from the stream, returns decrypted messages, and **collects** per-event errors instead of failing the whole batch. **`decrypt_event`** is for a single live event when you already have a key cache; it raises/throws on failure.
+**`decrypt_events`** is for history and backlog: it pulls conversation keys from the stream, returns decrypted messages, and **collects** per-event errors instead of failing the whole batch. **`decrypt_event`** is for a single live event; it raises/throws on failure.
+
+Pass **signing keys** so the SDK can verify senders. Map API public-key fields into `SigningKeyEntry`: `public_key_version` → `public_key_version` (same name), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, plus `identity_public_key_signature` and `user_id`.
+
+Two opt-in session stores let you omit the per-call key arguments:
-Pass **signing keys** so the SDK can verify senders. Map API public-key fields into `SigningKeyEntry`: `public_key_version` → `public_key_version` (same name), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, plus `identity_public_key_signature` and `user_id`. Verification is mandatory by default: omitting or passing an empty signing-key list does **not** skip it—signed events fail (collected in `errors` for `decrypt_events`, thrown for `decrypt_event`). To actually skip verification you must first call `set_reject_unverified(false)` (not recommended in production).
+- **`set_signing_keys(entries)`** stores participant signing keys; a decrypt call that omits (or passes an empty) signing-keys argument uses the store instead. Verification itself is unchanged—keys enter the store only through this call, never from the events being decrypted. Each call replaces the previous set.
+- **`set_cache_keys(true)`** enables the conversation-key cache (off by default). While enabled, `decrypt_events` caches, per conversation, the latest key whose key change carried a valid signature; `decrypt_event` falls back to it when its conversation-keys argument is omitted, and the encrypt helpers resolve an omitted conversation key from it. Disabling clears the cache.
+
+An explicit non-empty argument always wins over the stores. Explicit per-call arguments remain first-class—and are the right choice for serverless or multi-instance deployments, where a request can land on a fresh instance whose stores are empty.
+
+Verification is mandatory by default: omitting signing keys never skips it. With nothing passed and nothing stored, signed events fail (collected in `errors` for `decrypt_events`, thrown for `decrypt_event`). To actually skip verification you must first call `set_reject_unverified(false)` (not recommended in production).
@@ -430,11 +487,11 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i
log.warning("event %s failed: %s", idx, msg)
for dm in result["messages"]:
ev = dm["event"]
- if ev.get("type") == "Message":
- text = ev.get("content", {}).get("text")
+ if ev["type"] == "Message":
+ text = ev["content"].get("text")
cached = result["conversation_keys"]["keys"]
- live = chat.decrypt_event(one_event_b64, cached, signing_keys_for_sender)
+ live = chat.decrypt_event(one_event_b64, cached, signing_keys)
```
@@ -452,7 +509,7 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i
console.warn(`event ${idx} failed: ${msg}`);
}
const cached = result.conversationKeys.keys;
- const live = chat.decryptEvent(oneEventB64, cached, signingKeysForSender);
+ const live = chat.decryptEvent(oneEventB64, cached, signingKeys);
```
@@ -462,7 +519,7 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i
eprintln!("event {idx} failed: {msg}");
}
let cached = &result.conversation_keys.keys;
- let live = chat.decrypt_event(one_event_b64, cached, &signing_keys_for_sender)?;
+ let live = chat.decrypt_event(one_event_b64, cached, &signing_keys)?;
```
@@ -472,7 +529,7 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i
log.Printf("event %s failed: %s", idx, msg)
}
cached := result.ConversationKeys.Keys
- live, err := chat.DecryptEvent(oneEventB64, cached, signingKeysForSender)
+ live, err := chat.DecryptEvent(oneEventB64, cached, signingKeys)
_ = live
_ = err
```
@@ -482,14 +539,14 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i
var result = chat.DecryptEvents(rawEvents, signingKeys);
foreach (var kv in result.Errors) { /* kv.Key = event index, kv.Value = error */ }
var cached = result.ConversationKeys.Keys;
- var live = chat.DecryptEvent(oneEventB64, cached, signingKeysForSender);
+ var live = chat.DecryptEvent(oneEventB64, cached, signingKeys);
```
```java
DecryptEventsResult result = chat.decryptEvents(rawEvents, signingKeys);
Map cached = result.conversationKeys.keys;
- JsonNode live = chat.decryptEvent(oneEventB64, cached, signingKeysForSender);
+ JsonNode live = chat.decryptEvent(oneEventB64, cached, signingKeys);
```
@@ -498,9 +555,15 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i
## Encrypt and send helpers
-**`encrypt_message`** builds the signed ciphertext for a text message (optional entities, attachments via `media_hash_key`, TTL, notify flags). Map the returned payload into the send-message body: `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**, plus your **`message_id`**.
+**`encrypt_message(conversation_id, text)`** builds the signed ciphertext for a text message; optional `entities`, `attachments` (via `media_hash_key`), `should_notify`, and `ttl_msec`. The sender identity resolves from the session (`set_identity`) and the conversation key from the opt-in key cache (`set_cache_keys`)—or pass `sender_id` / `signing_key_version` and `conversation_key` + `conversation_key_version` explicitly. The SDK generates the **`message_id`** (a UUID embedded in the signed event) and returns it on the payload—never mint your own; reuse the same payload on retries so an id is never minted twice. Map the payload into the send-message body: `message_id` → **`message_id`**, `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**.
+
+**Replies are event-based.** `encrypt_reply(conversation_id, text, reply_to_event)` takes the base64 raw event being replied to. The SDK derives the quoted preview (sequence id, sender, text, entities, attachments) from it and embeds the signed original in the outgoing message so recipients can validate the quote. Pass `reply_to_ckces`—the raw key-change events—when the original was encrypted under an older key version than the reply. When the original was **edited**, pass the raw edit event as `reply_to_edit_event`: the preview then quotes what the message says now (its text and entities come from the edit), and the edit travels alongside the original for the receiver to check. The explicit `reply_to_*` fields remain as overrides for callers that no longer hold the raw event.
-Use **`encrypt_reply`**, **`encrypt_add_reaction`**, and **`encrypt_remove_reaction`** for replies and reactions (`sequence_id` targets the parent). **`encrypt` / `decrypt`** are for UTF-8 metadata under the conversation key (for example an encrypted group name)—not message envelopes. **`encrypt_stream` / `decrypt_stream`** encrypt attachment bytes; see [Media](/xchat/media). Low-level **`sign` / `verify` / `verify_key_binding`** support advanced flows; conversation-key changes, group creates, and member adds are signed by the [prepare methods](#conversation-keys).
+**Reactions are event-based too.** `encrypt_add_reaction(target_event, emoji)` and `encrypt_remove_reaction(...)` derive the conversation id and target sequence id from the raw event being reacted to; the same params can add and later remove a reaction. Set `conversation_id` and `target_message_sequence_id` explicitly only when you no longer hold the raw event.
+
+On the receiving side, a decrypted message that quotes a reply carries **`reply_preview_validation`** (`"Valid"` / `"Invalid"`; the JS binding uses `'valid'` / `'invalid'`): the SDK verified the embedded original's signature against your signing keys—never a key carried in the event—decrypted it, and compared the quoted content and author against it. When the preview embeds an edit event, the SDK verifies the edit the same way (same conversation, same author as the original) and checks the quoted text against the edited contents rather than the pre-edit text. The field is absent when the message carries no preview or the preview embeds no original. Treat `Invalid` previews as untrusted: the message itself is authentic, but the quoted material is not—render quotes only from the validated original.
+
+**`encrypt` / `decrypt`** are for UTF-8 metadata under the conversation key (for example an encrypted group name)—not message envelopes. **`encrypt_stream` / `decrypt_stream`** encrypt attachment bytes; see [Media](/xchat/media). Low-level **`sign` / `verify` / `verify_key_binding`** support advanced flows; conversation-key changes, group creates, and member adds are signed by the [prepare methods](#conversation-keys).
The conversation id passed to `encrypt_message` / `encrypt_reply` can be any form you hold—`A:B` from events, `A-B` from listings or URL paths (in either order), or the bare recipient user id—the SDK canonicalizes it before signing. Group ids (prefixed with `g`) pass through unchanged.
@@ -508,22 +571,24 @@ The conversation id passed to `encrypt_message` / `encrypt_reply` can be any for
```python
payload = chat.encrypt_message(
- message_id, sender_id, conversation_id, raw_conversation_key, "Hello",
- conversation_key_version, signing_key_version,
+ conversation_id, "Hello",
# Optional keyword args: entities, attachments, should_notify, ttl_msec
)
body = {
- "message_id": message_id,
- "encoded_message_create_event": payload["encrypted_content"],
- "encoded_message_event_signature": payload["encoded_event_signature"],
+ "message_id": payload.message_id,
+ "encoded_message_create_event": payload.encrypted_content,
+ "encoded_message_event_signature": payload.encoded_event_signature,
}
# POST body to /2/chat/conversations/{id}/messages
- reply = chat.encrypt_reply(
- reply_message_id, sender_id, conversation_id, raw_conversation_key,
- "Sounds good", conversation_key_version, signing_key_version,
- parent_sequence_id, # reply_to_sequence_id — the message being replied to
- )
+ # Preview derived from + embedded raw event so recipients can validate;
+ # add reply_to_ckces=[...] when the original used an older key version
+ reply = chat.encrypt_reply(conversation_id, "Sounds good", original_event_b64)
+
+ # Conversation and target derived from the raw event
+ add = chat.encrypt_add_reaction(original_event_b64, "👍")
+ remove = chat.encrypt_remove_reaction(original_event_b64, "👍")
+
name_ct = chat.encrypt("Group title", raw_conversation_key)
title = chat.decrypt(name_ct, raw_conversation_key)
```
@@ -531,32 +596,52 @@ The conversation id passed to `encrypt_message` / `encrypt_reply` can be any for
```typescript
const payload = chat.encryptMessage({
- messageId, senderId, conversationId, conversationKey: rawConversationKey, text: 'Hello',
- conversationKeyVersion, signingKeyVersion,
+ conversationId,
+ text: 'Hello',
+ // Optional: entities, attachments, shouldNotify, ttlMsec
});
const body = {
- message_id: messageId,
+ message_id: payload.messageId,
encoded_message_create_event: payload.encryptedContent,
encoded_message_event_signature: payload.encodedEventSignature,
};
+ // POST body to /2/chat/conversations/{id}/messages
+ // Preview derived from + embedded raw event so recipients can validate;
+ // add replyToCkces: [...] when the original used an older key version
const reply = chat.encryptReply({
- messageId: replyMessageId, senderId, conversationId, conversationKey: rawConversationKey,
- text: 'Sounds good', conversationKeyVersion, signingKeyVersion,
- replyToSequenceId: parentSequenceId, // the message being replied to
+ conversationId,
+ text: 'Sounds good',
+ replyToEvent: originalEventB64,
});
+
+ // Conversation and target derived from the raw event
+ const add = chat.encryptAddReaction({ emoji: '👍', targetEvent: originalEventB64 });
+ const remove = chat.encryptRemoveReaction({ emoji: '👍', targetEvent: originalEventB64 });
+
const nameCt = chat.encrypt('Group title', rawConversationKey);
const title = chat.decrypt(nameCt, rawConversationKey);
```
```rust
- // conv_key: XChatConversationKey from extract_conversation_keys / decrypt_conversation_key
- let payload = chat.encrypt_message(EncryptMessageParams::new(
- &message_id, &sender_id, &conversation_id, conv_key.to_bytes(), "Hello",
- &conversation_key_version, &signing_key_version,
+ let payload = chat.encrypt_message(EncryptMessageParams::new(conversation_id, "Hello"))?;
+ // Send body: payload.message_id → message_id,
+ // payload.encrypted_content → encoded_message_create_event,
+ // payload.encoded_event_signature → encoded_message_event_signature
+
+ // Preview derived from + embedded raw event so recipients can validate;
+ // set params.reply_to_ckces when the original used an older key version
+ let reply = chat.encrypt_reply(EncryptReplyParams::new(
+ conversation_id, "Sounds good", original_event_b64,
))?;
- // Map payload fields into the send-message JSON body as above
+
+ // Conversation and target derived from the raw event
+ let reaction = EncryptReactionParams::new(original_event_b64, "👍");
+ let add = chat.encrypt_add_reaction(&reaction)?;
+ let remove = chat.encrypt_remove_reaction(&reaction)?;
+
+ // conv_key: XChatConversationKey from extract_conversation_keys / decrypt_conversation_key
let name_ct = chat.encrypt("Group title", &conv_key)?;
let title = chat.decrypt(&name_ct, &conv_key)?;
```
@@ -564,42 +649,72 @@ The conversation id passed to `encrypt_message` / `encrypt_reply` can be any for
```go
payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
- MessageID: messageID, SenderID: senderID, ConversationID: conversationID,
- ConversationKey: rawKey, Text: "Hello",
- ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion,
+ ConversationID: conversationID,
+ Text: "Hello",
})
- // body: message_id, encoded_message_create_event, encoded_message_event_signature
+ // Send body: payload.MessageID → message_id,
+ // payload.EncryptedContent → encoded_message_create_event,
+ // payload.EncodedEventSignature → encoded_message_event_signature
+
+ // Preview derived from + embedded raw event so recipients can validate;
+ // set ReplyToCkces when the original used an older key version
+ reply, err := chat.EncryptReply(chatxdk.EncryptReplyParams{
+ ConversationID: conversationID,
+ Text: "Sounds good",
+ ReplyToEvent: originalEventB64,
+ })
+
+ // Conversation and target derived from the raw event
+ reaction := chatxdk.EncryptReactionParams{Emoji: "👍", TargetEvent: originalEventB64}
+ add, err := chat.EncryptAddReaction(reaction)
+ remove, err := chat.EncryptRemoveReaction(reaction)
+
nameCt, err := chat.Encrypt("Group title", rawKey)
title, err := chat.Decrypt(nameCt, rawKey)
_ = payload
+ _ = reply
+ _ = add
+ _ = remove
_ = title
_ = err
```
```csharp
- var payload = chat.EncryptMessage(new EncryptMessageParams {
- MessageId = messageId, SenderId = senderId, ConversationId = conversationId,
- ConversationKey = rawKey, Text = "Hello",
- ConversationKeyVersion = conversationKeyVersion, SigningKeyVersion = signingKeyVersion,
- });
- // Map EncryptedContent / EncodedEventSignature into the send-message body
+ var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hello"));
+ // Send body: payload.MessageId → message_id,
+ // payload.EncryptedContent → encoded_message_create_event,
+ // payload.EncodedEventSignature → encoded_message_event_signature
+
+ // Preview derived from + embedded raw event so recipients can validate;
+ // set ReplyToCkces when the original used an older key version
+ var reply = chat.EncryptReply(new EncryptReplyParams(conversationId, "Sounds good", originalEventB64));
+
+ // Conversation and target derived from the raw event
+ var reaction = new EncryptReactionParams(originalEventB64, "👍");
+ var add = chat.EncryptAddReaction(reaction);
+ var remove = chat.EncryptRemoveReaction(reaction);
+
var nameCt = chat.Encrypt("Group title", rawKey);
var title = chat.Decrypt(nameCt, rawKey);
```
```java
- EncryptMessageParams params = new EncryptMessageParams();
- params.messageId = messageId;
- params.senderId = senderId;
- params.conversationId = conversationId;
- params.conversationKey = rawKey;
- params.text = "Hello";
- params.conversationKeyVersion = conversationKeyVersion;
- params.signingKeyVersion = signingKeyVersion;
- SendPayload payload = chat.encryptMessage(params);
- // Map to encoded_message_create_event / encoded_message_event_signature on POST
+ SendPayload payload = chat.encryptMessage(new EncryptMessageParams(conversationId, "Hello"));
+ // Send body: payload.messageId → message_id,
+ // payload.encryptedContent → encoded_message_create_event,
+ // payload.encodedEventSignature → encoded_message_event_signature
+
+ // Preview derived from + embedded raw event so recipients can validate;
+ // set replyToCkces when the original used an older key version
+ SendPayload reply =
+ chat.encryptReply(new EncryptReplyParams(conversationId, "Sounds good", originalEventB64));
+
+ // Conversation and target derived from the raw event
+ EncryptReactionParams reaction = new EncryptReactionParams(originalEventB64, "👍");
+ SendPayload add = chat.encryptAddReaction(reaction);
+ SendPayload remove = chat.encryptRemoveReaction(reaction);
String nameCt = chat.encrypt("Group title", rawKey);
String title = chat.decrypt(nameCt, rawKey);
@@ -794,11 +909,11 @@ Base64/hex helpers, MIME sniffing, and image dimensions are available as module-
These conceptual types show up across languages (exact field names differ; JS often uses camelCase event discriminators like `message`):
-- **SendPayload** — return value of `encrypt_message` and related encrypt helpers; map into the Chat API send body.
+- **SendPayload** — return value of `encrypt_message` and the other encrypt helpers: the SDK-generated **`message_id`** (a UUID embedded in the signed event—send it as the message's `message_id` and keep it to dedup), `encrypted_content`, `encoded_event_signature`, signature metadata, `conversation_key_version`, and `should_notify`. Map into the Chat API send body.
- **PublicKeyRegistrationPayload** — output of `generate_keypairs` / public-key getters for the add-public-key API.
-- **SigningKeyEntry** — sender public material passed into decrypt for signature verification.
+- **SigningKeyEntry** — sender public material passed into decrypt for signature verification, or stored via `set_signing_keys`.
- **PreparedConversationChange** — output of the three prepare methods: the derived or passed `conversation_id`, the raw `conversation_key` bytes, `conversation_key_version`, `participant_keys` (`user_id`, `encrypted_key`, `public_key_version`), and `action_signatures` (`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, optional `signature_payload`—omitted on key-change signatures because that payload embeds the plaintext key).
-- **DecryptEventsResult** — messages, optional errors, and extracted `conversation_keys`.
+- **DecryptEventsResult** — messages, optional errors, and extracted `conversation_keys`. Decrypted messages that quote a reply carry `reply_preview_validation` (see [Encrypt and send helpers](#encrypt-and-send-helpers)).
For complete field lists, use language stubs in the [chat-xdk repo](https://github.com/xdevplatform/chat-xdk) (`docs/API.md`, `*.pyi`, `index.d.ts`).