diff --git a/es/xchat/cryptography-primer.mdx b/es/xchat/cryptography-primer.mdx index 33d391594..0c7b3cb71 100644 --- a/es/xchat/cryptography-primer.mdx +++ b/es/xchat/cryptography-primer.mdx @@ -64,7 +64,7 @@ X Chat usa tres tipos de material de claves, cada uno con un propósito específ Cuando alguien te agrega a una conversación, cifra la clave de conversación usando tu clave pública de identidad. Solo tu clave privada de identidad puede descifrarla. -Las mitades públicas se registran y descubren a través de las APIs de **claves públicas** de la plataforma (consulta Encryption keys en la referencia de la API). Las mitades privadas permanecen dentro del Chat XDK (por ejemplo, mediante la [copia de seguridad segura de claves](#copia-de-seguridad-segura-de-claves-almacenamiento-distribuido-de-claves) o un blob de claves cuidadosamente protegido). +Las mitades públicas se registran y descubren a través de las APIs de **clave pública** de la plataforma (consulta Encryption keys en la referencia de la API). Las mitades privadas permanecen dentro del Chat XDK (por ejemplo, mediante la [copia de seguridad segura de claves](#copia-de-seguridad-segura-de-claves-almacenamiento-distribuido-de-claves) o un blob de claves cuidadosamente protegido). ### 2. Par de claves de firma @@ -75,7 +75,7 @@ Las mitades públicas se registran y descubren a través de las APIs de **claves | **Clave pública de firma** | Se comparte con otros; se usa para verificar tus firmas | | **Clave privada de firma** | Se mantiene en secreto; se usa para firmar tus mensajes | -Cuando envías un mensaje, se firma con tu clave privada de firma. Los destinatarios verifican usando tu clave pública de firma (también publicada mediante las APIs de claves públicas). El Chat XDK firma como parte del cifrado de un mensaje y puede verificar al descifrar cuando proporcionas el material de clave pública del remitente. +Cuando envías un mensaje, se firma con tu clave privada de firma. Los destinatarios verifican usando tu clave pública de firma (también publicada mediante las APIs de clave pública). El Chat XDK firma como parte del cifrado de un mensaje y puede verificar al descifrar cuando proporcionas el material de clave pública del remitente. ### 3. Clave de conversación @@ -88,7 +88,7 @@ Cuando envías un mensaje, se firma con tu clave privada de firma. Los destinata | **Compartida entre participantes** | Todos los participantes que deberían leer la conversación tienen una copia | | **Versionada** | Las claves pueden rotarse; las apps deberían llevar registro de las versiones a lo largo del tiempo | -Las claves de conversación se generan cuando se configura una conversación o cuando las claves rotan. Cada participante recibe una **copia cifrada** de la clave, producida con su clave pública de identidad. Después de descifrar tu copia una vez, conservas la clave de conversación **en bruto** y la usas para cifrar mensajes (y [multimedia](/xchat/media)) de forma rápida. La configuración de esas copias para una conversación se hace mediante el Chat XDK junto con los endpoints de **claves** de conversación—se recorre en [Primeros pasos](/xchat/getting-started#4-set-up-conversation-keys). +Las claves de conversación se generan cuando se configura una conversación o cuando las claves rotan. Cada participante recibe una **copia cifrada** de la clave, producida con su clave pública de identidad. Después de descifrar tu copia una vez, conservas la clave de conversación **en bruto** y la usas para cifrar mensajes (y [multimedia](/xchat/media)) de forma rápida. La configuración de esas copias para una conversación se hace mediante el Chat XDK junto con los endpoints de **clave** de conversación—se recorre en [Primeros pasos](/xchat/getting-started#4-set-up-conversation-keys). --- @@ -143,9 +143,9 @@ Un desafío central del cifrado de extremo a extremo es la **distribución de cl Cuando se prepara una conversación para mensajería: -1. Se genera una clave de conversación aleatoria (en el Chat XDK) -2. Para **cada participante**, esa clave se cifra dirigida a su **clave pública de identidad** -3. Esas copias cifradas se almacenan y entregan mediante las APIs de Chat de X +1. El Chat XDK genera una clave de conversación aleatoria +2. El Chat XDK cifra esa clave dirigida a la **clave pública de identidad de cada participante** +3. Tu app publica esas copias cifradas a través de las APIs de Chat de X 4. Cada participante descifra **su** copia con su clave privada de identidad (en el Chat XDK) X solo manipula las copias **envueltas**, nunca la clave de conversación en bruto. @@ -166,7 +166,7 @@ Tu app debería: ## Copia de seguridad segura de claves: almacenamiento distribuido de claves -Tus claves **privadas** de identidad y firma deben almacenarse con cuidado. X Chat incluye un sistema de **copia de seguridad segura de claves** (implementado con Juicebox) para que las claves puedan recuperarse con un código de acceso entre dispositivos sin darle a ningún servidor el secreto completo. +Tus claves **privadas** de identidad y firma deben almacenarse con cuidado. X Chat incluye un sistema de **copia de seguridad segura de claves** para que las claves puedan recuperarse con un código de acceso entre dispositivos sin darle a ningún servidor el secreto completo. ### El problema con el almacenamiento tradicional de claves @@ -201,7 +201,7 @@ flowchart LR Obtienes recuperabilidad (nuevo dispositivo + código de acceso) sin que una sola parte tenga el secreto completo. -No configuras servidores de copia de seguridad de claves a mano para el flujo normal. El Chat XDK incluye el cliente de copia de seguridad; la configuración de realms proviene de la X API como **`juicebox_config`** en tu registro de clave pública (el campo lleva el nombre de Juicebox, la implementación subyacente). El almacenamiento del código de acceso por primera vez y el desbloqueo posterior son llamadas del Chat XDK—consulta [inicializar con claves existentes](/xchat/getting-started#2-initialize-the-chat-xdk-with-existing-keys) y [crear y registrar las claves](/xchat/getting-started#3-create-and-register-keys-first-time-setup) en Primeros pasos. Algunas apps (especialmente servidores y bots) usan un blob de claves exportado en lugar de la copia de seguridad segura de claves; protege ese material como una contraseña. +No configuras servidores de copia de seguridad de claves a mano para el flujo normal. El Chat XDK incluye el cliente de copia de seguridad; la configuración de realms proviene de la X API como el campo **`juicebox_config`** en tu registro de clave pública. El almacenamiento del código de acceso por primera vez y el desbloqueo posterior son llamadas del Chat XDK—consulta [inicializar con claves existentes](/xchat/getting-started#2-initialize-the-chat-xdk-with-existing-keys) y [crear y registrar las claves](/xchat/getting-started#3-create-and-register-keys-first-time-setup) en Primeros pasos. Algunas apps (especialmente servidores y bots) usan un blob de claves exportado en lugar de la copia de seguridad segura de claves; protege ese material como una contraseña. --- @@ -224,7 +224,7 @@ Si algo cambia en el material firmado, la verificación falla. Solo alguien con ### En tu app -El Chat XDK firma cuando cifras mensajes salientes y verifica cuando descifras mensajes entrantes contra el material de clave pública del remitente (obtenido de las APIs de claves públicas). La verificación es **obligatoria por defecto**: el SDK rechaza los eventos firmados no verificados a menos que desactives explícitamente la comprobación (no recomendado). Los detalles están en la referencia del [Chat XDK](/xchat/xchat-xdk). +El Chat XDK firma cuando cifras mensajes salientes y verifica cuando descifras mensajes entrantes contra el material de clave pública del remitente (obtenido de las APIs de clave pública). La verificación es **obligatoria por defecto**: el SDK rechaza los eventos firmados no verificados a menos que desactives explícitamente la comprobación (no recomendado). Los detalles están en la referencia del [Chat XDK](/xchat/xchat-xdk). ### Cambios de estado firmados (firmas de acción) @@ -264,11 +264,11 @@ Las firmas están vinculadas al contenido del evento y son inmutables: un evento | Término | Definición | |:--------|:-----------| | **Cifrado simétrico** | Misma clave cifra y descifra (se usa para mensajes y flujos de multimedia) | -| **Cifrado asimétrico** | Claves distintas para cifrar y descifrar (se usa para envolver claves de conversación) | +| **Cifrado asimétrico** | Claves distintas para cifrar y descifrar (se usa para intercambiar claves de conversación) | | **Clave pública** | Es seguro compartirla; se usa para cifrar *hacia* alguien o verificar sus firmas | | **Clave privada** | Debe permanecer secreta; se usa para descifrar o firmar | | **Par de claves** | Una clave pública y una clave privada vinculadas | -| **ECDH / ECIES** | Algoritmos usados al envolver claves de conversación hacia claves de identidad | +| **ECDH / ECIES** | Algoritmos usados al intercambiar claves de conversación mediante claves de identidad | | **ECDSA** | Algoritmo de firma usado para la autoría de mensajes | | **P-256** | Curva elíptica usada en X Chat (secp256r1) | | **Clave de conversación** | Clave simétrica compartida por los participantes en una conversación (versionada a lo largo del tiempo) | diff --git a/es/xchat/getting-started.mdx b/es/xchat/getting-started.mdx index 5fea0a354..4d15ed22f 100644 --- a/es/xchat/getting-started.mdx +++ b/es/xchat/getting-started.mdx @@ -45,11 +45,10 @@ Las apps de X Chat usan dos piezas juntas: ```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 @@ Las apps de X Chat usan dos piezas juntas: com.x chatxdk - 0.2.1 + 0.4.0 ``` @@ -133,14 +132,14 @@ Crea un cliente de API con tu token de acceso OAuth 2.0 de **usuario**: ## 2. Inicializa el Chat XDK con claves existentes -Este paso **carga claves que ya tienes**—úsalo cuando esta identidad ya completó la configuración inicial: +Este paso **carga claves que ya tienes**—úsalo cuando esta identidad ya completó la configuración inicial antes: - **Copia de seguridad segura de claves:** construye el SDK con el `juicebox_config` de tu registro de clave pública y luego usa `unlock` con tu código de acceso para recuperar las claves privadas (por ejemplo, en un dispositivo nuevo). -- **Blob de claves:** `import_keys` con un blob que exportaste previamente mediante `export_keys`. +- **Blob de claves:** `import_keys` con un blob que exportaste previamente mediante `export_keys`, pasando la versión registrada de la clave junto con él (Rust y Go llaman a esta variante `import_keys_with_version` / `ImportKeysWithVersion`). -Después establece la versión de tu clave pública registrada (`public_key_version` en tu registro). +Después llama a **`set_identity(user_id, signing_key_version)`** una vez, con tu ID de usuario y el `public_key_version` de tu registro. Esto guarda la identidad de sesión: cada llamada posterior a encrypt y prepare firma como esta identidad, así que nunca pasas un ID de remitente ni una versión de clave de firma por llamada. -**¿Configuras por primera vez?** Construye el SDK de la misma forma pero omite `unlock`/`import_keys` y continúa en el [paso 3](#3-crea-y-registra-las-claves-configuracion-inicial) para crear, respaldar y registrar tus claves. +**¿Configuras por primera vez?** Construye el SDK de la misma forma pero omite `unlock`/`import_keys` y continúa en el [paso 3](#3-create-and-register-keys-first-time-setup) para crear, respaldar y registrar tus claves. @@ -160,7 +159,9 @@ Después establece la versión de tu clave pública registrada (`public_key_vers 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 @@ Después establece la versión de tu clave pública registrada (`public_key_vers getAuthToken: async (realmId) => getRealmTokenFromYourBackend(realmId), }); await chat.unlock('YOUR_PASSCODE'); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity('YOUR_USER_ID', signingKeyVersion); ``` @@ -189,11 +190,11 @@ Después establece la versión de tu clave pública registrada (`public_key_vers 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 @@ Después establece la versión de tu clave pública registrada (`public_key_vers 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 @@ Después establece la versión de tu clave pública registrada (`public_key_vers 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 @@ Después establece la versión de tu clave pública registrada (`public_key_vers 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); } ``` @@ -244,18 +247,24 @@ Después establece la versión de tu clave pública registrada (`public_key_vers Los ejemplos para servidor y bot suelen usar un **blob de claves** (`export_keys` / `import_keys`). Las apps cliente suelen usar la **copia de seguridad segura de claves** (`setup` / `unlock` con un código de acceso). Consulta la referencia del [Chat XDK](/xchat/xchat-xdk) para ambos caminos. -**¿Traes tus propias claves?** `import_keys` solo acepta el blob opaco que produce `export_keys` del Chat XDK—es una serialización privada y versionada del estado completo de las claves, no claves P-256 en bruto ni codificadas en PEM. No puedes construir este blob por tu cuenta: genera las claves con `generate_keypairs` ([paso 3](#3-crea-y-registra-las-claves-configuracion-inicial)), exporta el blob una vez y guárdalo codificado en base64. Los blobs hechos a mano o modificados fallan al importarse. +**¿Traes tus propias claves?** `import_keys` solo acepta el blob opaco que produce `export_keys` del Chat XDK—es una serialización privada y versionada del estado completo de las claves, no claves P-256 en bruto ni codificadas en PEM. No puedes construir este blob por tu cuenta: genera las claves con `generate_keypairs` ([paso 3](#3-create-and-register-keys-first-time-setup)), exporta el blob una vez y guárdalo codificado en base64. Los blobs hechos a mano o modificados fallan al importarse. --- ## 3. Crea y registra las claves (configuración inicial) -Omite este paso si cargaste claves existentes en el [paso 2](#2-inicializa-el-chat-xdk-con-claves-existentes). De lo contrario, la configuración inicial de una identidad nueva hace **tres cosas**: +Omite este paso si cargaste claves existentes en el [paso 2](#2-initialize-the-chat-xdk-with-existing-keys). De lo contrario, la configuración inicial de una identidad nueva hace **tres cosas**: 1. **Crear los pares de claves** — `generate_keypairs` produce los pares de claves de identidad y de firma. -2. **Registrar las claves públicas** — envía el payload de registro con POST al endpoint add-public-key para que otros puedan cifrar hacia ti y verificar tus firmas. -3. **Guardar las claves privadas** — `setup` con un código de acceso las escribe en la copia de seguridad segura de claves (clientes), o `export_keys` devuelve un blob de claves para que lo guardes de forma segura (servidores y bots). +2. **Guardar las claves privadas** — `setup` con un código de acceso las escribe en la copia de seguridad segura de claves (clientes), o `export_keys` devuelve un blob de claves para que lo guardes de forma segura (servidores y bots). +3. **Registrar las claves públicas** — envía el payload de registro con POST al endpoint add-public-key para que otros puedan cifrar hacia ti y verificar tus firmas. + +Termina llamando a `set_identity` con la versión de clave del registro, para que esta sesión firme como la nueva identidad. + + +Los scripts listos para ejecutar de registro por única vez para cada binding viven en [`chat-xdk/examples`](https://github.com/xdevplatform/chat-xdk/tree/main/examples) (Python, TypeScript, Go, Rust, C# y Java). Úsalos en vez de armar a mano el flujo de abajo cuando solo necesitas incorporar una nueva identidad. + @@ -280,7 +289,7 @@ Omite este paso si cargaste claves existentes en el [paso 2](#2-inicializa-el-ch ), ) 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")) ``` @@ -300,7 +309,7 @@ Omite este paso si cargaste claves existentes en el [paso 2](#2-inicializa-el-ch generate_version: registration.generateVersion, }); await chat.setup('YOUR_PASSCODE'); - chat.setKeyVersion(String(registration.version ?? signingKeyVersion)); + chat.setIdentity('YOUR_USER_ID', String(registration.version ?? '1')); ``` @@ -316,6 +325,8 @@ Omite este paso si cargaste claves existentes en el [paso 2](#2-inicializa-el-ch 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); ``` @@ -335,9 +346,15 @@ Omite este paso si cargaste claves existentes en el [paso 2](#2-inicializa-el-ch 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) + } ``` @@ -349,7 +366,7 @@ Omite este paso si cargaste claves existentes en el [paso 2](#2-inicializa-el-ch $"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"); ``` @@ -367,7 +384,7 @@ Omite este paso si cargaste claves existentes en el [paso 2](#2-inicializa-el-ch 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"); ``` @@ -380,7 +397,7 @@ Usa un código de acceso fuerte para la copia de seguridad segura de claves. Per ## 4. Configura las claves de conversación -Llama a **`prepare_conversation_key_change`** con tu ID de usuario, tu versión de clave de firma y la clave pública de identidad de cada participante. Una sola llamada genera una nueva clave de conversación, la cifra para cada participante y firma el cambio. Envía el resultado con POST al endpoint **add conversation keys** (`POST /2/chat/conversations/{id}/keys`)—el cuerpo necesita `conversation_key_version`, `conversation_participant_keys` (SDK `encrypted_key` → API `encrypted_conversation_key`) y **`action_signatures`** (obligatorio; la API rechaza la llamada sin ellas). Conserva la clave de conversación **en bruto** para enviar. +Llama a **`prepare_conversation_key_change`** con la clave pública de identidad de cada participante; la identidad del remitente proviene de la sesión que estableciste en el paso 2. Una sola llamada genera una nueva clave de conversación, la cifra para cada participante y firma el cambio. Envía el resultado con POST al endpoint **add conversation keys** (`POST /2/chat/conversations/{id}/keys`)—el cuerpo necesita `conversation_key_version`, `conversation_participant_keys` (SDK `encrypted_key` → API `encrypted_conversation_key`) y **`action_signatures`** (obligatorio; la API rechaza la llamada sin ellas). Conserva la clave de conversación **en bruto** para enviar. La respuesta devuelve el id canónico de la conversación (`data.conversation_id`—el par unido con guión para un 1:1, o el id con prefijo `g` para un grupo) y el `data.sequence_id` del cambio de clave. Usa ese id devuelto para solicitudes posteriores en lugar de reconstruirlo en el cliente. La misma llamada también **rota** las claves más adelante: pasa el id de conversación existente a `prepare_conversation_key_change` y haz POST con la versión de clave más reciente. Rota cuando sospeches que la clave de conversación quedó expuesta—la rotación protege **mensajes futuros** únicamente; los mensajes cifrados bajo versiones anteriores de la clave siguen siendo legibles para cualquiera que tenga esas versiones. @@ -398,8 +415,6 @@ La respuesta devuelve el id canónico de la conversación (`data.conversation_id 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 ) @@ -446,8 +461,6 @@ La respuesta devuelve el id canónico de la conversación (`data.conversation_id // 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'), @@ -480,9 +493,9 @@ La respuesta devuelve el id canónico de la conversación (`data.conversation_id ```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 @@ -532,8 +545,6 @@ La respuesta devuelve el id canónico de la conversación (`data.conversation_id ```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}, @@ -572,21 +583,18 @@ La respuesta devuelve el id canónico de la conversación (`data.conversation_id 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 { @@ -625,12 +633,9 @@ La respuesta devuelve el id canónico de la conversación (`data.conversation_id 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> parts = new ArrayList<>(); for (var pk : prepared.participantKeys) { @@ -673,36 +678,32 @@ La respuesta devuelve el id canónico de la conversación (`data.conversation_id ## 5. Envía un mensaje -Cifra con los bytes **en bruto** de la clave de conversación. En la solicitud de envío, mapea: +Cifra con la clave de conversación **en bruto** del paso 4. El SDK genera el id del mensaje (un UUID), lo incrusta en el evento firmado y lo devuelve en el payload—nunca acuñas uno tú mismo. En la solicitud de envío, mapea: | Campo del Chat XDK | Campo del cuerpo de la solicitud | |:-------------------|:---------------------------------| | `encrypted_content` / `encryptedContent` / `EncryptedContent` | `encoded_message_create_event` | | `encoded_event_signature` / `encodedEventSignature` / `EncodedEventSignature` | `encoded_message_event_signature` | -| Tu id generado | `message_id` | +| Payload `message_id` / `messageId` / `MessageId` | `message_id` | Usa un id de conversación con **guión** en la ruta URL cuando la API lo requiera (`:` → `-`). El propio SDK es flexible: `encrypt_message` y `encrypt_reply` aceptan el id en cualquier forma que tengas—`A:B` de eventos, `A-B` de listados o rutas URL (en cualquier orden), o solo el id de usuario del destinatario—y lo canonicalizan antes de firmar. Los ids de grupo (con prefijo `g`) pasan sin cambios. ```python - import uuid from xdk.chat.models import SendMessageRequest - message_id = str(uuid.uuid4()) + # Sender identity resolves from set_identity (step 2) payload = chat.encrypt_message( - message_id, - "YOUR_USER_ID", "CONVERSATION_ID", - conv_key, "Hello!", - conv_key_version, - signing_key_version, + conversation_key=conv_key, + conversation_key_version=conv_key_version, ) client.chat.send_message( "RECIPIENT_USER_ID", SendMessageRequest( - message_id=message_id, + message_id=payload.message_id, # SDK-generated, embedded in the signed event encoded_message_create_event=payload.encrypted_content, encoded_message_event_signature=payload.encoded_event_signature, ), @@ -711,20 +712,15 @@ Usa un id de conversación con **guión** en la ruta URL cuando la API lo requie ```typescript - import { randomUUID } from 'crypto'; - - const messageId = randomUUID(); + // Sender identity resolves from setIdentity (step 2) const payload = chat.encryptMessage({ - messageId, - senderId: 'YOUR_USER_ID', conversationId: 'CONVERSATION_ID', - conversationKey: convKey, text: 'Hello!', + conversationKey: convKey, conversationKeyVersion: convKeyVersion, - signingKeyVersion, }); await client.chat.sendMessage('RECIPIENT_USER_ID', { - message_id: messageId, + message_id: payload.messageId, // SDK-generated, embedded in the signed event encoded_message_create_event: payload.encryptedContent, encoded_message_event_signature: payload.encodedEventSignature, }); @@ -734,18 +730,14 @@ Usa un id de conversación con **guión** en la ruta URL cuando la API lo requie ```rust use chat_xdk_core::EncryptMessageParams; - let message_id = uuid::Uuid::new_v4().to_string(); - let payload = chat.encrypt_message(EncryptMessageParams::new( - &message_id, - &sender_id, - &conversation_id, - conv_key, - "Hello!", - &conv_key_version, - &signing_key_version, - ))?; + // Sender identity resolves from set_identity (step 2) + let payload = chat.encrypt_message( + EncryptMessageParams::new(&conversation_id, "Hello!") + .with_conversation_key(conv_key, &conv_key_version), + )?; let body = serde_json::json!({ - "message_id": message_id, + // SDK-generated, embedded in the signed event + "message_id": payload.message_id, "encoded_message_create_event": payload.encrypted_content, "encoded_message_event_signature": payload.encoded_event_signature, }); @@ -758,18 +750,19 @@ Usa un id de conversación con **guión** en la ruta URL cuando la API lo requie ```go - messageID := uuid.NewString() + // Sender identity resolves from SetIdentity (step 2) payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{ - MessageID: messageID, - SenderID: senderID, ConversationID: conversationID, - ConversationKey: convKey, Text: "Hello!", + ConversationKey: convKey, ConversationKeyVersion: convKeyVersion, - SigningKeyVersion: signingKeyVersion, }) + if err != nil { + log.Fatal(err) + } body, _ := json.Marshal(map[string]string{ - "message_id": messageID, + // SDK-generated, embedded in the signed event + "message_id": payload.MessageID, "encoded_message_create_event": payload.EncryptedContent, "encoded_message_event_signature": payload.EncodedEventSignature, }) @@ -785,18 +778,14 @@ Usa un id de conversación con **guión** en la ruta URL cuando la API lo requie ```csharp - var messageId = Guid.NewGuid().ToString(); - var payload = chat.EncryptMessage(new EncryptMessageParams { - MessageId = messageId, - SenderId = senderId, - ConversationId = conversationId, + // Sender identity resolves from SetIdentity (step 2) + var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hello!") { ConversationKey = convKey, - Text = "Hello!", ConversationKeyVersion = convKeyVersion, - SigningKeyVersion = signingKeyVersion, }); var sendJson = System.Text.Json.JsonSerializer.Serialize(new Dictionary { - ["message_id"] = messageId, + // SDK-generated, embedded in the signed event + ["message_id"] = payload.MessageId, ["encoded_message_create_event"] = payload.EncryptedContent, ["encoded_message_event_signature"] = payload.EncodedEventSignature, }); @@ -810,19 +799,16 @@ Usa un id de conversación con **guión** en la ruta URL cuando la API lo requie ```java - EncryptMessageParams params = new EncryptMessageParams(); - params.messageId = UUID.randomUUID().toString(); - params.senderId = senderId; - params.conversationId = conversationId; + // Sender identity resolves from setIdentity (step 2) + EncryptMessageParams params = new EncryptMessageParams(conversationId, "Hello!"); params.conversationKey = convKey; - params.text = "Hello!"; params.conversationKeyVersion = convKeyVersion; - params.signingKeyVersion = signingKeyVersion; SendPayload payload = chat.encryptMessage(params); String pathId = conversationId.replace(':', '-'); String sendJson = new ObjectMapper().writeValueAsString(Map.of( - "message_id", params.messageId, + // SDK-generated, embedded in the signed event + "message_id", payload.messageId, "encoded_message_create_event", payload.encryptedContent, "encoded_message_event_signature", payload.encodedEventSignature)); HttpRequest req = HttpRequest.newBuilder() @@ -836,6 +822,10 @@ Usa un id de conversación con **guión** en la ruta URL cuando la API lo requie + +Los snippets pasan la clave de conversación explícitamente porque en este flujo acabas de crearla en el paso 4. Una vez que la caché de claves esté activada y una pasada de `decrypt_events` haya verificado la clave de la conversación ([paso 6](#6-receive-and-decrypt)), basta con `encrypt_message(conversation_id, text)` a solas—el SDK completa con la última clave verificada. Los reintentos deberían reenviar el **mismo** payload cifrado, así que nunca se acuña dos veces un id. + + --- ## 6. Recibe y descifra @@ -844,14 +834,14 @@ Usa [webhooks o el activity stream](/xchat/real-time-events) para el tráfico en - Campos de payload en vivo: `encoded_event`, `conversation_key_change_event` opcional - Historial: `GET /2/chat/conversations/{id}/events` — prefiere **`decrypt_events`** sobre todos los eventos más `meta.conversation_key_events` -- Pasa las claves públicas del remitente al descifrar para la verificación de la firma (mapea los campos de la API a `SigningKeyEntry`; consulta [Chat XDK](/xchat/xchat-xdk)) +- Descifrar necesita las **claves de firma** de los remitentes para que el SDK pueda verificar quién escribió cada mensaje. Estas son las claves *públicas* de los demás participantes — obténlas del mismo endpoint de claves públicas que usaste en el paso 4 y mapea los campos a `SigningKeyEntry` (los snippets de abajo incluyen el mapeo) +- Puedes pasar las claves de firma (y, para `decrypt_event`, las claves de conversación) en cada llamada, **o** establecer dos almacenes de sesión opcionales una vez y usar las formas cortas de llamada. Los snippets siguientes usan los almacenes: `set_signing_keys(entries)` mantiene las claves de los participantes, y `set_cache_keys(true)` (desactivado por defecto) guarda la última clave **verificada por firma** de cada conversación para que las llamadas posteriores puedan omitir los argumentos de clave. Ambos estilos verifican de forma idéntica - JavaScript usa tipos de evento en camelCase (`message`); los demás lenguajes usan `"Message"` y campos snake_case en JSON ```python - conversation_keys = {} # conversation_id -> { version: key_bytes } - + # Once per process: fill the signing-key store and enable the key cache def signing_keys_for(user_id: str) -> list[dict]: resp = client.chat.get_user_public_keys( user_id, @@ -870,25 +860,34 @@ Usa [webhooks o el activity stream](/xchat/real-time-events) para el tráfico en for r in resp.data ] + chat.set_signing_keys( + signing_keys_for("YOUR_USER_ID") + signing_keys_for("RECIPIENT_USER_ID") + ) + chat.set_cache_keys(True) + + # Initial load or pagination: batch decrypt. Conversation keys are + # extracted from the KeyChange events in the batch; per-event failures + # are collected in result["errors"], never raised. + result = chat.decrypt_events(all_events_b64) + for dm in result["messages"]: + event = dm["event"] + if event["type"] == "Message" and event["content"]["content_type"] == "Text": + print(event["sender_id"], event["content"]["text"], event["verified"]) + + # Live traffic: one event at a time def handle_payload(payload: dict): - cid = payload["conversation_id"] if payload.get("conversation_key_change_event"): - conversation_keys[cid] = chat.extract_conversation_keys( - [payload["conversation_key_change_event"]] - )["keys"] - event = chat.decrypt_event( - payload["encoded_event"], - conversation_keys.get(cid, {}), - signing_keys_for(payload["sender_id"]), - ) - if event.get("type") == "Message" and event.get("content", {}).get("content_type") == "Text": - print(event["sender_id"], event["content"]["text"], event.get("verified")) + # A rotation enters the key cache only after its signature + # verifies, which is what decrypt_events does + chat.decrypt_events([payload["conversation_key_change_event"]]) + event = chat.decrypt_event(payload["encoded_event"]) # raises on failure + if event["type"] == "Message" and event["content"]["content_type"] == "Text": + print(event["sender_id"], event["content"]["text"], event["verified"]) ``` ```typescript - const conversationKeys = new Map>(); - + // Once per process: fill the signing-key store and enable the key cache async function signingKeysFor(userId: string) { const resp = await client.chat.getUserPublicKeys(userId, { publicKeyFields: [ @@ -909,24 +908,33 @@ Usa [webhooks o el activity stream](/xchat/real-time-events) para el tráfico en })); } - async function handlePayload(payload: { - conversation_id: string; + chat.setSigningKeys([ + ...(await signingKeysFor('YOUR_USER_ID')), + ...(await signingKeysFor('RECIPIENT_USER_ID')), + ]); + chat.setCacheKeys(true); + + // Initial load or pagination: batch decrypt. Conversation keys are + // extracted from the KeyChange events in the batch; per-event failures + // are collected in result.errors, never thrown. + const result = chat.decryptEvents(allEventsB64); + for (const dm of result.messages) { + if (dm.event.type === 'message' && dm.event.content?.contentType === 'text') { + console.log(dm.event.senderId, dm.event.content.text, dm.event.verified); + } + } + + // Live traffic: one event at a time + function handlePayload(payload: { encoded_event: string; - sender_id: string; conversation_key_change_event?: string; }) { - const cid = payload.conversation_id; if (payload.conversation_key_change_event) { - conversationKeys.set( - cid, - chat.extractConversationKeys([payload.conversation_key_change_event]).keys, - ); + // A rotation enters the key cache only after its signature + // verifies, which is what decryptEvents does + chat.decryptEvents([payload.conversation_key_change_event]); } - const event = chat.decryptEvent( - payload.encoded_event, - conversationKeys.get(cid) ?? {}, - await signingKeysFor(payload.sender_id), - ); + const event = chat.decryptEvent(payload.encoded_event); // throws on failure if (event.type === 'message' && event.content?.contentType === 'text') { console.log(event.senderId, event.content.text, event.verified); } @@ -935,25 +943,48 @@ Usa [webhooks o el activity stream](/xchat/real-time-events) para el tráfico en ```rust - // Build Vec from GET /2/users/{id}/public_keys - // (public_key_version, public_key, signing_public_key, identity_public_key_signature) + // Once per instance: fill the signing-key store (Vec + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.set_signing_keys(participant_signing_keys); + chat.set_cache_keys(true); + + // Initial load: batch decrypt — per-event failures land in result.errors + let result = chat.decrypt_events(&all_events_b64, &[]); + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what decrypt_events does if let Some(kc) = key_change_b64.as_deref() { - let extracted = chat.extract_conversation_keys(&[kc]); - conv_keys.extend(extracted.keys); + chat.decrypt_events(&[kc], &[]); } - let event = chat.decrypt_event(&encoded_event, &conv_keys, &sender_signing_keys)?; + let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?; ``` ```go - if keyChange != "" { - extracted, _ := chat.ExtractConversationKeys([]string{keyChange}) - for v, k := range extracted.Keys { - convKeys[v] = k + // Once per instance: fill the signing-key store ([]SigningKeyEntry + // from GET /2/users/{id}/public_keys) and enable the key cache + if err := chat.SetSigningKeys(participantSigningKeys); err != nil { + log.Fatal(err) + } + chat.SetCacheKeys(true) + + // Initial load: batch decrypt — per-event failures land in result.Errors + result, err := chat.DecryptEvents(allEventsB64, nil) + if err != nil { + log.Fatal(err) + } + for _, dm := range result.Messages { + if dm.Event.Type == "Message" { + fmt.Println(dm.Event.AsMessage().Text()) } } - event, err := chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys) + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what DecryptEvents does + if keyChange != "" { + chat.DecryptEvents([]string{keyChange}, nil) + } + event, err := chat.DecryptEvent(encodedEvent, nil, nil) if err == nil && event.Type == "Message" { fmt.Println(event.AsMessage().Text()) } @@ -961,24 +992,49 @@ Usa [webhooks o el activity stream](/xchat/real-time-events) para el tráfico en ```csharp - if (!string.IsNullOrEmpty(keyChangeB64)) + // Once per instance: fill the signing-key store (SigningKeyEntry list + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.SetSigningKeys(participantSigningKeys); + chat.SetCacheKeys(true); + + // Initial load: batch decrypt — per-event failures land in result.Errors + var result = chat.DecryptEvents(allEventsB64); + foreach (var dm in result.Messages) { - var extracted = chat.ExtractConversationKeys(new[] { keyChangeB64 }); - foreach (var kv in extracted.Keys) - convKeys[kv.Key] = kv.Value; + if (dm.Event.GetProperty("type").GetString() == "Message") + Console.WriteLine(dm.Event.GetProperty("content").GetProperty("text").GetString()); } - var evt = chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys); + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what DecryptEvents does + if (!string.IsNullOrEmpty(keyChangeB64)) + chat.DecryptEvents(new[] { keyChangeB64 }); + var evt = chat.DecryptEvent(encodedEvent); // throws on failure if (evt.GetProperty("type").GetString() == "Message") Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString()); ``` ```java + // Once per instance: fill the signing-key store (SigningKeyEntry list + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.setSigningKeys(participantSigningKeys); + chat.setCacheKeys(true); + + // Initial load: batch decrypt — per-event failures land in result.errors + DecryptEventsResult result = chat.decryptEvents(allEventsB64, null); + for (DecryptedMessage dm : result.messages) { + if ("Message".equals(dm.event.path("type").asText())) { + System.out.println(dm.event.path("content").path("text").asText()); + } + } + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what decryptEvents does if (keyChangeB64 != null && !keyChangeB64.isEmpty()) { - var extracted = chat.extractConversationKeys(List.of(keyChangeB64)); - convKeys.putAll(extracted.keys); + chat.decryptEvents(List.of(keyChangeB64), null); } - JsonNode evt = chat.decryptEvent(encodedEvent, convKeys, senderSigningKeys); + JsonNode evt = chat.decryptEvent(encodedEvent, (Map) null, null); if ("Message".equals(evt.path("type").asText())) { System.out.println(evt.path("content").path("text").asText()); } @@ -986,33 +1042,15 @@ Usa [webhooks o el activity stream](/xchat/real-time-events) para el tráfico en -Bots completos de poll-and-reply para todos los lenguajes: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). + +**¿Serverless o multi-instancia?** El almacén de claves de firma y la caché de claves viven en la memoria de la instancia del SDK. Cuando eso no encaja—una invocación descifra, otra envía—pasa las claves explícitamente en su lugar: `decrypt_events(events, signing_keys)`, `decrypt_event(event_b64, conversation_keys, signing_keys)` y las sobrecargas `conversation_key`/`conversation_key_version` en los métodos de cifrado. Persiste tú mismo las `conversation_keys` que devuelve `decrypt_events` y pásalas de nuevo. + + +Bots completos de sondeo-y-respuesta para cada lenguaje: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). --- ## Buenas prácticas -- Cachea las claves de conversación en bruto y las claves públicas de los remitentes; refréscalas ante fallos de verificación de firma +- Mantén fresco el almacén de claves de firma: vuelve a llamar a `set_signing_keys` con el conjunto completo de participantes cuando un remitente registre una nueva versión de clave, y actualízalo ante fallos de verificación de firma - Deduplica las entregas en vivo con `event_uuid` -- Pagina el historial de eventos hasta completar la paginación para no perder metadatos de cambio de clave -- No registres códigos de acceso, claves privadas ni el texto plano de mensajes en producción -- En apps web, mantén los tokens de OAuth (y la emisión de tokens de realm de la copia de seguridad de claves) en un servidor; procura mantener las claves privadas solo en el Chat XDK del cliente - ---- - -## Próximos pasos - - - - Métodos y tipos para todos los bindings de lenguaje - - - Imágenes y archivos adjuntos cifrados - - - Conversaciones y metadatos con múltiples participantes - - - Webhooks y entrega de actividad - - diff --git a/es/xchat/groups.mdx b/es/xchat/groups.mdx index 326d17506..a53a2ac92 100644 --- a/es/xchat/groups.mdx +++ b/es/xchat/groups.mdx @@ -1,7 +1,7 @@ --- title: Conversaciones de grupo sidebarTitle: Grupos -description: Crea conversaciones de grupo de X Chat con varios participantes, claves de conversación compartidas, títulos cifrados y mensajes firmados. +description: Crea conversaciones de grupo en X Chat con claves de conversación compartidas, títulos cifrados, gestión de miembros y mensajes firmados. keywords: ["X Chat groups", "group DM", "conversation keys", "group name encryption"] --- @@ -29,17 +29,18 @@ La criptografía sigue siendo: **Chat XDK** para claves y cargas útiles; **X AP 1. Genera el id de grupo con `POST /2/chat/conversations/group/initialize` — el `data.conversation_id` de la respuesta es el id con prefijo `g` que usas en todo lo que sigue. 2. Carga la clave pública de identidad y el `public_key_version` de cada miembro (rutas `GET` de claves públicas bajo **Encryption keys**; [`GET /2/users/public_keys`](/x-api/users/get-public-keys-for-multiple-users) obtiene varios usuarios en una sola solicitud). Verifica cada registro con `verify_key_binding` antes de usarlo (consulta la advertencia en [Primeros pasos](/xchat/getting-started#4-set-up-conversation-keys)). -3. Ejecuta **`prepare_group_create`** una vez, con **todos** los miembros (incluyéndote a ti mismo), el id con prefijo `g` y las listas de ids de miembros/administradores. Una sola llamada genera la clave de conversación, la envuelve para cada miembro y firma la creación — devuelve **dos** firmas de acción (el cambio de clave de conversación y la creación del grupo). +3. Ejecuta **`prepare_group_create`** una vez, con **todos** los miembros (incluyéndote a ti mismo), el id con prefijo `g` y las listas de ids de miembros/administradores. Una sola llamada genera la clave de conversación, la envuelve para cada miembro y firma la creación con la identidad de sesión de `set_identity` — devuelve **dos** firmas de acción (el cambio de clave de conversación y la creación del grupo). 4. `POST /2/chat/conversations/group` con los miembros/administradores del grupo, `conversation_key_version`, `conversation_participant_keys` (SDK **`encrypted_key`** → API **`encrypted_conversation_key`**) y **ambas** `action_signatures`. Los fallos de validación regresan como mensajes estables y legibles, por ejemplo `"Too many members: adding these members would exceed the allowed group size."` o `"Cannot add all members: one or more of the requested members cannot be added to this conversation."`. 5. Conserva la clave de conversación **en bruto** y la **versión** para cifrar/descifrar. -El `title` y el `avatar_url` que pasas a `prepare_group_create` se firman y se incrustan textualmente en el evento de creación del grupo, y el servidor los compara con la solicitud — así que los valores `group_name` / `group_avatar_url` en el cuerpo del POST deben ser **byte a byte idénticos** a lo que pasaste al SDK, o la llamada falla la validación de firma. +`prepare_group_create` firma el `title` y el `avatar_url` que le pasas y los incrusta textualmente en el evento de creación del grupo. El servidor los compara con tu solicitud, así que los valores `group_name` / `group_avatar_url` en el cuerpo del POST deben ser **byte a byte idénticos** a lo que pasaste al SDK — de lo contrario, la llamada falla la validación de firma. ```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", ) @@ -50,8 +51,9 @@ El `title` y el `avatar_url` que pasas a `prepare_group_create` se firman y se i ```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', }); @@ -60,9 +62,9 @@ El `title` y el `avatar_url` que pasas a `prepare_group_create` se firman y se i ```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)?; @@ -71,8 +73,8 @@ El `title` y el `avatar_url` que pasas a `prepare_group_create` se firman y se i ```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", }) @@ -83,23 +85,20 @@ El `title` y el `avatar_url` que pasas a `prepare_group_create` se firman y se i ```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 ``` ```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 @@ -180,13 +179,24 @@ Enviar y recibir en un grupo es lo mismo que en un 1:1 una vez que tienes la cla Siempre cifra con la versión **más reciente** de la clave después de una rotación impulsada por cambios de membresía. +### Cambios de clave de miembros que se fueron + +Los eventos de cambio de clave de un grupo están firmados por quien los realizó, a menudo el creador o un administrador. Si ese miembro más tarde **abandona el grupo** (o desactiva su cuenta), los endpoints de claves públicas dejan de devolver sus claves, por lo que la ruta de descifrado verificado (`decrypt_events` con claves de firma) falla en esos eventos de cambio de clave con `signature missing or no matching signing key`. Los eventos no están corruptos; simplemente ya no se sirve el material de verificación. + +Los grupos de larga duración deben anticipar esto y recurrir a **`extract_conversation_keys`** para los eventos de cambio de clave que no puedan verificarse. Esta ruta omite la verificación de firma y recupera la clave de conversación descifrándola con tu clave de identidad. El modelo de seguridad se mantiene porque: + +- Solo el material de clave que fue **cifrado hacia tu clave de identidad** puede recuperarse en absoluto — un tercero no puede inyectar una clave que puedas leer +- Cada **mensaje** sigue verificándose por firma contra su propio remitente, por lo que la autoría de mensajes no se ve afectada + +Mantén la ruta verificada primero: usa `decrypt_events` (que también alimenta la caché de claves del SDK cuando `set_cache_keys(true)` está habilitado) y recurre a `extract_conversation_keys` solo para los eventos de cambio de clave que rechaza. + --- ## Lista de verificación -1. Genera el id con prefijo `g` con `POST /2/chat/conversations/group/initialize` -2. `prepare_group_create` con **cada** miembro; envía con POST los envoltorios de clave de participantes y **ambas** firmas de acción a `POST /2/chat/conversations/group` -3. Cachea la clave en bruto + versión; actualiza en eventos de cambio de clave -4. En cambios de membresía, `prepare_group_members_change` (dos firmas) → `POST /2/chat/conversations/{id}/members` -5. Descifra los metadatos del grupo con `decrypt` cuando los campos sean texto cifrado -6. Envía/recibe con los mismos patrones que en 1:1 +1. Genera el id con prefijo `g` con `POST /2/chat/conversations/group/initialize` +2. `prepare_group_create` con **cada** miembro; envía con POST los envoltorios de clave de participantes y **ambas** firmas de acción a `POST /2/chat/conversations/group` +3. Cachea la clave en bruto + versión; actualiza en eventos de cambio de clave +4. En cambios de membresía, `prepare_group_members_change` (dos firmas) → `POST /2/chat/conversations/{id}/members` +5. Descifra los metadatos del grupo con `decrypt` cuando los campos sean texto cifrado +6. Envía/recibe con los mismos patrones que en 1:1 diff --git a/es/xchat/media.mdx b/es/xchat/media.mdx index 409dbaa74..dc1a0c236 100644 --- a/es/xchat/media.mdx +++ b/es/xchat/media.mdx @@ -122,23 +122,19 @@ Usa los cuerpos de solicitud en las páginas de OpenAPI bajo **API reference → ## Enviar con un adjunto -Cifra con un adjunto de multimedia y luego haz POST del cuerpo send-message (mismo mapeo de campos que en [Primeros pasos](/xchat/getting-started#5-send-a-message)). +Cifra con un adjunto de multimedia y luego haz POST del cuerpo send-message (mismo mapeo de campos que en [Primeros pasos](/xchat/getting-started#5-send-a-message)). El SDK genera el `message_id` y lo devuelve en el payload—envía ese valor y reutiliza el mismo payload en los reintentos, así nunca se acuña un id dos veces. ```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, @@ -151,7 +147,7 @@ Cifra con un adjunto de multimedia y luego haz POST del cuerpo send-message (mis 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, ), @@ -160,26 +156,23 @@ Cifra con un adjunto de multimedia y luego haz POST del cuerpo send-message (mis ```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, }); @@ -187,10 +180,23 @@ Cifra con un adjunto de multimedia y luego haz POST del cuerpo send-message (mis ```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, }); @@ -203,10 +209,12 @@ Cifra con un adjunto de multimedia y luego haz POST del cuerpo send-message (mis ```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, @@ -216,49 +224,51 @@ Cifra con un adjunto de multimedia y luego haz POST del cuerpo send-message (mis 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 ``` ```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 ``` ```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 ``` +El par de clave de conversación puede omitirse por completo: con `set_cache_keys(true)` habilitado, `encrypt_message` resuelve la clave y la versión a partir del cambio de clave verificado más reciente de la conversación (consulta [Primeros pasos](/xchat/getting-started)). + --- ## Descargar y descifrar -Ruta: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/download-chat-media). El cuerpo de la respuesta es texto cifrado. En los mensajes entrantes, lee `media_hash_key` desde los adjuntos descifrados / `media_hashes`. +Ruta: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/download-chat-media). El cuerpo de la respuesta es texto cifrado. En mensajes entrantes, lee `media_hash_key` desde los adjuntos descifrados / `media_hashes`. -**Elige la clave por la versión de clave del evento.** Cada evento de mensaje descifrado lleva la `keyVersion` (JS; `key_version` en los demás bindings) con la que se cifró su contenido. Descifra un adjunto con la clave de conversación de **esa** versión—`conversationKeys.keys[event.keyVersion]`—no la más reciente. Después de una rotación de clave (por ejemplo al agregar un miembro), la clave más reciente no puede descifrar multimedia adjunta a mensajes anteriores. +**Elige la clave según la versión de clave del evento.** Cada evento de mensaje descifrado lleva el `keyVersion` (JS; `key_version` en los demás bindings) bajo el cual se cifró su contenido. Descifra un adjunto con la clave de conversación de **esa** versión—`conversationKeys.keys[event.keyVersion]`—no la más reciente. Tras una rotación de clave (por ejemplo, al agregar un miembro), la clave más reciente no puede descifrar los medios adjuntos a mensajes anteriores. @@ -358,9 +368,9 @@ Ruta: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/downl ## Consejos -- Usa la misma **versión de clave de conversación** que cuando se cifró la multimedia -- No registres multimedia en texto plano ni claves en bruto +- Usa la misma **versión de la clave de conversación** con la que se cifró el multimedia +- No registres en logs multimedia en texto plano ni claves en bruto - Detecta el MIME **después** de descifrar -- Clientes web: cifra/descifra en el cliente cuando sea posible; mantén los tokens de OAuth en tu servidor +- Clientes web: cifra/descifra en el cliente cuando sea posible; conserva los tokens OAuth en tu servidor -Los esquemas completos de solicitud y respuesta para cada ruta de multimedia están bajo **API reference → Media** en la barra lateral (inicializar subida, añadir chunk, finalizar subida y descargar multimedia). +Los esquemas completos de solicitud y respuesta para cada ruta de multimedia están bajo **API reference → Media** en la barra lateral (initialize upload, append chunk, finalize upload y download media). diff --git a/es/xchat/real-time-events.mdx b/es/xchat/real-time-events.mdx index 42d05c89b..95a9a35a5 100644 --- a/es/xchat/real-time-events.mdx +++ b/es/xchat/real-time-events.mdx @@ -10,7 +10,7 @@ X entrega **`chat.received`**, **`chat.sent`** y actividad relacionada de X Chat |:-----|:--------| | **X Activity API** | `GET /2/activity/stream`; `POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions` (consulta la seguridad de OpenAPI por operación) | | **Webhooks** | Rutas opcionales `POST` / `GET` `/2/webhooks` y `PUT` / `DELETE` `/2/webhooks/{webhook_id}` si terminas en tu propia URL HTTPS | -| **Chat XDK** | `extract_conversation_keys`, `decrypt_event` / `decrypt_events` | +| **Chat XDK** | `decrypt_event` / `decrypt_events`, con los almacenes de sesión `set_signing_keys` / `set_cache_keys` | Los tipos de evento privados de X Chat requieren autorización del usuario que monitoreas. Los adjuntos de archivos cifrados de X Chat usan **`media_hash_key`** y la descarga de multimedia de X Chat—no `expansions=attachments.media_keys` / `media.fields=variants` de la Post API. @@ -32,10 +32,10 @@ Los tipos de evento privados de X Chat requieren autorización del usuario que m **Suscripciones de Activity:** gestiona suscripciones duraderas con: -- `POST /2/activity/subscriptions` — crear -- `GET /2/activity/subscriptions` — listar (paginado) -- `PUT /2/activity/subscriptions/{subscription_id}` — actualizar -- `DELETE /2/activity/subscriptions/{subscription_id}` o `DELETE /2/activity/subscriptions?ids=` — eliminar +- `POST /2/activity/subscriptions` — crear +- `GET /2/activity/subscriptions` — listar (paginado) +- `PUT /2/activity/subscriptions/{subscription_id}` — actualizar +- `DELETE /2/activity/subscriptions/{subscription_id}` o `DELETE /2/activity/subscriptions?ids=` — eliminar Los cuerpos de solicitud y los scopes requeridos se definen en la operación OpenAPI de cada ruta. Crear una suscripción de la X Activity API (XAA) requiere **autorización de contexto de usuario** (OAuth 2.0 de contexto de usuario con los scopes correspondientes, como `dm.read` para eventos de chat) para el usuario cuya actividad monitoreas. @@ -80,124 +80,82 @@ Suscríbete también a `chat.sent` si necesitas copias salientes. Otros lenguaje ## 2. CRC (solo webhooks) -Si usas webhooks, responde a los Challenge-Response Checks (GET `crc_token`) con HMAC-SHA256 del token usando tu consumer secret, en la forma JSON que espera tu producto de webhooks (normalmente `sha256=`). +Si usas webhooks, responde a los Challenge-Response Checks (GET `crc_token`) con HMAC-SHA256 del token usando el consumer secret, en la forma JSON que tu producto de webhook espera (típicamente `sha256=`). --- ## 3. Descifra con el Chat XDK -Campos en vivo: **`payload.encoded_event`**, opcional **`payload.conversation_key_change_event`**. Deduplica por **`event_uuid`**. +Campos en vivo: **`payload.encoded_event`**, opcional **`payload.conversation_key_change_event`**. Deduplica las entregas por **`event_uuid`**; deduplica los mensajes por el **`message_id`** que lleva el evento descifrado—forma parte del contenido firmado, mientras que los sequence ids son metadatos sin firmar asignados por el backend. -JavaScript usa tipos de evento en camelCase (`message`); los demás bindings usan `"Message"` y campos en snake_case. +Los snippets de abajo usan los dos almacenes de sesión **opcionales** para el handler más corto: `set_signing_keys` mantiene las claves públicas de los participantes (obtenidas una vez del [endpoint de claves públicas](/x-api/chat/get-user-public-keys)), y `set_cache_keys(true)` conserva la clave verificada de cada conversación, así `decrypt_event` solo necesita el evento. Cuando un payload lleva `conversation_key_change_event`, pásalo antes por `decrypt_events`: eso verifica el cambio de clave y, con la caché activa, retiene su clave para la llamada a `decrypt_event`. ¿Prefieres no tener estado en la instancia? Pasa las claves por llamada en su lugar—consulta la nota al final de esta sección. + +JavaScript usa tipos de evento en camelCase (`message`); los demás bindings usan `"Message"` y campos snake_case. ```python - from chat_xdk import Chat - - chat = Chat(JUICEBOX_CONFIG_JSON) - chat.unlock("YOUR_PASSCODE") - chat.set_key_version(SIGNING_KEY_VERSION) - conversation_keys = {} - - def signing_keys(user_id: str): - resp = api_client.chat.get_user_public_keys( - user_id, - public_key_fields=[ - "public_key_version", "public_key", "signing_public_key", "identity_public_key_signature", - ], - ) - return [ - { - "user_id": user_id, - "public_key_version": r["public_key_version"], - "public_key": r["signing_public_key"], - "identity_public_key": r["public_key"], - "identity_public_key_signature": r["identity_public_key_signature"], - } - for r in resp.data - ] + # chat has keys loaded and set_identity called (see Getting Started) + chat.set_cache_keys(True) + chat.set_signing_keys(participant_signing_keys) # all participants, from the public-key routes data = body.get("data") or {} if data.get("event_type") in ("chat.received", "chat.sent"): p = data.get("payload") or {} - cid = p.get("conversation_id") if p.get("conversation_key_change_event"): - conversation_keys[cid] = chat.extract_conversation_keys( - [p["conversation_key_change_event"]] - )["keys"] - ev = chat.decrypt_event( - p["encoded_event"], - conversation_keys.get(cid, {}), - signing_keys(p["sender_id"]), - ) + # Verify the key change and retain its key in the cache + chat.decrypt_events([p["conversation_key_change_event"]]) + ev = chat.decrypt_event(p["encoded_event"]) + if ev["type"] == "Message": + print(ev["sender_id"], ev["content"]["text"]) ``` ```typescript - import { createChat } from '@xdevplatform/chat-xdk'; - - const chat = await createChat({ - juiceboxConfig: JUICEBOX_CONFIG_JSON, - getAuthToken: async (realmId) => getRealmToken(realmId), - }); - await chat.unlock('YOUR_PASSCODE'); - chat.setKeyVersion(SIGNING_KEY_VERSION); - const conversationKeys = new Map>(); - - async function signingKeys(userId: string) { - const resp = await apiClient.chat.getUserPublicKeys(userId, { - publicKeyFields: [ - 'public_key_version', 'public_key', 'signing_public_key', 'identity_public_key_signature', - ], - }); - return resp.data.map((r: any) => ({ - userId, - publicKeyVersion: r.public_key_version, - publicKey: r.signing_public_key, - identityPublicKey: r.public_key, - identityPublicKeySignature: r.identity_public_key_signature, - })); - } + // chat has keys loaded and setIdentity called (see Getting Started) + chat.setCacheKeys(true); + chat.setSigningKeys(participantSigningKeys); // all participants, from the public-key routes const data = body?.data ?? {}; if (data.event_type === 'chat.received' || data.event_type === 'chat.sent') { const p = data.payload ?? {}; - const cid = p.conversation_id as string; if (p.conversation_key_change_event) { - conversationKeys.set( - cid, - chat.extractConversationKeys([p.conversation_key_change_event]).keys, - ); + // Verify the key change and retain its key in the cache + chat.decryptEvents([p.conversation_key_change_event]); + } + const ev = chat.decryptEvent(p.encoded_event); + if (ev.type === 'message') { + console.log(ev.senderId, ev.content.text); } - const ev = chat.decryptEvent( - p.encoded_event, - conversationKeys.get(cid) ?? {}, - await signingKeys(p.sender_id), - ); } ``` ```rust - // chat: ChatCore or Chat, already unlocked / keys imported + // chat has keys loaded and set_identity called (see Getting Started) + chat.set_cache_keys(true); + chat.set_signing_keys(participant_signing_keys); // all participants + if let Some(kc) = key_change.as_deref() { - let extracted = chat.extract_conversation_keys(&[kc]); - conv_keys.extend(extracted.keys); + // Verify the key change and retain its key in the cache + let _ = chat.decrypt_events(&[kc], &[]); } - // sender_signing_keys from GET /2/users/{sender_id}/public_keys - let event = chat.decrypt_event(&encoded_event, &conv_keys, &sender_signing_keys)?; + // Decrypt with the cached conversation key; verify against the stored signing keys + let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?; ``` ```go + // chat has keys loaded and SetIdentity called (see Getting Started) + chat.SetCacheKeys(true) + _ = chat.SetSigningKeys(participantSigningKeys) // all participants + if keyChange != "" { - extracted, _ := chat.ExtractConversationKeys([]string{keyChange}) - for v, k := range extracted.Keys { - convKeys[v] = k - } + // Verify the key change and retain its key in the cache + _, _ = chat.DecryptEvents([]string{keyChange}, nil) } - event, err := chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys) + // Decrypt with the cached conversation key; verify against the stored signing keys + event, err := chat.DecryptEvent(encodedEvent, nil, nil) if err == nil && event.Type == "Message" { fmt.Println(event.AsMessage().Text()) } @@ -205,24 +163,33 @@ JavaScript usa tipos de evento en camelCase (`message`); los demás bindings usa ```csharp + // chat has keys loaded and SetIdentity called (see Getting Started) + chat.SetCacheKeys(true); + chat.SetSigningKeys(participantSigningKeys); // all participants + if (!string.IsNullOrEmpty(keyChangeB64)) { - var extracted = chat.ExtractConversationKeys(new[] { keyChangeB64 }); - foreach (var kv in extracted.Keys) - convKeys[kv.Key] = kv.Value; + // Verify the key change and retain its key in the cache + chat.DecryptEvents(new[] { keyChangeB64 }); } - var evt = chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys); + // Decrypt with the cached conversation key; verify against the stored signing keys + var evt = chat.DecryptEvent(encodedEvent); if (evt.GetProperty("type").GetString() == "Message") Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString()); ``` ```java + // chat has keys loaded and setIdentity called (see Getting Started) + chat.setCacheKeys(true); + chat.setSigningKeys(participantSigningKeys); // all participants + if (keyChangeB64 != null && !keyChangeB64.isEmpty()) { - var extracted = chat.extractConversationKeys(List.of(keyChangeB64)); - convKeys.putAll(extracted.keys); + // Verify the key change and retain its key in the cache + chat.decryptEvents(List.of(keyChangeB64), null); } - JsonNode evt = chat.decryptEvent(encodedEvent, convKeys, senderSigningKeys); + // Decrypt with the cached conversation key; verify against the stored signing keys + JsonNode evt = chat.decryptEvent(encodedEvent, (Map) null, null); if ("Message".equals(evt.path("type").asText())) { System.out.println(evt.path("content").path("text").asText()); } @@ -230,6 +197,8 @@ JavaScript usa tipos de evento en camelCase (`message`); los demás bindings usa +Para mantener los mapas de claves en tus propias manos en su lugar, `extract_conversation_keys` descifra las claves de `conversation_key_change_event` y `decrypt_event` las acepta (junto con las claves de firma del remitente) como argumentos explícitos—un argumento explícito no vacío siempre gana sobre los almacenes. + Historial: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — consulta [Primeros pasos](/xchat/getting-started#6-receive-and-decrypt). --- @@ -257,6 +226,6 @@ Historial: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conver ## Prácticas - Verifica las firmas del webhook según los requisitos de la plataforma -- Cachea las claves de conversación y las claves públicas de los remitentes -- Aplica los blobs de cambio de clave antes de descifrar mensajes dependientes -- Deduplica por `event_uuid` +- Configura los almacenes de sesión una vez: `set_signing_keys` para todos los participantes, `set_cache_keys(true)` para las claves de conversación +- Aplica los blobs de cambio de clave (mediante `decrypt_events`) antes de descifrar los mensajes dependientes +- Deduplica las entregas por `event_uuid` y los mensajes por el `message_id` firmado diff --git a/es/xchat/troubleshooting.mdx b/es/xchat/troubleshooting.mdx index 48dd5408b..4b3ac4d7d 100644 --- a/es/xchat/troubleshooting.mdx +++ b/es/xchat/troubleshooting.mdx @@ -1,7 +1,7 @@ --- title: Solución de problemas sidebarTitle: Solución de problemas -description: Diagnostica problemas comunes de cifrado en X Chat, como errores del Chat XDK, recuperación de copia segura de claves y fallos de descifrado. +description: "Diagnostica problemas de cifrado en X Chat: errores del Chat XDK, recuperación de copia de claves, fallos de descifrado y payloads firmados." keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encryption"] --- @@ -62,55 +62,122 @@ Para webhooks, OAuth, códigos de estado HTTP y límites de tasa, usa la documen -### El cifrado o descifrado falla porque las claves no están cargadas +### El cifrado o descifrado falla porque las claves o la identidad no están configuradas -Carga primero las claves privadas y después establece la **versión** de clave pública desde tu registro en X. +Carga primero las claves privadas y luego establece la **identidad de sesión**—tu ID de usuario más el `public_key_version` de tu registro en X. Los métodos `encrypt_*` y `prepare_*` firman con ella; llamarlos sin identidad de sesión (y sin una sobrecarga explícita por llamada) es un error. ```python chat.unlock(passcode) # or: chat.import_keys(blob) - chat.set_key_version(signing_key_version) + chat.set_identity(my_user_id, signing_key_version) ``` ```typescript await chat.unlock(passcode); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity(myUserId, signingKeyVersion); ``` ```rust chat.import_keys(&blob)?; - chat.set_key_version(&signing_key_version); + chat.set_identity(&my_user_id, &signing_key_version); ``` ```go blob, _ := chatxdk.Base64ToBytes(privateKeysB64) _ = chat.ImportKeys(blob) - chat.SetKeyVersion(signingKeyVersion) + _ = chat.SetIdentity(myUserID, signingKeyVersion) ``` ```csharp chat.ImportKeys(blobBytes); - chat.SetKeyVersion(signingKeyVersion); + chat.SetIdentity(myUserId, signingKeyVersion); ``` ```java chat.importKeys(blobBytes); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity(myUserId, signingKeyVersion); ``` +### Tu clave pública local nunca coincide con las claves registradas de la cuenta + +A menudo los clientes necesitan responder *"¿la clave en este dispositivo es una de las claves registradas para esta cuenta?"* — tras una restauración o importación, para adoptar el `public_key_version` correcto, o para decidir si el onboarding ya ocurrió. Comparar la salida de `get_public_keys` del Chat XDK contra el campo `public_key` de la API **como cadenas siempre falla**, incluso para la misma clave, porque las dos usan codificaciones distintas: + +- La **API** almacena y devuelve la clave exactamente como la subió el registro: la codificación DER (SPKI) — la clave en bruto detrás de un prefijo fijo de identificador de algoritmo +- La `get_public_keys` del **Chat XDK** devuelve la clave en bruto sola, sin ese prefijo + +La misma clave, dos escrituras. Para compararlas, decodifica ambas desde base64 y comprueba que los bytes de la API **terminan con** los bytes del SDK (bytes idénticos también coinciden, por si ambas partes alguna vez tienen la misma codificación): + + + + ```python + import base64 + + def same_key(local_b64: str, server_b64: str) -> bool: + local = base64.b64decode(local_b64) # chat.get_public_keys()["identity"] + server = base64.b64decode(server_b64) # API row's "public_key" + return local == server or (len(server) > len(local) and server.endswith(local)) + ``` + + + ```typescript + const sameKey = (localB64: string, serverB64: string): boolean => { + const local = Buffer.from(localB64, 'base64'); // chat.getPublicKeys().identity + const server = Buffer.from(serverB64, 'base64'); // API row's public_key + return local.equals(server) || + (server.length > local.length && server.subarray(server.length - local.length).equals(local)); + }; + ``` + + + ```rust + fn same_key(local: &[u8], server: &[u8]) -> bool { + local == server || (server.len() > local.len() && server.ends_with(local)) + } + ``` + + + ```go + func sameKey(local, server []byte) bool { + return bytes.Equal(local, server) || + (len(server) > len(local) && bytes.HasSuffix(server, local)) + } + ``` + + + ```csharp + static bool SameKey(byte[] local, byte[] server) => + local.SequenceEqual(server) || + (server.Length > local.Length && + server.AsSpan(server.Length - local.Length).SequenceEqual(local)); + ``` + + + ```java + static boolean sameKey(byte[] local, byte[] server) { + if (Arrays.equals(local, server)) return true; + if (server.length <= local.length) return false; + byte[] tail = Arrays.copyOfRange(server, server.length - local.length, server.length); + return Arrays.equals(tail, local); + } + ``` + + + +Una vez que coincidan, adopta el `public_key_version` de esa fila para `set_identity`. Cuando compares versiones (por ejemplo, para elegir la clave más reciente), compara **numéricamente** — las versiones son marcas de tiempo en milisegundos de longitud de cadena variable, por lo que una comparación lexicográfica elige la incorrecta. + ### Falta la clave de conversación para un mensaje -No tienes la clave **en bruto** para el `conversation_key_version` de ese mensaje. +Un error como `Message encrypted with key version '…' but no matching key found` significa que no tienes la clave **en bruto** para el `conversation_key_version` de ese mensaje. -1. Descifra el material de clave desde `conversation_key_change_event` (eventos en vivo) o `meta.conversation_key_events` (historial) con `extract_conversation_keys`, **o** incluye esos blobs en `decrypt_events` +1. Descifra el material de clave desde `conversation_key_change_event` (eventos en vivo) o `meta.conversation_key_events` (historial) con `extract_conversation_keys`, **o** incluye esos blobs en `decrypt_events`—con `set_cache_keys(true)` habilitado, `decrypt_events` también retiene la clave verificada más reciente de cada conversación para que las llamadas posteriores a `decrypt_event` y `encrypt_*` puedan omitirla 2. Confirma que se agregaron claves de conversación para esa versión y que sigues siendo participante (consulta [Primeros pasos](/xchat/getting-started#4-set-up-conversation-keys)) ### El par no tiene claves públicas @@ -132,8 +199,10 @@ Quizás no ha completado la incorporación. Después de que se registren, carga La verificación es **fail-closed por defecto** (`reject_unverified = true`): el SDK ya rechaza los eventos firmados no verificados, así que un fallo aquí significa que las entradas de verificación son incorrectas, no que debas activar la comprobación. Causas comunes: - Entrada de clave de firma faltante o incompleta para el **remitente** (todos los campos que requiere el Chat XDK—consulta la referencia del [Chat XDK](/xchat/xchat-xdk)) +- No se pasaron claves de firma en la llamada y ninguna está almacenada mediante `set_signing_keys` - El remitente rotó versiones—vuelve a obtener sus claves públicas - Una versión de clave por debajo del piso aceptado nunca verifica +- En un **evento de cambio de clave de grupo**, quien lo firmó ya abandonó el grupo, así que sus claves ya no se sirven — consulta [Cambios de clave de miembros que se fueron](/xchat/groups#cambios-de-clave-de-miembros-que-se-fueron) El setter `set_reject_unverified` existe para **optar por salir** de este predeterminado (`false`, no recomendado). Si lo desactivaste antes, restaura el predeterminado fail-closed: @@ -170,6 +239,10 @@ El setter `set_reject_unverified` existe para **optar por salir** de este predet +### Una respuesta lleva `reply_preview_validation: "Invalid"` + +Las respuestas descifradas pueden llevar `reply_preview_validation` (`"Valid"` / `"Invalid"`; JavaScript usa `'valid'` / `'invalid'`). `Invalid` significa que la vista previa citada dentro del mensaje no coincide con el evento original firmado que incrusta—trata la cita como no confiable y renderiza el contenido citado solo desde el original validado. El mensaje en sí se verifica por separado y sigue siendo auténtico; no se lanza ninguna excepción por una vista previa inválida. + ### Los eventos antiguos fallan la verificación de forma permanente Errores como `signature missing or no matching signing key` o un desajuste ECDSA en eventos **antiguos** son permanentes. Las firmas son inmutables y se verifican reconstruyendo el payload firmado desde el evento en sí, así que un evento firmado sobre bytes distintos (o nunca firmado) fallará en cada carga futura—ninguna reintentación, actualización de claves o llamada a la API puede sanarlo. Trata estos eventos como tombstones, no como errores reintentables. Rotar la clave de conversación inicia un historial limpio y verificable desde ese punto en adelante; los mensajes nuevos no se ven afectados. @@ -184,8 +257,8 @@ Estos errores son específicos del cifrado de X Chat (no errores HTTP generales) |:---------|:---------| | Bytes de clave incorrectos | Pasa los bytes de la clave de conversación **en bruto** al Chat XDK, no la cadena de clave cifrada que retorna la API | | Nombres de campo JSON incorrectos | Mapea `encrypted_content` → `encoded_message_create_event` y `encoded_event_signature` → `encoded_message_event_signature` | -| Falta el id del mensaje | Genera `message_id` por tu cuenta y envía el mismo valor en el cuerpo de la solicitud | -| Desajuste de versión | Alinea `conversation_key_version` con la clave que usas; alinea la versión de la clave de firma con `set_key_version` / tu registro de clave pública | +| Id de mensaje incorrecto | Envía el `message_id` del payload devuelto—el SDK lo genera y lo incrusta en el evento firmado, así que cualquier otro valor falla. En los reintentos, reutiliza el mismo payload cifrado para que el id nunca se acuñe dos veces | +| Desajuste de versión | Alinea `conversation_key_version` con la clave que usas; alinea la versión de la clave de firma pasada a `set_identity` con tu registro de clave pública | | Forma del id en la ruta | Las rutas URL siguen necesitando el id de conversación con guión (`:` → `-`), pero para firmar el SDK acepta cualquier forma: `A:B`, `A-B` (en cualquier orden) o solo el id de usuario del destinatario—todos se canonicalizan a los mismos bytes firmados | ### La API devuelve 400 en una llamada que cambia estado @@ -210,5 +283,5 @@ Al investigar fallos criptográficos: - Registra solo los ids de conversación, ids de evento y **versiones** de clave - **No** registres texto plano, códigos de acceso, claves privadas ni blobs de clave completos -- Confirma que `set_key_version` coincide con `public_key_version` en tu registro de clave pública +- Confirma que la versión de clave de firma pasada a `set_identity` coincide con el `public_key_version` de tu registro de clave pública - Para historial incompleto, pagina **todas** las páginas de eventos para no saltarte metadatos de cambio de clave antes de descifrar diff --git a/es/xchat/xchat-xdk.mdx b/es/xchat/xchat-xdk.mdx index 31d15145f..1dee11461 100644 --- a/es/xchat/xchat-xdk.mdx +++ b/es/xchat/xchat-xdk.mdx @@ -32,7 +32,7 @@ Recorrido de la app: [Primeros pasos](/xchat/getting-started). Bots de ejemplo: [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 @@ Recorrido de la app: [Primeros pasos](/xchat/getting-started). Bots de ejemplo: com.x chatxdk - 0.2.1 + 0.4.0 ``` @@ -70,31 +70,37 @@ Recorrido de la app: [Primeros pasos](/xchat/getting-started). Bots de ejemplo: ## Inicio rápido -Descifra un backlog, cachea claves, descifra un evento y cifra una respuesta. Conecta el cuerpo de envío a [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) como se explica en [Primeros pasos](/xchat/getting-started). +Carga claves, establece tu identidad una vez, descifra un backlog, descifra un evento en vivo y cifra un mensaje. Conecta el cuerpo de envío a [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) como se explica en [Primeros pasos](/xchat/getting-started). + +Los snippets usan los dos almacenes de sesión **opcionales** para las formas más cortas de llamada: `set_signing_keys` mantiene las claves públicas de los demás participantes (obtenidas del [endpoint de claves públicas](/x-api/chat/get-user-public-keys)) para que las llamadas de descifrado puedan verificar a los remitentes sin un argumento por llamada, y `set_cache_keys(true)` deja que el SDK recuerde la clave verificada de cada conversación para que las llamadas de cifrado solo necesiten el id de conversación y el texto. Omite cualquiera de los dos y pasa los mismos valores por llamada—ambos estilos verifican de forma idéntica; consulta [Descifrar](#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 @@ Descifra un backlog, cachea claves, descifra un evento y cifra una respuesta. Co 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 @@ Descifra un backlog, cachea claves, descifra un evento y cifra una respuesta. Co 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 @@ Descifra un backlog, cachea claves, descifra un evento y cifra una respuesta. Co ## Ciclo de vida y claves -Construye el SDK, guarda las claves privadas (copia de seguridad segura de claves protegida con código de acceso o un blob de claves local), registra las claves **públicas** con la Chat API y establece tu **versión de clave pública** registrada después de unlock o import. La copia de seguridad segura de claves está implementada con **Juicebox**, y por eso los campos de configuración relacionados llevan ese nombre. Llama a `generate_keypairs` una vez por identidad de dispositivo/app; envía el payload de registro con POST al endpoint de claves públicas. Usa `setup` / `unlock` (y helpers de código de acceso relacionados) para la copia de seguridad segura de claves en cada binding. `export_keys` / `import_keys` (persistencia de blob de clave en bruto para bots y servidores) están disponibles solo en los **bindings nativos**—Python, Go, .NET, JVM y Rust. El binding JS/WASM no expone la exportación ni importación de claves en bruto: en un navegador cualquier script que acceda a la instancia podría exfiltrar la identidad, así que JS mantiene las claves dentro de la copia de seguridad segura de claves. Un servidor JS que quiera evitar un round-trip al realm de copia de seguridad por solicitud debería reutilizar una única instancia `Chat` desbloqueada entre solicitudes, o ejecutar un binding nativo donde se admitan blobs de clave. +Construye el SDK, guarda las claves privadas (copia de seguridad segura de claves protegida por código de acceso o un blob de claves local), registra las claves **públicas** con la Chat API y llama a **`set_identity(user_id, signing_key_version)`** después de unlock o import—establece el remitente y la versión de clave de firma con la que cada acción firmada predetermina, de modo que los métodos encrypt y prepare funcionan sin argumentos de identidad por llamada. Llama a `generate_keypairs` una vez por identidad de dispositivo/app; envía el payload de registro al endpoint de claves públicas. Usa `setup` / `unlock` (y helpers de código de acceso relacionados) para la copia de seguridad segura de claves en cada binding. `export_keys` / `import_keys` (persistencia de blob de claves en bruto para bots y servidores) están disponibles **solo en los bindings nativos**—Python, Go, .NET, JVM y Rust. El binding JS/WASM no expone exportación o importación de claves en bruto: en un navegador, cualquier script que alcance la instancia podría exfiltrar la identidad, así que JS mantiene las claves dentro de la copia de seguridad segura de claves. Un servidor JS que quiera evitar un round-trip a un realm de respaldo por solicitud debería reutilizar una instancia `Chat` desbloqueada entre solicitudes, o correr un binding nativo donde los blobs de claves estén soportados. + +El SDK también necesita la versión que la X API reporta para tu clave pública registrada, de modo que las entradas de cambio de clave dirigidas a otras versiones se omiten. `set_identity` la registra junto con el id de usuario; `import_keys` la acepta directamente como argumento opcional (Rust y Go usan `import_keys_with_version` / `ImportKeysWithVersion`). @@ -217,13 +272,13 @@ Construye el SDK, guarda las claves privadas (copia de seguridad segura de clave 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 @@ Construye el SDK, guarda las claves privadas (copia de seguridad segura de clave }); 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 @@ Construye el SDK, guarda las claves privadas (copia de seguridad segura de clave ```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 @@ Construye el SDK, guarda las claves privadas (copia de seguridad segura de clave // 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 @@ Construye el SDK, guarda las claves privadas (copia de seguridad segura de clave ```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 @@ Construye el SDK, guarda las claves privadas (copia de seguridad segura de clave ```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(); } @@ -295,29 +350,29 @@ Construye el SDK, guarda las claves privadas (copia de seguridad segura de clave -La configuración de la copia de seguridad segura de claves acepta tres formas: el objeto `juicebox_config` de la X API (recomendado—pasado verbatim), un wrapper completo `sdk_config` o un `token_map` desnudo. +La configuración de copia de seguridad segura de claves acepta tres formas: el objeto `juicebox_config` de la X API (recomendado—pasado textualmente), un wrapper completo `sdk_config`, o un `token_map` desnudo. -Opcional: la verificación de firma está **activada por defecto** (`reject_unverified = true`)—llama a `set_reject_unverified(false)` para desactivarla (no recomendado); `update_config` si cambia la configuración de realms de copia de seguridad; `is_unlocked` / `has_identity_key` para el estado de la UI. Las listas completas de campos están en los stubs del [repo chat-xdk](https://github.com/xdevplatform/chat-xdk). +Opcional: la verificación de firmas está **activa por defecto** (`reject_unverified = true`)—llama a `set_reject_unverified(false)` para desactivarla (no recomendado); `update_config` si cambia la configuración del realm de respaldo; `is_unlocked` / `has_identity_key` para el estado de la UI. Las listas completas de campos viven en los stubs del [repositorio chat-xdk](https://github.com/xdevplatform/chat-xdk). --- ## Claves de conversación -Tres métodos **prepare** hacen que una sola llamada haga todo lo que un cambio de clave necesita: generar una nueva clave de conversación, cifrarla para cada participante (a partir de las claves públicas que pases) y firmar el cambio. Todos devuelven la misma forma **`PreparedConversationChange`**, lista para POST—renombra el campo del SDK `encrypted_key` a **`encrypted_conversation_key`** en `conversation_participant_keys` y mapea las firmas de acción al campo de cuerpo requerido **`action_signatures`**. +Tres métodos **prepare** hacen que una sola llamada haga todo lo que un cambio de clave necesita: generar una clave de conversación nueva, cifrarla para cada participante (a partir de las claves públicas que pases) y firmar el cambio. La identidad del remitente y la versión de la clave de firma provienen de la sesión (`set_identity`); establece `sender_id` / `signing_key_version` en los params para sobrescribir. Todos devuelven la misma forma **`PreparedConversationChange`**, lista para hacer POST—renombra el campo del SDK `encrypted_key` a **`encrypted_conversation_key`** en `conversation_participant_keys`, y mapea las firmas de acción al campo del cuerpo requerido **`action_signatures`**. | Escenario | Método | Firmas de acción devueltas | |:----------|:-------|:---------------------------| | Iniciar un 1:1 (omite el id de conversación—el SDK lo deriva) o rotar la clave de cualquier conversación (pasa el id) | `prepare_conversation_key_change` | 1 | -| Crear un grupo (id acuñado por `POST /2/chat/conversations/group/initialize`) | `prepare_group_create` | 2—envía ambas | +| Crear un grupo (id generado por `POST /2/chat/conversations/group/initialize`) | `prepare_group_create` | 2—envía ambas | | Agregar miembros a un grupo | `prepare_group_members_change` | 2—envía ambas | -Conserva los bytes de la clave **en bruto** para `encrypt_message` y multimedia; nunca pases el sobre cifrado de la API a encrypt. +Conserva los bytes **en bruto** de la clave para `encrypt_message` y multimedia; nunca pases el sobre cifrado de la API a encrypt. -**Verifica las claves obtenidas antes de envolverlas.** Los métodos prepare cifran la nueva clave de conversación hacia cualesquiera claves públicas que pases. Antes de pasarlas, llama a `verify_key_binding(identity, signing, signature)` en cada registro obtenido—sus campos `public_key`, `signing_public_key` e `identity_public_key_signature` de la API de claves públicas—para que una clave de identidad sustituida no pueda recibir la clave de conversación. +**Verifica las claves recuperadas antes de envolverlas.** Los métodos prepare cifran la nueva clave de conversación hacia cualesquiera claves públicas que le pases. Antes de pasarlas, llama a `verify_key_binding(identity, signing, signature)` en cada registro obtenido—sus campos `public_key`, `signing_public_key` e `identity_public_key_signature` de la API de claves públicas—para que una clave de identidad sustituida no pueda recibir la clave de conversación. -Usa `extract_conversation_keys` en los payloads de eventos de cambio de clave para reconstruir `{ keys, latest_version }`. `decrypt_conversation_key` desenvuelve un solo blob ECIES. +Usa `extract_conversation_keys` en payloads de eventos de cambio de clave para reconstruir `{ keys, latest_version }`. `decrypt_conversation_key` desenvuelve un único blob ECIES. @@ -327,7 +382,7 @@ Usa `extract_conversation_keys` en los payloads de eventos de cambio de clave pa # {"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 @@ Usa `extract_conversation_keys` en los payloads de eventos de cambio de clave pa ```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 @@ Usa `extract_conversation_keys` en los payloads de eventos de cambio de clave pa ```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 @@ Usa `extract_conversation_keys` en los payloads de eventos de cambio de clave pa ```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 @@ Usa `extract_conversation_keys` en los payloads de eventos de cambio de clave pa ```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 @@ Usa `extract_conversation_keys` en los payloads de eventos de cambio de clave pa ```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); @@ -404,15 +452,24 @@ Usa `extract_conversation_keys` en los payloads de eventos de cambio de clave pa -Para crear grupo y agregar miembros, pasa los params que cada método necesita (listas de ids de miembros/admins para `prepare_group_create`; nuevos más el roster actual para `prepare_group_members_change`)—consulta [Grupos](/xchat/groups#create-the-group-and-establish-keys) para ejemplos. Ambos devuelven **dos** firmas de acción; el POST debe incluir ambas. +Para group create y member adds, pasa los params que cada método necesita (listas de ids de miembros/admin para `prepare_group_create`; los nuevos más el roster actual para `prepare_group_members_change`)—consulta [Grupos](/xchat/groups#create-the-group-and-establish-keys) para ejemplos. Ambos devuelven **dos** firmas de acción; el POST debe incluir ambas. --- ## Descifrar -**`decrypt_events`** es para el historial y el backlog: extrae las claves de conversación del stream, devuelve los mensajes descifrados y **recolecta** errores por evento en lugar de fallar el lote completo. **`decrypt_event`** es para un solo evento en vivo cuando ya tienes un caché de claves; lanza excepción/throw ante un fallo. +**`decrypt_events`** es para historial y backlog: extrae las claves de conversación del stream, devuelve los mensajes descifrados y **recopila** errores por evento en lugar de fallar todo el lote. **`decrypt_event`** es para un solo evento en vivo; lanza/arroja en caso de fallo. + +Pasa **claves de firma** para que el SDK pueda verificar a los remitentes. Mapea los campos de clave pública de la API a `SigningKeyEntry`: `public_key_version` → `public_key_version` (mismo nombre), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, más `identity_public_key_signature` y `user_id`. + +Dos almacenes de sesión opcionales te permiten omitir los argumentos de clave por llamada: -Pasa **claves de firma** para que el SDK pueda verificar a los remitentes. Mapea los campos de clave pública de la API a `SigningKeyEntry`: `public_key_version` → `public_key_version` (mismo nombre), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, más `identity_public_key_signature` y `user_id`. La verificación es obligatoria por defecto: omitir o pasar una lista vacía de claves de firma **no** la salta—los eventos firmados fallan (recolectados en `errors` para `decrypt_events`, lanzados para `decrypt_event`). Para realmente saltar la verificación primero debes llamar a `set_reject_unverified(false)` (no recomendado en producción). +- **`set_signing_keys(entries)`** guarda las claves de firma de los participantes; una llamada de descifrado que omita (o pase vacío) el argumento de claves de firma usa el almacén en su lugar. La verificación en sí no cambia—las claves entran al almacén solo mediante esta llamada, nunca desde los eventos que se descifran. Cada llamada reemplaza el conjunto anterior. +- **`set_cache_keys(true)`** habilita la caché de claves de conversación (desactivada por defecto). Mientras está activa, `decrypt_events` cachea, por conversación, la clave más reciente cuyo cambio de clave llevó una firma válida; `decrypt_event` recurre a ella cuando se omite su argumento de claves de conversación, y los helpers de cifrado resuelven una clave de conversación omitida a partir de ella. Desactivarla limpia la caché. + +Un argumento explícito no vacío siempre gana sobre los almacenes. Los argumentos explícitos por llamada siguen siendo de primera clase—y son la elección correcta para despliegues serverless o multi-instancia, donde una solicitud puede caer en una instancia nueva cuyos almacenes están vacíos. + +La verificación es obligatoria por defecto: omitir las claves de firma nunca la salta. Sin nada pasado y nada almacenado, los eventos firmados fallan (recolectados en `errors` para `decrypt_events`, lanzados para `decrypt_event`). Para saltar realmente la verificación debes llamar primero a `set_reject_unverified(false)` (no recomendado en producción). @@ -430,11 +487,11 @@ Pasa **claves de firma** para que el SDK pueda verificar a los remitentes. Mapea 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 @@ Pasa **claves de firma** para que el SDK pueda verificar a los remitentes. Mapea 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 @@ Pasa **claves de firma** para que el SDK pueda verificar a los remitentes. Mapea 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 @@ Pasa **claves de firma** para que el SDK pueda verificar a los remitentes. Mapea 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,48 +539,56 @@ Pasa **claves de firma** para que el SDK pueda verificar a los remitentes. Mapea 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); ``` --- -## Helpers de cifrado y envío +## Cifrar y helpers de envío + +**`encrypt_message(conversation_id, text)`** construye el texto cifrado firmado para un mensaje de texto; opcionales `entities`, `attachments` (mediante `media_hash_key`), `should_notify` y `ttl_msec`. La identidad del remitente se resuelve desde la sesión (`set_identity`) y la clave de conversación desde la caché de claves opcional (`set_cache_keys`)—o pasa `sender_id` / `signing_key_version` y `conversation_key` + `conversation_key_version` de forma explícita. El SDK genera el **`message_id`** (un UUID incrustado en el evento firmado) y lo devuelve en el payload—nunca acuñes el tuyo; reutiliza el mismo payload en los reintentos para que un id nunca se acuñe dos veces. Mapea el payload al cuerpo de send-message: `message_id` → **`message_id`**, `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**. -**`encrypt_message`** construye el texto cifrado firmado para un mensaje de texto (entidades opcionales, adjuntos vía `media_hash_key`, TTL, flags de notificación). Mapea el payload devuelto al cuerpo send-message: `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**, más tu **`message_id`**. +**Las respuestas son basadas en eventos.** `encrypt_reply(conversation_id, text, reply_to_event)` toma el evento en bruto en base64 al que se responde. El SDK deriva la vista previa citada (sequence id, remitente, texto, entities, attachments) a partir de él e incrusta el original firmado en el mensaje saliente para que los destinatarios puedan validar la cita. Pasa `reply_to_ckces`—los eventos de cambio de clave en bruto—cuando el original se cifró bajo una versión de clave más antigua que la respuesta. Cuando el original fue **editado**, pasa el evento de edición en bruto como `reply_to_edit_event`: la vista previa cita entonces lo que el mensaje dice ahora (su texto y entities provienen de la edición) y la edición viaja junto al original para que el receptor la verifique. Los campos explícitos `reply_to_*` permanecen como sobrescrituras para llamadores que ya no tienen el evento en bruto. -Usa **`encrypt_reply`**, **`encrypt_add_reaction`** y **`encrypt_remove_reaction`** para respuestas y reacciones (`sequence_id` apunta al padre). **`encrypt` / `decrypt`** son para metadatos UTF-8 bajo la clave de conversación (por ejemplo, un nombre de grupo cifrado)—no sobres de mensajes. **`encrypt_stream` / `decrypt_stream`** cifran los bytes de los adjuntos; consulta [Multimedia](/xchat/media). Los métodos de bajo nivel **`sign` / `verify` / `verify_key_binding`** admiten flujos avanzados; los cambios de clave de conversación, creaciones de grupo y adiciones de miembros son firmados por los [métodos prepare](#claves-de-conversaci-n). +**Las reacciones también son basadas en eventos.** `encrypt_add_reaction(target_event, emoji)` y `encrypt_remove_reaction(...)` derivan el id de conversación y el sequence id objetivo a partir del evento en bruto al que se reacciona; los mismos params pueden agregar y luego quitar una reacción. Establece `conversation_id` y `target_message_sequence_id` de forma explícita solo cuando ya no tengas el evento en bruto. -El id de conversación pasado a `encrypt_message` / `encrypt_reply` puede ser cualquier forma que tengas—`A:B` de eventos, `A-B` de listados o rutas URL (en cualquier orden) o solo el id de usuario del destinatario—el SDK lo canonicaliza antes de firmar. Los ids de grupo (con prefijo `g`) pasan sin cambios. +En el lado del receptor, un mensaje descifrado que cita una respuesta lleva **`reply_preview_validation`** (`"Valid"` / `"Invalid"`; el binding JS usa `'valid'` / `'invalid'`): el SDK verificó la firma del original incrustado contra tus claves de firma—nunca una clave transportada en el evento—lo descifró y comparó el contenido citado y el autor contra él. Cuando la vista previa incrusta un evento de edición, el SDK verifica la edición de la misma forma (misma conversación, mismo autor que el original) y comprueba el texto citado contra los contenidos editados en lugar del texto previo a la edición. El campo está ausente cuando el mensaje no lleva vista previa o la vista previa no incrusta un original. Trata las vistas previas `Invalid` como no confiables: el mensaje en sí es auténtico, pero el material citado no lo es—renderiza las citas solo desde el original validado. + +**`encrypt` / `decrypt`** son para metadatos UTF-8 bajo la clave de conversación (por ejemplo un nombre de grupo cifrado)—no sobres de mensaje. **`encrypt_stream` / `decrypt_stream`** cifran bytes de adjuntos; consulta [Multimedia](/xchat/media). Las funciones de bajo nivel **`sign` / `verify` / `verify_key_binding`** admiten flujos avanzados; los cambios de clave de conversación, group creates y member adds los firman los [métodos prepare](#conversation-keys). + +El id de conversación pasado a `encrypt_message` / `encrypt_reply` puede tener cualquier forma que tengas—`A:B` de eventos, `A-B` de listados o rutas URL (en cualquier orden), o solo el id de usuario del destinatario—el SDK lo canonicaliza antes de firmar. Los ids de grupo (con prefijo `g`) pasan sin cambios. ```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 @@ El id de conversación pasado a `encrypt_message` / `encrypt_reply` puede ser cu ```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 @@ El id de conversación pasado a `encrypt_message` / `encrypt_reply` puede ser cu ```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); @@ -611,7 +726,7 @@ El id de conversación pasado a `encrypt_message` / `encrypt_reply` puede ser cu ## Streams de multimedia -Cifra los bytes del archivo con la **misma** clave de conversación usada para el texto, sube mediante las APIs de multimedia de Chat y adjunta **`media_hash_key`** en `encrypt_message`. Este no es el modelo de multimedia de Posts (`expansions=attachments.media_keys`). Flujo completo de subida/descarga: [Multimedia](/xchat/media). +Cifra los bytes del archivo con la **misma** clave de conversación usada para el texto, sube mediante las APIs de multimedia del chat y adjunta **`media_hash_key`** en `encrypt_message`. Este no es el modelo de multimedia de Posts (`expansions=attachments.media_keys`). Flujo completo de subida/descarga: [Multimedia](/xchat/media). @@ -661,10 +776,10 @@ Cifra los bytes del archivo con la **misma** clave de conversación usada para e ### Streaming incremental para multimedia grande -Para archivos grandes, evita mantener el payload completo en memoria: `stream_encryptor()` / `stream_decryptor()` devuelven un `StreamEncryptor` / `StreamDecryptor` al que alimentas por chunks (unos 1 MB cada uno) con `push(chunk)`, y luego llamas a `finish()` una vez al final. Al descifrar, `finish()` detecta un stream truncado (falla si la entrada terminó antes del frame final), así que no trates el texto plano acumulado como completo hasta que tenga éxito. +Para archivos grandes, evita mantener toda la carga útil en memoria: `stream_encryptor()` / `stream_decryptor()` devuelven un `StreamEncryptor` / `StreamDecryptor` que alimentas por fragmentos (de aproximadamente 1 MB cada uno) con `push(chunk)`, y luego llamas a `finish()` una vez al final. Al descifrar, `finish()` detecta un stream truncado (falla si la entrada terminó antes del frame final), así que no trates el texto plano acumulado como completo hasta que `finish()` tenga éxito. -**Solo JS/WASM:** `finish()` consume y libera el objeto WASM subyacente—nunca llames a `free()` después de `finish()` (lanza error). Llama a `free()` solo para abandonar un stream *antes* de finalizar (por ejemplo, en una ruta de error). +**Solo JS/WASM:** `finish()` consume y libera el objeto WASM subyacente—nunca llames a `free()` después de `finish()` (lanza excepción). Llama a `free()` solo para abandonar un stream *antes* de finalizarlo (por ejemplo, en una ruta de error). @@ -701,7 +816,7 @@ Para archivos grandes, evita mantener el payload completo en memoria: `stream_en ## Utilidades -Los helpers de Base64/hex, sniffing de MIME y dimensiones de imagen están disponibles como funciones a nivel de módulo (Python/JS/Rust/Go) o `ChatXdkUtilities` (C#/Java)—útiles al construir metadatos de adjuntos sin traer librerías adicionales. +Los helpers de base64/hex, detección MIME y dimensiones de imágenes están disponibles como funciones a nivel de módulo (Python/JS/Rust/Go) o en `ChatXdkUtilities` (C#/Java)—útiles al construir metadatos de adjuntos sin traer librerías adicionales. @@ -792,23 +907,23 @@ Los helpers de Base64/hex, sniffing de MIME y dimensiones de imagen están dispo ## Tipos importantes -Estos tipos conceptuales aparecen en varios lenguajes (los nombres exactos de los campos difieren; JS suele usar discriminadores de evento en camelCase como `message`): +Estos tipos conceptuales aparecen entre lenguajes (los nombres exactos de campos difieren; JS suele usar discriminadores de evento en camelCase como `message`): -- **SendPayload** — valor de retorno de `encrypt_message` y helpers de cifrado relacionados; mapea al cuerpo de envío de la Chat API. -- **PublicKeyRegistrationPayload** — salida de `generate_keypairs` / getters de claves públicas para la API add-public-key. -- **SigningKeyEntry** — material público del remitente pasado a decrypt para la verificación de firma. -- **PreparedConversationChange** — salida de los tres métodos prepare: el `conversation_id` derivado o pasado, los bytes de `conversation_key` en bruto, `conversation_key_version`, `participant_keys` (`user_id`, `encrypted_key`, `public_key_version`) y `action_signatures` (`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, `signature_payload` opcional—omitido en firmas de cambio de clave porque ese payload incrusta la clave en texto plano). -- **DecryptEventsResult** — mensajes, errores opcionales y `conversation_keys` extraídas. +- **SendPayload** — valor de retorno de `encrypt_message` y los demás helpers de cifrado: el **`message_id`** generado por el SDK (un UUID incrustado en el evento firmado—envíalo como el `message_id` del mensaje y consérvalo para deduplicar), `encrypted_content`, `encoded_event_signature`, metadatos de firma, `conversation_key_version` y `should_notify`. Mapea al cuerpo de send de la Chat API. +- **PublicKeyRegistrationPayload** — salida de `generate_keypairs` / getters de clave pública para la API de add-public-key. +- **SigningKeyEntry** — material público del remitente pasado a decrypt para verificación de firmas, o almacenado mediante `set_signing_keys`. +- **PreparedConversationChange** — salida de los tres métodos prepare: el `conversation_id` derivado o pasado, los bytes en bruto de `conversation_key`, `conversation_key_version`, `participant_keys` (`user_id`, `encrypted_key`, `public_key_version`) y `action_signatures` (`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, `signature_payload` opcional—omitido en las firmas de cambio de clave porque ese payload incrusta la clave en texto plano). +- **DecryptEventsResult** — messages, `errors` opcional y `conversation_keys` extraídas. Los mensajes descifrados que citan una respuesta llevan `reply_preview_validation` (consulta [Cifrar y helpers de envío](#encrypt-and-send-helpers)). -Para listas completas de campos, usa los stubs de lenguaje en el [repo chat-xdk](https://github.com/xdevplatform/chat-xdk) (`docs/API.md`, `*.pyi`, `index.d.ts`). +Para listas completas de campos, usa los stubs de lenguaje en el [repositorio chat-xdk](https://github.com/xdevplatform/chat-xdk) (`docs/API.md`, `*.pyi`, `index.d.ts`). --- ## Errores -Python normalmente lanza **`ValueError`** con un mensaje descriptivo (por ejemplo, un código de acceso inválido). TypeScript/JavaScript lanza **`Error`**. Go devuelve `(value, error)`. Prefiere **`decrypt_events`** para el historial de modo que un evento defectuoso no aborte el lote; inspecciona la colección de errores para ver fallos parciales. +Python normalmente lanza **`ValueError`** con un mensaje descriptivo (por ejemplo, un código de acceso inválido). TypeScript/JavaScript lanza **`Error`**. Go devuelve `(value, error)`. Prefiere **`decrypt_events`** para el historial, así un evento defectuoso no aborta el lote; inspecciona la colección de errores para fallos parciales. -Algunos errores de verificación son **permanentes**. Las firmas son inmutables y se verifican reconstruyendo el payload firmado desde el evento en sí, así que un evento antiguo que falla con `signature missing or no matching signing key` o un desajuste ECDSA fallará en cada carga futura—ninguna reintentación, actualización de claves o llamada a la API puede sanarlo. Trátalos como tombstones, no como errores transitorios. Rotar la clave de conversación inicia un historial limpio y verificable desde ese punto en adelante. +Algunos errores de verificación son **permanentes**. Las firmas son inmutables y se verifican reconstruyendo el payload firmado desde el evento en sí, así que un evento antiguo que falla con `signature missing or no matching signing key` o un desajuste ECDSA fallará en cada carga futura—ninguna reintentación, actualización de claves o llamada a la API puede sanarlo. Trata estos como tombstones, no como errores transitorios. Rotar la clave de conversación inicia un historial limpio y verificable desde ese punto en adelante. --- @@ -819,7 +934,7 @@ Algunos errores de verificación son **permanentes**. Las firmas son inmutables Conecta el Chat XDK a la Chat API - Cifrado por stream y REST de multimedia + Cifrado de streams y REST de multimedia Webhooks y entrega de actividad diff --git a/ja/xchat/getting-started.mdx b/ja/xchat/getting-started.mdx index f1a555f1d..efcc63a62 100644 --- a/ja/xchat/getting-started.mdx +++ b/ja/xchat/getting-started.mdx @@ -45,11 +45,10 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: ```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 アプリは、2 つの要素を組み合わせて使用します: com.x chatxdk - 0.2.1 + 0.4.0 ``` @@ -136,11 +135,11 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: このステップでは、**すでに持っている鍵をロード**します — この identity が過去に初回セットアップを完了している場合に使用してください: - **安全な鍵バックアップ:** 公開鍵レコードの `juicebox_config` で SDK を構築し、パスコードで `unlock` して秘密鍵を復元します(たとえば新しいデバイスで)。 -- **鍵 blob:** 以前に `export_keys` でエクスポートした blob を `import_keys` に渡します。 +- **鍵 blob:** 以前に `export_keys` でエクスポートした blob を `import_keys` に渡し、登録済みの鍵バージョンをあわせて指定します(Rust と Go ではこのバリアントを `import_keys_with_version` / `ImportKeysWithVersion` と呼びます)。 -次に、登録済みの公開鍵バージョン(レコード上の `public_key_version`)を設定します。 +次に、**`set_identity(user_id, signing_key_version)`** を、あなたのユーザー ID とレコードの `public_key_version` とともに一度だけ呼び出します。これはセッション ID を保存します: 以降のすべての encrypt および prepare 呼び出しはこの ID として署名するため、呼び出しごとに送信者 ID や署名鍵バージョンを渡す必要はありません。 -**初めてセットアップする場合は?** 同じ方法で SDK を構築しますが、`unlock` / `import_keys` はスキップし、[ステップ 3](#3-鍵を作成して登録する初回セットアップ) に進んで鍵の作成、バックアップ、登録を行ってください。 +**初めてセットアップする場合は?** 同じ方法で SDK を構築しますが、`unlock` / `import_keys` はスキップし、[ステップ 3](#3-create-and-register-keys-first-time-setup) に進んで鍵の作成、バックアップ、登録を行ってください。 @@ -160,7 +159,9 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: getAuthToken: async (realmId) => getRealmTokenFromYourBackend(realmId), }); await chat.unlock('YOUR_PASSCODE'); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity('YOUR_USER_ID', signingKeyVersion); ``` @@ -189,11 +190,11 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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); } ``` @@ -244,18 +247,24 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: サーバーおよびボットのサンプルでは、通常**鍵 blob**(`export_keys` / `import_keys`)を使用します。クライアントアプリでは通常**安全な鍵バックアップ**(パスコードを使った `setup` / `unlock`)を使用します。両方のパスについては [Chat XDK](/xchat/xchat-xdk) リファレンスを参照してください。 -**独自の鍵を持ち込みますか?** `import_keys` は、Chat XDK の `export_keys` が生成する不透明な blob のみを受け付けます — これは鍵の完全な状態をバージョン付きで非公開にシリアライズしたものであり、未加工または PEM エンコードされた P-256 鍵ではありません。この blob を自分で構築することはできません: `generate_keypairs`([ステップ 3](#3-鍵を作成して登録する初回セットアップ))で鍵を生成し、blob を一度エクスポートして、base64 エンコードで保存してください。手作りまたは改変された blob はインポートに失敗します。 +**独自の鍵を持ち込みますか?** `import_keys` は、Chat XDK の `export_keys` が生成する不透明な blob のみを受け付けます — これは鍵の完全な状態をバージョン付きで非公開にシリアライズしたものであり、未加工または PEM エンコードされた P-256 鍵ではありません。この blob を自分で構築することはできません: `generate_keypairs`([ステップ 3](#3-create-and-register-keys-first-time-setup))で鍵を生成し、blob を一度エクスポートして、base64 エンコードで保存してください。手作りまたは改変された blob はインポートに失敗します。 --- ## 3. 鍵を作成して登録する(初回セットアップ) -[ステップ 2](#2-既存の鍵で-chat-xdk-を初期化する) で既存の鍵をロードした場合は、このステップをスキップしてください。それ以外の場合、新しい identity の 1 回限りのセットアップでは **3 つのこと**を行います: +[ステップ 2](#2-initialize-the-chat-xdk-with-existing-keys) で既存の鍵をロードした場合は、このステップをスキップしてください。それ以外の場合、新しい identity の 1 回限りのセットアップでは **3 つのこと**を行います: 1. **キーペアを作成する** — `generate_keypairs` が identity と署名のキーペアを生成します。 -2. **公開鍵を登録する** — 他のユーザーがあなた宛てに暗号化し、あなたの署名を検証できるよう、登録ペイロードを add-public-key エンドポイントに POST します。 -3. **秘密鍵を保存する** — パスコードで `setup` すると安全な鍵バックアップに書き込まれます(クライアント)。または `export_keys` が返す鍵 blob を安全に保存します(サーバーとボット)。 +2. **秘密鍵を保存する** — パスコードで `setup` すると安全な鍵バックアップに書き込まれます(クライアント)。または `export_keys` が返す鍵 blob を安全に保存します(サーバーとボット)。 +3. **公開鍵を登録する** — 他のユーザーがあなた宛てに暗号化し、あなたの署名を検証できるよう、登録ペイロードを add-public-key エンドポイントに POST します。 + +最後に、登録の鍵バージョンで `set_identity` を呼び出して、このセッションが新しい identity として署名するようにします。 + + +すべてのバインディング(Python、TypeScript、Go、Rust、C#、Java)向けの、すぐに実行できるワンタイム登録スクリプトが [`chat-xdk/examples`](https://github.com/xdevplatform/chat-xdk/tree/main/examples) にあります。新しい identity をオンボードするだけの場合は、以下のフローを手作りするのではなくそれらを利用してください。 + @@ -280,7 +289,7 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: ), ) 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")) ``` @@ -300,7 +309,7 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: generate_version: registration.generateVersion, }); await chat.setup('YOUR_PASSCODE'); - chat.setKeyVersion(String(registration.version ?? signingKeyVersion)); + chat.setIdentity('YOUR_USER_ID', String(registration.version ?? '1')); ``` @@ -316,6 +325,8 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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); ``` @@ -335,9 +346,15 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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) + } ``` @@ -349,7 +366,7 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: $"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"); ``` @@ -367,7 +384,7 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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"); ``` @@ -380,7 +397,7 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: ## 4. 会話鍵をセットアップする -**`prepare_conversation_key_change`** を、あなたのユーザー ID、署名鍵のバージョン、およびすべての参加者の identity 公開鍵とともに呼び出します。1 回の呼び出しで新しい会話鍵を生成し、各参加者向けに暗号化し、変更に署名します。結果を **add conversation keys** エンドポイント(`POST /2/chat/conversations/{id}/keys`)に POST します — ボディには `conversation_key_version`、`conversation_participant_keys`(SDK の `encrypted_key` → API の `encrypted_conversation_key`)、および **`action_signatures`** が必要です(必須;API は署名がないと呼び出しを拒否します)。送信用に**未加工の**会話鍵を保持します。 +**`prepare_conversation_key_change`** を、すべての参加者の identity 公開鍵とともに呼び出します。送信者 ID はステップ 2 で設定したセッションから取得されます。1 回の呼び出しで新しい会話鍵を生成し、各参加者向けに暗号化し、変更に署名します。結果を **add conversation keys** エンドポイント(`POST /2/chat/conversations/{id}/keys`)に POST します — ボディには `conversation_key_version`、`conversation_participant_keys`(SDK の `encrypted_key` → API の `encrypted_conversation_key`)、および **`action_signatures`** が必要です(必須;API は署名がないと呼び出しを拒否します)。送信用に**未加工の**会話鍵を保持します。 レスポンスは、正規の会話 ID(`data.conversation_id` — 1:1 の場合はハイフンで結合されたペア、グループの場合は g プレフィックス付き ID)と、鍵変更の `data.sequence_id` を返します。以降のリクエストでは、クライアント側で再構築するのではなく、返された ID を使用してください。同じ呼び出しは後で鍵を**ローテーション**するためにも使えます: 既存の会話 ID を `prepare_conversation_key_change` に渡し、新しい鍵バージョンで POST します。会話鍵が漏洩したと疑われる場合はローテーションしてください — ローテーションは**将来の**メッセージのみを保護します;以前の鍵バージョンで暗号化されたメッセージは、そのバージョンを保持している人にとっては引き続き読み取り可能です。 @@ -398,8 +415,6 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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 ) @@ -446,8 +461,6 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: // 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'), @@ -480,9 +493,9 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: ```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 @@ -532,8 +545,6 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: ```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}, @@ -572,21 +583,18 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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 { @@ -625,12 +633,9 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: 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> parts = new ArrayList<>(); for (var pk : prepared.participantKeys) { @@ -673,36 +678,32 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: ## 5. メッセージを送信する -**未加工の**会話鍵バイトで暗号化します。送信リクエストでは、次のようにマップします: +ステップ 4 の**未加工の**会話鍵で暗号化します。SDK はメッセージ ID(UUID)を生成し、署名付きイベントに埋め込み、ペイロード上で返します — 自分で生成することはありません。送信リクエストでは、次のようにマップします: | Chat XDK フィールド | リクエストボディフィールド | |:---------------|:-------------------| | `encrypted_content` / `encryptedContent` / `EncryptedContent` | `encoded_message_create_event` | | `encoded_event_signature` / `encodedEventSignature` / `EncodedEventSignature` | `encoded_message_event_signature` | -| 生成した ID | `message_id` | +| ペイロードの `message_id` / `messageId` / `MessageId` | `message_id` | API で要求される場合、URL パスでは**ハイフン付きの**会話 ID を使用します(`:` → `-`)。SDK 自体は柔軟です: `encrypt_message` と `encrypt_reply` は、保持している任意の形式で ID を受け付けます — イベントからの `A:B`、リストや URL パスからの `A-B`(どちらの順序でも可)、あるいは受信者のユーザー ID だけでも構いません — そして署名する前に正規化します。グループ ID(`g` プレフィックス付き)はそのまま渡されます。 ```python - import uuid from xdk.chat.models import SendMessageRequest - message_id = str(uuid.uuid4()) + # Sender identity resolves from set_identity (step 2) payload = chat.encrypt_message( - message_id, - "YOUR_USER_ID", "CONVERSATION_ID", - conv_key, "Hello!", - conv_key_version, - signing_key_version, + conversation_key=conv_key, + conversation_key_version=conv_key_version, ) client.chat.send_message( "RECIPIENT_USER_ID", SendMessageRequest( - message_id=message_id, + message_id=payload.message_id, # SDK-generated, embedded in the signed event encoded_message_create_event=payload.encrypted_content, encoded_message_event_signature=payload.encoded_event_signature, ), @@ -711,20 +712,15 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I ```typescript - import { randomUUID } from 'crypto'; - - const messageId = randomUUID(); + // Sender identity resolves from setIdentity (step 2) const payload = chat.encryptMessage({ - messageId, - senderId: 'YOUR_USER_ID', conversationId: 'CONVERSATION_ID', - conversationKey: convKey, text: 'Hello!', + conversationKey: convKey, conversationKeyVersion: convKeyVersion, - signingKeyVersion, }); await client.chat.sendMessage('RECIPIENT_USER_ID', { - message_id: messageId, + message_id: payload.messageId, // SDK-generated, embedded in the signed event encoded_message_create_event: payload.encryptedContent, encoded_message_event_signature: payload.encodedEventSignature, }); @@ -734,18 +730,14 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I ```rust use chat_xdk_core::EncryptMessageParams; - let message_id = uuid::Uuid::new_v4().to_string(); - let payload = chat.encrypt_message(EncryptMessageParams::new( - &message_id, - &sender_id, - &conversation_id, - conv_key, - "Hello!", - &conv_key_version, - &signing_key_version, - ))?; + // Sender identity resolves from set_identity (step 2) + let payload = chat.encrypt_message( + EncryptMessageParams::new(&conversation_id, "Hello!") + .with_conversation_key(conv_key, &conv_key_version), + )?; let body = serde_json::json!({ - "message_id": message_id, + // SDK-generated, embedded in the signed event + "message_id": payload.message_id, "encoded_message_create_event": payload.encrypted_content, "encoded_message_event_signature": payload.encoded_event_signature, }); @@ -758,18 +750,19 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I ```go - messageID := uuid.NewString() + // Sender identity resolves from SetIdentity (step 2) payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{ - MessageID: messageID, - SenderID: senderID, ConversationID: conversationID, - ConversationKey: convKey, Text: "Hello!", + ConversationKey: convKey, ConversationKeyVersion: convKeyVersion, - SigningKeyVersion: signingKeyVersion, }) + if err != nil { + log.Fatal(err) + } body, _ := json.Marshal(map[string]string{ - "message_id": messageID, + // SDK-generated, embedded in the signed event + "message_id": payload.MessageID, "encoded_message_create_event": payload.EncryptedContent, "encoded_message_event_signature": payload.EncodedEventSignature, }) @@ -785,18 +778,14 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I ```csharp - var messageId = Guid.NewGuid().ToString(); - var payload = chat.EncryptMessage(new EncryptMessageParams { - MessageId = messageId, - SenderId = senderId, - ConversationId = conversationId, + // Sender identity resolves from SetIdentity (step 2) + var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hello!") { ConversationKey = convKey, - Text = "Hello!", ConversationKeyVersion = convKeyVersion, - SigningKeyVersion = signingKeyVersion, }); var sendJson = System.Text.Json.JsonSerializer.Serialize(new Dictionary { - ["message_id"] = messageId, + // SDK-generated, embedded in the signed event + ["message_id"] = payload.MessageId, ["encoded_message_create_event"] = payload.EncryptedContent, ["encoded_message_event_signature"] = payload.EncodedEventSignature, }); @@ -810,19 +799,16 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I ```java - EncryptMessageParams params = new EncryptMessageParams(); - params.messageId = UUID.randomUUID().toString(); - params.senderId = senderId; - params.conversationId = conversationId; + // Sender identity resolves from setIdentity (step 2) + EncryptMessageParams params = new EncryptMessageParams(conversationId, "Hello!"); params.conversationKey = convKey; - params.text = "Hello!"; params.conversationKeyVersion = convKeyVersion; - params.signingKeyVersion = signingKeyVersion; SendPayload payload = chat.encryptMessage(params); String pathId = conversationId.replace(':', '-'); String sendJson = new ObjectMapper().writeValueAsString(Map.of( - "message_id", params.messageId, + // SDK-generated, embedded in the signed event + "message_id", payload.messageId, "encoded_message_create_event", payload.encryptedContent, "encoded_message_event_signature", payload.encodedEventSignature)); HttpRequest req = HttpRequest.newBuilder() @@ -836,6 +822,10 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I + +このフローでは、ステップ 4 で作成したばかりなので、スニペットは会話鍵を明示的に渡しています。鍵キャッシュがオンで、`decrypt_events` パスが会話の鍵を検証済み([ステップ 6](#6-receive-and-decrypt))であれば、`encrypt_message(conversation_id, text)` だけで十分です — SDK が最新の検証済み鍵を埋めます。再試行では**同じ**暗号化されたペイロードを再送信し、ID が二度と生成されないようにしてください。 + + --- ## 6. 受信して復号する @@ -843,15 +833,15 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I ライブトラフィックには [Webhook またはアクティビティストリーム](/xchat/real-time-events) を使用します。履歴には会話**イベント**をページングします。 - ライブペイロードのフィールド: `encoded_event`、オプションの `conversation_key_change_event` -- 履歴: `GET /2/chat/conversations/{id}/events` — 全イベントに対する **`decrypt_events`** に加えて `meta.conversation_key_events` を優先 -- 署名検証のために送信者の公開鍵を decrypt に渡します(API フィールドを `SigningKeyEntry` にマップします;[Chat XDK](/xchat/xchat-xdk) を参照) +- 履歴: `GET /2/chat/conversations/{id}/events` — 全イベントに対する **`decrypt_events`** と `meta.conversation_key_events` を優先 +- 復号には、SDK が各メッセージの作者を検証できるように、送信者の**署名鍵**が必要です。これらは他の参加者の*公開*鍵です — ステップ 4 で使用したのと同じ public-keys エンドポイントから取得し、フィールドを `SigningKeyEntry` にマップします(以下のスニペットにはマッピングが含まれています) +- 署名鍵(および `decrypt_event` については会話鍵)を毎回渡す**か**、2 つのオプションのセッションストアを一度設定して短い呼び出し形式を使用できます。以下のスニペットはストアを使用します: `set_signing_keys(entries)` は参加者の鍵を保持し、`set_cache_keys(true)`(デフォルトではオフ)は各会話の最新の**署名検証済み**鍵を保持するため、後続の呼び出しでは鍵引数を省略できます。両方のスタイルは同じように検証します - JavaScript は camelCase のイベントタイプ(`message`)を使用します;他の言語は JSON で `"Message"` と snake_case フィールドを使用します ```python - conversation_keys = {} # conversation_id -> { version: key_bytes } - + # Once per process: fill the signing-key store and enable the key cache def signing_keys_for(user_id: str) -> list[dict]: resp = client.chat.get_user_public_keys( user_id, @@ -870,25 +860,34 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I for r in resp.data ] + chat.set_signing_keys( + signing_keys_for("YOUR_USER_ID") + signing_keys_for("RECIPIENT_USER_ID") + ) + chat.set_cache_keys(True) + + # Initial load or pagination: batch decrypt. Conversation keys are + # extracted from the KeyChange events in the batch; per-event failures + # are collected in result["errors"], never raised. + result = chat.decrypt_events(all_events_b64) + for dm in result["messages"]: + event = dm["event"] + if event["type"] == "Message" and event["content"]["content_type"] == "Text": + print(event["sender_id"], event["content"]["text"], event["verified"]) + + # Live traffic: one event at a time def handle_payload(payload: dict): - cid = payload["conversation_id"] if payload.get("conversation_key_change_event"): - conversation_keys[cid] = chat.extract_conversation_keys( - [payload["conversation_key_change_event"]] - )["keys"] - event = chat.decrypt_event( - payload["encoded_event"], - conversation_keys.get(cid, {}), - signing_keys_for(payload["sender_id"]), - ) - if event.get("type") == "Message" and event.get("content", {}).get("content_type") == "Text": - print(event["sender_id"], event["content"]["text"], event.get("verified")) + # A rotation enters the key cache only after its signature + # verifies, which is what decrypt_events does + chat.decrypt_events([payload["conversation_key_change_event"]]) + event = chat.decrypt_event(payload["encoded_event"]) # raises on failure + if event["type"] == "Message" and event["content"]["content_type"] == "Text": + print(event["sender_id"], event["content"]["text"], event["verified"]) ``` ```typescript - const conversationKeys = new Map>(); - + // Once per process: fill the signing-key store and enable the key cache async function signingKeysFor(userId: string) { const resp = await client.chat.getUserPublicKeys(userId, { publicKeyFields: [ @@ -909,24 +908,33 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I })); } - async function handlePayload(payload: { - conversation_id: string; + chat.setSigningKeys([ + ...(await signingKeysFor('YOUR_USER_ID')), + ...(await signingKeysFor('RECIPIENT_USER_ID')), + ]); + chat.setCacheKeys(true); + + // Initial load or pagination: batch decrypt. Conversation keys are + // extracted from the KeyChange events in the batch; per-event failures + // are collected in result.errors, never thrown. + const result = chat.decryptEvents(allEventsB64); + for (const dm of result.messages) { + if (dm.event.type === 'message' && dm.event.content?.contentType === 'text') { + console.log(dm.event.senderId, dm.event.content.text, dm.event.verified); + } + } + + // Live traffic: one event at a time + function handlePayload(payload: { encoded_event: string; - sender_id: string; conversation_key_change_event?: string; }) { - const cid = payload.conversation_id; if (payload.conversation_key_change_event) { - conversationKeys.set( - cid, - chat.extractConversationKeys([payload.conversation_key_change_event]).keys, - ); + // A rotation enters the key cache only after its signature + // verifies, which is what decryptEvents does + chat.decryptEvents([payload.conversation_key_change_event]); } - const event = chat.decryptEvent( - payload.encoded_event, - conversationKeys.get(cid) ?? {}, - await signingKeysFor(payload.sender_id), - ); + const event = chat.decryptEvent(payload.encoded_event); // throws on failure if (event.type === 'message' && event.content?.contentType === 'text') { console.log(event.senderId, event.content.text, event.verified); } @@ -935,25 +943,48 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I ```rust - // Build Vec from GET /2/users/{id}/public_keys - // (public_key_version, public_key, signing_public_key, identity_public_key_signature) + // Once per instance: fill the signing-key store (Vec + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.set_signing_keys(participant_signing_keys); + chat.set_cache_keys(true); + + // Initial load: batch decrypt — per-event failures land in result.errors + let result = chat.decrypt_events(&all_events_b64, &[]); + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what decrypt_events does if let Some(kc) = key_change_b64.as_deref() { - let extracted = chat.extract_conversation_keys(&[kc]); - conv_keys.extend(extracted.keys); + chat.decrypt_events(&[kc], &[]); } - let event = chat.decrypt_event(&encoded_event, &conv_keys, &sender_signing_keys)?; + let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?; ``` ```go - if keyChange != "" { - extracted, _ := chat.ExtractConversationKeys([]string{keyChange}) - for v, k := range extracted.Keys { - convKeys[v] = k + // Once per instance: fill the signing-key store ([]SigningKeyEntry + // from GET /2/users/{id}/public_keys) and enable the key cache + if err := chat.SetSigningKeys(participantSigningKeys); err != nil { + log.Fatal(err) + } + chat.SetCacheKeys(true) + + // Initial load: batch decrypt — per-event failures land in result.Errors + result, err := chat.DecryptEvents(allEventsB64, nil) + if err != nil { + log.Fatal(err) + } + for _, dm := range result.Messages { + if dm.Event.Type == "Message" { + fmt.Println(dm.Event.AsMessage().Text()) } } - event, err := chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys) + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what DecryptEvents does + if keyChange != "" { + chat.DecryptEvents([]string{keyChange}, nil) + } + event, err := chat.DecryptEvent(encodedEvent, nil, nil) if err == nil && event.Type == "Message" { fmt.Println(event.AsMessage().Text()) } @@ -961,24 +992,49 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I ```csharp - if (!string.IsNullOrEmpty(keyChangeB64)) + // Once per instance: fill the signing-key store (SigningKeyEntry list + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.SetSigningKeys(participantSigningKeys); + chat.SetCacheKeys(true); + + // Initial load: batch decrypt — per-event failures land in result.Errors + var result = chat.DecryptEvents(allEventsB64); + foreach (var dm in result.Messages) { - var extracted = chat.ExtractConversationKeys(new[] { keyChangeB64 }); - foreach (var kv in extracted.Keys) - convKeys[kv.Key] = kv.Value; + if (dm.Event.GetProperty("type").GetString() == "Message") + Console.WriteLine(dm.Event.GetProperty("content").GetProperty("text").GetString()); } - var evt = chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys); + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what DecryptEvents does + if (!string.IsNullOrEmpty(keyChangeB64)) + chat.DecryptEvents(new[] { keyChangeB64 }); + var evt = chat.DecryptEvent(encodedEvent); // throws on failure if (evt.GetProperty("type").GetString() == "Message") Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString()); ``` ```java + // Once per instance: fill the signing-key store (SigningKeyEntry list + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.setSigningKeys(participantSigningKeys); + chat.setCacheKeys(true); + + // Initial load: batch decrypt — per-event failures land in result.errors + DecryptEventsResult result = chat.decryptEvents(allEventsB64, null); + for (DecryptedMessage dm : result.messages) { + if ("Message".equals(dm.event.path("type").asText())) { + System.out.println(dm.event.path("content").path("text").asText()); + } + } + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what decryptEvents does if (keyChangeB64 != null && !keyChangeB64.isEmpty()) { - var extracted = chat.extractConversationKeys(List.of(keyChangeB64)); - convKeys.putAll(extracted.keys); + chat.decryptEvents(List.of(keyChangeB64), null); } - JsonNode evt = chat.decryptEvent(encodedEvent, convKeys, senderSigningKeys); + JsonNode evt = chat.decryptEvent(encodedEvent, (Map) null, null); if ("Message".equals(evt.path("type").asText())) { System.out.println(evt.path("content").path("text").asText()); } @@ -986,33 +1042,15 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I -全言語向けの完全なポール&リプライボット: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples)。 + +**サーバーレスまたはマルチインスタンス?** 署名鍵ストアと鍵キャッシュは SDK インスタンスのメモリに存在します。それが合わない場合 — 1 つの呼び出しが復号し、別のものが送信する — 代わりに鍵を明示的に渡してください: `decrypt_events(events, signing_keys)`、`decrypt_event(event_b64, conversation_keys, signing_keys)`、および暗号化メソッドの `conversation_key`/`conversation_key_version` 上書き。`decrypt_events` が返す `conversation_keys` を自分で永続化し、次回渡してください。 + + +すべての言語向けの完全なポール&リプライボット: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples)。 --- ## ベストプラクティス -- 未加工の会話鍵と送信者の公開鍵をキャッシュし、署名検証が失敗した場合は更新する +- 署名鍵ストアを最新に保つ: 送信者が新しい鍵バージョンを登録したときは、参加者全員を含めて `set_signing_keys` を再呼び出しし、署名検証の失敗時は更新する - `event_uuid` でライブ配信の重複を排除する -- ページネーションが完了するまでイベント履歴をページングし、鍵変更のメタデータを見逃さないようにする -- 本番環境でパスコード、秘密鍵、メッセージ平文をログに残さない -- Web アプリでは、OAuth トークン(および鍵バックアップ realm トークン発行)をサーバー側で保持し、秘密鍵はクライアントの Chat XDK 内でのみ保持することが望ましい - ---- - -## 次のステップ - - - - すべての言語バインディングのメソッドと型 - - - 暗号化された画像とファイル添付 - - - 複数参加者の会話とメタデータ - - - Webhook とアクティビティ配信 - - diff --git a/ja/xchat/groups.mdx b/ja/xchat/groups.mdx index b2f0143f9..f78e9b439 100644 --- a/ja/xchat/groups.mdx +++ b/ja/xchat/groups.mdx @@ -29,7 +29,7 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt 1. `POST /2/chat/conversations/group/initialize` でグループ ID を発行します — レスポンスの `data.conversation_id` は、以下のすべての箇所で使用する g プレフィックス付きの ID です。 2. 各メンバーの identity 公開鍵と `public_key_version` をロードします(**Encryption keys** の下にある `GET` 公開鍵ルート;[`GET /2/users/public_keys`](/x-api/users/get-public-keys-for-multiple-users) は複数のユーザーを 1 回のリクエストで取得します)。使用前に各レコードを `verify_key_binding` で検証してください([Getting Started](/xchat/getting-started#4-set-up-conversation-keys) の警告を参照)。 -3. **`prepare_group_create`** を、**すべての**メンバー(自分自身を含む)、g プレフィックス付き ID、メンバー/管理者 ID リストとともに一度実行します。1 回の呼び出しで会話鍵を生成し、すべてのメンバー用にラップし、作成に署名します — **2 つ**のアクション署名(会話鍵の変更とグループ作成)を返します。 +3. **`prepare_group_create`** を、**すべての**メンバー(自分自身を含む)、g プレフィックス付き ID、メンバー/管理者 ID リストとともに一度実行します。1 回の呼び出しで会話鍵を生成し、すべてのメンバー用にラップし、`set_identity` で設定したセッション ID で作成に署名します — **2 つ**のアクション署名(会話鍵の変更とグループ作成)を返します。 4. `POST /2/chat/conversations/group` に、グループのメンバー/管理者、`conversation_key_version`、`conversation_participant_keys`(SDK **`encrypted_key`** → API **`encrypted_conversation_key`**)、および**両方**の `action_signatures` を送信します。検証失敗は、安定した人間が読めるメッセージとして返されます。たとえば `"Too many members: adding these members would exceed the allowed group size."` や `"Cannot add all members: one or more of the requested members cannot be added to this conversation."` です。 5. 暗号化/復号のために、**未加工の**会話鍵と**バージョン**を保持します。 @@ -38,8 +38,9 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ```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", ) @@ -50,8 +51,9 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ```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', }); @@ -60,9 +62,9 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ```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)?; @@ -71,8 +73,8 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ```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", }) @@ -83,23 +85,20 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ```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 ``` ```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 @@ -180,6 +179,17 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt メンバーシップに起因するローテーション後は、常に**最新の**鍵バージョンで暗号化してください。 +### 離脱したメンバーによる鍵変更 + +グループの鍵変更イベントは、それを実行した人 — 多くの場合は作成者または管理者 — によって署名されます。そのメンバーが後で**グループを離脱**した(またはアカウントを無効化した)場合、公開鍵エンドポイントはそのメンバーの鍵を返さなくなるため、検証済みの復号パス(署名鍵付きの `decrypt_events`)はそれらの鍵変更イベントで `signature missing or no matching signing key` で失敗します。イベントは壊れていません;検証素材が単に配信されなくなっただけです。 + +長命なグループはこれを想定し、検証できない鍵変更イベントについては **`extract_conversation_keys`** にフォールバックする必要があります。このパスは署名チェックをスキップし、あなたの identity 鍵で復号することによって会話鍵を回復します。セキュリティモデルは以下の理由で維持されます: + +- **あなたの identity 鍵に暗号化された**鍵素材のみが回復可能です — 第三者があなたが読める鍵を注入することはできません +- すべての**メッセージ**は依然として送信者ごとに署名検証されるため、メッセージの作成者情報には影響しません + +検証済みパスを最優先にしてください: `decrypt_events`(`set_cache_keys(true)` が有効な場合、SDK の鍵キャッシュにも供給されます)を使用し、それが拒否した鍵変更イベントに対してのみ `extract_conversation_keys` を使ってください。 + --- ## チェックリスト diff --git a/ja/xchat/media.mdx b/ja/xchat/media.mdx index 8565ec972..6b707aed3 100644 --- a/ja/xchat/media.mdx +++ b/ja/xchat/media.mdx @@ -122,23 +122,19 @@ flowchart LR ## 添付ファイル付きで送信する -メディア添付付きで暗号化し、その後メッセージ送信ボディを POST します(フィールドマッピングは [Getting Started](/xchat/getting-started#5-send-a-message) と同じ)。 +メディア添付付きで暗号化し、その後メッセージ送信ボディを POST します(フィールドマッピングは [Getting Started](/xchat/getting-started#5-send-a-message) と同じ)。SDK は `message_id` を生成してペイロードで返します — その値を送信し、ID が二度と生成されないように再試行時には同じペイロードを再利用してください。 ```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, @@ -151,7 +147,7 @@ flowchart LR 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, ), @@ -160,26 +156,23 @@ flowchart LR ```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, }); @@ -187,10 +180,23 @@ flowchart LR ```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, }); @@ -203,10 +209,12 @@ flowchart LR ```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, @@ -216,42 +224,44 @@ flowchart LR 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 ``` ```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 ``` ```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 ``` +会話鍵のペアは完全に省略することもできます: `set_cache_keys(true)` を有効にすると、`encrypt_message` は会話の最新の検証済み鍵変更から鍵とバージョンを解決します([Getting Started](/xchat/getting-started) を参照)。 + --- ## ダウンロードして復号する diff --git a/ja/xchat/real-time-events.mdx b/ja/xchat/real-time-events.mdx index 7edc39d75..5e789c320 100644 --- a/ja/xchat/real-time-events.mdx +++ b/ja/xchat/real-time-events.mdx @@ -10,7 +10,7 @@ X は **`chat.received`**、**`chat.sent`**、および関連する X Chat ア |:------|:-----| | **X Activity API** | `GET /2/activity/stream`;`POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions`(オペレーションごとの OpenAPI security を参照) | | **Webhooks** | 独自の HTTPS URL で終端する場合のオプションの `POST` / `GET` `/2/webhooks` および `PUT` / `DELETE` `/2/webhooks/{webhook_id}` ルート | -| **Chat XDK** | `extract_conversation_keys`、`decrypt_event` / `decrypt_events` | +| **Chat XDK** | `decrypt_event` / `decrypt_events`、および `set_signing_keys` / `set_cache_keys` セッションストア | プライベート X Chat イベントタイプは、監視するユーザーに対する認可が必要です。暗号化された X Chat ファイル添付は、Post API の `expansions=attachments.media_keys` / `media.fields=variants` ではなく、**`media_hash_key`** と X Chat メディアダウンロードを使用します。 @@ -86,118 +86,76 @@ Webhook を使用する場合、コンシューマシークレットを使った ## 3. Chat XDK で復号する -ライブフィールド: **`payload.encoded_event`**、オプションの **`payload.conversation_key_change_event`**。**`event_uuid`** で重複を排除します。 +ライブフィールド: **`payload.encoded_event`**、オプションの **`payload.conversation_key_change_event`**。**`event_uuid`** で配信の重複を排除します;メッセージは、復号済みイベントに含まれる **`message_id`** で重複を排除します — これは署名済みコンテンツの一部です。一方、シーケンス ID はバックエンドが割り当てる、署名されていないメタデータです。 + +以下のスニペットは、最短のハンドラを実現するために 2 つの**オプションの**セッションストアを使用します: `set_signing_keys` は参加者の公開鍵を保持し([public-keys エンドポイント](/x-api/chat/get-user-public-keys)から一度取得します)、`set_cache_keys(true)` は各会話の検証済み鍵を保持するため、`decrypt_event` はイベントだけを必要とします。ペイロードに `conversation_key_change_event` が含まれる場合、まずそれを `decrypt_events` に通してください: 鍵変更を検証し、キャッシュがオンならその鍵を保持して、続く `decrypt_event` の呼び出しで使用できるようにします。インスタンス状態を持ちたくない? 代わりに呼び出しごとに鍵を渡してください — このセクションの末尾のノートを参照。 JavaScript は camelCase のイベントタイプ(`message`)を使用します;他のバインディングは `"Message"` と snake_case フィールドを使用します。 ```python - from chat_xdk import Chat - - chat = Chat(JUICEBOX_CONFIG_JSON) - chat.unlock("YOUR_PASSCODE") - chat.set_key_version(SIGNING_KEY_VERSION) - conversation_keys = {} - - def signing_keys(user_id: str): - resp = api_client.chat.get_user_public_keys( - user_id, - public_key_fields=[ - "public_key_version", "public_key", "signing_public_key", "identity_public_key_signature", - ], - ) - return [ - { - "user_id": user_id, - "public_key_version": r["public_key_version"], - "public_key": r["signing_public_key"], - "identity_public_key": r["public_key"], - "identity_public_key_signature": r["identity_public_key_signature"], - } - for r in resp.data - ] + # chat has keys loaded and set_identity called (see Getting Started) + chat.set_cache_keys(True) + chat.set_signing_keys(participant_signing_keys) # all participants, from the public-key routes data = body.get("data") or {} if data.get("event_type") in ("chat.received", "chat.sent"): p = data.get("payload") or {} - cid = p.get("conversation_id") if p.get("conversation_key_change_event"): - conversation_keys[cid] = chat.extract_conversation_keys( - [p["conversation_key_change_event"]] - )["keys"] - ev = chat.decrypt_event( - p["encoded_event"], - conversation_keys.get(cid, {}), - signing_keys(p["sender_id"]), - ) + # Verify the key change and retain its key in the cache + chat.decrypt_events([p["conversation_key_change_event"]]) + ev = chat.decrypt_event(p["encoded_event"]) + if ev["type"] == "Message": + print(ev["sender_id"], ev["content"]["text"]) ``` ```typescript - import { createChat } from '@xdevplatform/chat-xdk'; - - const chat = await createChat({ - juiceboxConfig: JUICEBOX_CONFIG_JSON, - getAuthToken: async (realmId) => getRealmToken(realmId), - }); - await chat.unlock('YOUR_PASSCODE'); - chat.setKeyVersion(SIGNING_KEY_VERSION); - const conversationKeys = new Map>(); - - async function signingKeys(userId: string) { - const resp = await apiClient.chat.getUserPublicKeys(userId, { - publicKeyFields: [ - 'public_key_version', 'public_key', 'signing_public_key', 'identity_public_key_signature', - ], - }); - return resp.data.map((r: any) => ({ - userId, - publicKeyVersion: r.public_key_version, - publicKey: r.signing_public_key, - identityPublicKey: r.public_key, - identityPublicKeySignature: r.identity_public_key_signature, - })); - } + // chat has keys loaded and setIdentity called (see Getting Started) + chat.setCacheKeys(true); + chat.setSigningKeys(participantSigningKeys); // all participants, from the public-key routes const data = body?.data ?? {}; if (data.event_type === 'chat.received' || data.event_type === 'chat.sent') { const p = data.payload ?? {}; - const cid = p.conversation_id as string; if (p.conversation_key_change_event) { - conversationKeys.set( - cid, - chat.extractConversationKeys([p.conversation_key_change_event]).keys, - ); + // Verify the key change and retain its key in the cache + chat.decryptEvents([p.conversation_key_change_event]); + } + const ev = chat.decryptEvent(p.encoded_event); + if (ev.type === 'message') { + console.log(ev.senderId, ev.content.text); } - const ev = chat.decryptEvent( - p.encoded_event, - conversationKeys.get(cid) ?? {}, - await signingKeys(p.sender_id), - ); } ``` ```rust - // chat: ChatCore or Chat, already unlocked / keys imported + // chat has keys loaded and set_identity called (see Getting Started) + chat.set_cache_keys(true); + chat.set_signing_keys(participant_signing_keys); // all participants + if let Some(kc) = key_change.as_deref() { - let extracted = chat.extract_conversation_keys(&[kc]); - conv_keys.extend(extracted.keys); + // Verify the key change and retain its key in the cache + let _ = chat.decrypt_events(&[kc], &[]); } - // sender_signing_keys from GET /2/users/{sender_id}/public_keys - let event = chat.decrypt_event(&encoded_event, &conv_keys, &sender_signing_keys)?; + // Decrypt with the cached conversation key; verify against the stored signing keys + let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?; ``` ```go + // chat has keys loaded and SetIdentity called (see Getting Started) + chat.SetCacheKeys(true) + _ = chat.SetSigningKeys(participantSigningKeys) // all participants + if keyChange != "" { - extracted, _ := chat.ExtractConversationKeys([]string{keyChange}) - for v, k := range extracted.Keys { - convKeys[v] = k - } + // Verify the key change and retain its key in the cache + _, _ = chat.DecryptEvents([]string{keyChange}, nil) } - event, err := chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys) + // Decrypt with the cached conversation key; verify against the stored signing keys + event, err := chat.DecryptEvent(encodedEvent, nil, nil) if err == nil && event.Type == "Message" { fmt.Println(event.AsMessage().Text()) } @@ -205,24 +163,33 @@ JavaScript は camelCase のイベントタイプ(`message`)を使用しま ```csharp + // chat has keys loaded and SetIdentity called (see Getting Started) + chat.SetCacheKeys(true); + chat.SetSigningKeys(participantSigningKeys); // all participants + if (!string.IsNullOrEmpty(keyChangeB64)) { - var extracted = chat.ExtractConversationKeys(new[] { keyChangeB64 }); - foreach (var kv in extracted.Keys) - convKeys[kv.Key] = kv.Value; + // Verify the key change and retain its key in the cache + chat.DecryptEvents(new[] { keyChangeB64 }); } - var evt = chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys); + // Decrypt with the cached conversation key; verify against the stored signing keys + var evt = chat.DecryptEvent(encodedEvent); if (evt.GetProperty("type").GetString() == "Message") Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString()); ``` ```java + // chat has keys loaded and setIdentity called (see Getting Started) + chat.setCacheKeys(true); + chat.setSigningKeys(participantSigningKeys); // all participants + if (keyChangeB64 != null && !keyChangeB64.isEmpty()) { - var extracted = chat.extractConversationKeys(List.of(keyChangeB64)); - convKeys.putAll(extracted.keys); + // Verify the key change and retain its key in the cache + chat.decryptEvents(List.of(keyChangeB64), null); } - JsonNode evt = chat.decryptEvent(encodedEvent, convKeys, senderSigningKeys); + // Decrypt with the cached conversation key; verify against the stored signing keys + JsonNode evt = chat.decryptEvent(encodedEvent, (Map) null, null); if ("Message".equals(evt.path("type").asText())) { System.out.println(evt.path("content").path("text").asText()); } @@ -230,6 +197,8 @@ JavaScript は camelCase のイベントタイプ(`message`)を使用しま +自分で鍵マップを管理したい場合は、`extract_conversation_keys` が `conversation_key_change_event` から鍵を復号し、`decrypt_event` は(送信者の署名鍵とともに)それらを明示的な引数として受け取ります — 明示的な空でない引数は常にストアより優先されます。 + 履歴: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — [Getting Started](/xchat/getting-started#6-receive-and-decrypt) を参照。 --- @@ -257,6 +226,6 @@ JavaScript は camelCase のイベントタイプ(`message`)を使用しま ## プラクティス - プラットフォームの要件に従って Webhook 署名を検証する -- 会話鍵と送信者の公開鍵をキャッシュする -- 依存メッセージを復号する前に鍵変更 blob を適用する -- `event_uuid` で重複を排除する +- セッションストアを一度だけ設定する: 参加者全員に対して `set_signing_keys`、会話鍵には `set_cache_keys(true)` +- 依存するメッセージを復号する前に、鍵変更 blob を(`decrypt_events` 経由で)適用する +- `event_uuid` で配信を、署名済み `message_id` でメッセージを重複排除する diff --git a/ja/xchat/troubleshooting.mdx b/ja/xchat/troubleshooting.mdx index 783bba7ff..623a31841 100644 --- a/ja/xchat/troubleshooting.mdx +++ b/ja/xchat/troubleshooting.mdx @@ -1,7 +1,7 @@ --- title: トラブルシューティング sidebarTitle: トラブルシューティング -description: Chat XDK のエラー、セキュアな鍵バックアップからの復元、復号の失敗など、X Chat の暗号化に関するよくある問題を診断します。 +description: Chat XDK のエラー、安全な鍵バックアップからの復元、復号の失敗、署名付き送信ペイロードの構築など、X Chat の暗号化に関するよくある問題を診断します。 keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encryption"] --- @@ -62,55 +62,122 @@ Webhook、OAuth、HTTP ステータスコード、レート制限については -### 鍵がロードされていないため暗号化または復号に失敗する +### 鍵や identity が未設定のために暗号化または復号に失敗する -まず秘密鍵をロードし、次に X のレコードから公開鍵**バージョン**を設定します。 +まず秘密鍵をロードし、次に**セッション ID**を設定します — あなたのユーザー ID と X 上のレコードの `public_key_version` です。`encrypt_*` および `prepare_*` メソッドはこの ID で署名します;セッション ID が未設定(かつ呼び出しごとの明示的な上書きもない状態)でそれらを呼び出すとエラーになります。 ```python chat.unlock(passcode) # or: chat.import_keys(blob) - chat.set_key_version(signing_key_version) + chat.set_identity(my_user_id, signing_key_version) ``` ```typescript await chat.unlock(passcode); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity(myUserId, signingKeyVersion); ``` ```rust chat.import_keys(&blob)?; - chat.set_key_version(&signing_key_version); + chat.set_identity(&my_user_id, &signing_key_version); ``` ```go blob, _ := chatxdk.Base64ToBytes(privateKeysB64) _ = chat.ImportKeys(blob) - chat.SetKeyVersion(signingKeyVersion) + _ = chat.SetIdentity(myUserID, signingKeyVersion) ``` ```csharp chat.ImportKeys(blobBytes); - chat.SetKeyVersion(signingKeyVersion); + chat.SetIdentity(myUserId, signingKeyVersion); ``` ```java chat.importKeys(blobBytes); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity(myUserId, signingKeyVersion); ``` +### ローカルの公開鍵がアカウントの登録済み鍵と一致しない + +クライアントはしばしば、*「このデバイス上の鍵は、このアカウントに登録されている鍵のいずれかか?」* に答える必要があります — 復元またはインポート後に正しい `public_key_version` を採用するため、あるいはオンボーディングがすでに完了しているかを判断するためです。Chat XDK の `get_public_keys` の出力を API の `public_key` フィールドと**文字列として比較すると、同じ鍵であっても常に失敗します**。両者が異なるエンコーディングを使用しているためです: + +- **API** は登録時にアップロードされたとおりの鍵を保存して返します: DER (SPKI) エンコーディング — 固定のアルゴリズム識別子プレフィックスの後ろにある生の鍵 +- **Chat XDK** の `get_public_keys` は、そのプレフィックスなしの生の鍵だけを返します + +同じ鍵、2 つの表記です。比較するには、両方を base64 デコードし、API のバイト列が SDK のバイト列で**終わる**ことを確認します(両側が同じエンコーディングを持つ場合に備え、完全一致も一致として扱います): + + + + ```python + import base64 + + def same_key(local_b64: str, server_b64: str) -> bool: + local = base64.b64decode(local_b64) # chat.get_public_keys()["identity"] + server = base64.b64decode(server_b64) # API row's "public_key" + return local == server or (len(server) > len(local) and server.endswith(local)) + ``` + + + ```typescript + const sameKey = (localB64: string, serverB64: string): boolean => { + const local = Buffer.from(localB64, 'base64'); // chat.getPublicKeys().identity + const server = Buffer.from(serverB64, 'base64'); // API row's public_key + return local.equals(server) || + (server.length > local.length && server.subarray(server.length - local.length).equals(local)); + }; + ``` + + + ```rust + fn same_key(local: &[u8], server: &[u8]) -> bool { + local == server || (server.len() > local.len() && server.ends_with(local)) + } + ``` + + + ```go + func sameKey(local, server []byte) bool { + return bytes.Equal(local, server) || + (len(server) > len(local) && bytes.HasSuffix(server, local)) + } + ``` + + + ```csharp + static bool SameKey(byte[] local, byte[] server) => + local.SequenceEqual(server) || + (server.Length > local.Length && + server.AsSpan(server.Length - local.Length).SequenceEqual(local)); + ``` + + + ```java + static boolean sameKey(byte[] local, byte[] server) { + if (Arrays.equals(local, server)) return true; + if (server.length <= local.length) return false; + byte[] tail = Arrays.copyOfRange(server, server.length - local.length, server.length); + return Arrays.equals(tail, local); + } + ``` + + + +一致したら、その行の `public_key_version` を `set_identity` に採用してください。バージョンを比較するとき(たとえば最新の鍵を選ぶ場合)は、**数値として**比較してください — バージョンはミリ秒のタイムスタンプで文字列長が可変なので、辞書順比較では誤ったものが選ばれます。 + ### メッセージに対する会話鍵が欠落している -そのメッセージの `conversation_key_version` に対応する**未加工の**鍵を持っていません。 +`Message encrypted with key version '…' but no matching key found` のようなエラーは、そのメッセージの `conversation_key_version` に対応する**未加工の**鍵を持っていないことを意味します。 -1. `extract_conversation_keys` で `conversation_key_change_event`(ライブイベント)または `meta.conversation_key_events`(履歴)から鍵素材を復号する、**または**これらの blob を `decrypt_events` に含める +1. `extract_conversation_keys` で `conversation_key_change_event`(ライブイベント)または `meta.conversation_key_events`(履歴)から鍵素材を復号する、**または**これらの blob を `decrypt_events` に含める — `set_cache_keys(true)` が有効な場合、`decrypt_events` は各会話の最新の検証済み鍵も保持し、後続の `decrypt_event` と `encrypt_*` の呼び出しでは省略できます 2. そのバージョンの会話鍵が追加されていること、およびあなたがまだ参加者であることを確認する([Getting Started](/xchat/getting-started#4-set-up-conversation-keys) を参照) ### ピアが公開鍵を持っていない @@ -132,8 +199,10 @@ Webhook、OAuth、HTTP ステータスコード、レート制限については 検証は**デフォルトで fail-closed**(`reject_unverified = true`)です: SDK はすでに検証されていない署名付きイベントを拒否しているため、ここでの失敗はチェックをオンにする必要があるという意味ではなく、検証入力が間違っていることを意味します。一般的な原因: - **送信者**の署名鍵エントリが欠落または不完全(Chat XDK に必要なすべてのフィールド — [Chat XDK](/xchat/xchat-xdk) リファレンスを参照) +- 呼び出しに署名鍵が渡されておらず、`set_signing_keys` 経由で保存もされていない - 送信者がバージョンをローテーションした — 公開鍵を再取得する -- 受け入れフロアより下の鍵バージョンは検証されない +- 受け入れフロアより下の鍵バージョンは決して検証されない +- **グループの鍵変更イベント**で、署名者がすでにそのグループを離脱していると、その鍵はもはや配信されない — [離脱したメンバーによる鍵変更](/xchat/groups#離脱したメンバーによる鍵変更) を参照 `set_reject_unverified` セッターは、このデフォルトから**オプトアウト**する(`false`、推奨されません)ために存在します。以前に無効化した場合は、fail-closed のデフォルトを復元してください: @@ -170,6 +239,10 @@ Webhook、OAuth、HTTP ステータスコード、レート制限については +### 返信の `reply_preview_validation` が `"Invalid"` になる + +復号された返信には `reply_preview_validation`(`"Valid"` / `"Invalid"`;JavaScript は `'valid'` / `'invalid'` を使用)が付与される場合があります。`Invalid` は、メッセージ内の引用プレビューが埋め込まれた署名済みオリジナルイベントと一致しないことを意味します — 引用は信頼できないものとして扱い、引用コンテンツは検証済みのオリジナルからのみレンダリングしてください。メッセージ自体は別に検証されており依然として真正です;無効なプレビューによって例外がスローされることはありません。 + ### 古いイベントが恒久的に検証に失敗する **古い**イベントでの `signature missing or no matching signing key` や ECDSA の不一致のようなエラーは恒久的です。署名は不変であり、イベント自体から署名済みペイロードを再構築することで検証されるため、異なるバイトに対して署名された(またはそもそも署名されていない)イベントは、将来のすべてのロードで失敗します — 再試行、鍵の更新、または API 呼び出しでは治せません。これらのイベントは、再試行可能なエラーではなくトゥームストーンとして扱ってください。会話鍵をローテーションすると、その時点からクリーンで検証可能な履歴が始まります;新しいメッセージには影響しません。 @@ -184,8 +257,8 @@ Webhook、OAuth、HTTP ステータスコード、レート制限については |:------|:----| | 間違った鍵バイト | API からの暗号化された鍵文字列ではなく、**未加工の**会話鍵バイトを Chat XDK に渡す | | 間違った JSON フィールド名 | `encrypted_content` → `encoded_message_create_event`、`encoded_event_signature` → `encoded_message_event_signature` にマップする | -| メッセージ ID の欠落 | `message_id` を自分で生成し、同じ値をリクエストボディで送信する | -| バージョンの不一致 | `conversation_key_version` を使用する鍵と揃える;署名鍵バージョンを `set_key_version` / 公開鍵レコードと揃える | +| 間違ったメッセージ ID | 返却されたペイロードの `message_id` を送信する — SDK が生成し、署名付きイベントに埋め込むため、他の値はすべて失敗します。再試行時には同じ暗号化ペイロードを再利用して、ID が二度と生成されないようにする | +| バージョンの不一致 | `conversation_key_version` を使用する鍵と揃える;`set_identity` に渡す署名鍵バージョンを公開鍵レコードと揃える | | パス ID の形式 | URL パスには依然としてハイフン付きの会話 ID が必要(`:` → `-`)ですが、署名の場合、SDK は任意の形式を受け付けます: `A:B`、`A-B`(どちらの順序でも)、または受信者のユーザー ID だけ — すべては同じ署名バイトに正規化される | ### 状態を変更する呼び出しに対して API が 400 を返す @@ -210,5 +283,5 @@ Webhook、OAuth、HTTP ステータスコード、レート制限については - 会話 ID、イベント ID、鍵の**バージョン**のみをログに記録する - 平文、パスコード、秘密鍵、または完全な鍵 blob は**ログに記録しない** -- `set_key_version` が公開鍵レコードの `public_key_version` と一致することを確認する +- `set_identity` に渡した署名鍵バージョンが、公開鍵レコードの `public_key_version` と一致することを確認する - 履歴が不完全な場合、復号する前に鍵変更のメタデータがスキップされないよう、**すべての**イベントページをページングする diff --git a/ja/xchat/xchat-xdk.mdx b/ja/xchat/xchat-xdk.mdx index 321d26c9f..c26b2a1f1 100644 --- a/ja/xchat/xchat-xdk.mdx +++ b/ja/xchat/xchat-xdk.mdx @@ -1,11 +1,11 @@ --- title: Chat XDK リファレンス sidebarTitle: Chat XDK -description: X Chat 用に鍵管理、暗号化、復号、署名を処理する暗号化 SDK である Chat XDK のリファレンス。サポート対象言語で利用できます。 +description: サポートされる言語全体で X Chat の鍵管理、暗号化、復号、署名を処理する暗号化 SDK である Chat XDK のリファレンス。 keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] --- -**Chat XDK** は、X Chat の鍵管理、暗号化、復号、および署名を処理します。X の HTTP API は呼び出し**ません** — [Python](/xdks/python/overview) または [TypeScript](/xdks/typescript/overview) の **XDK**、あるいは HTTPS とユーザーアクセストークンと組み合わせて使用してください。 +**Chat XDK** は X Chat の鍵管理、暗号化、復号、署名を処理します。X の HTTP API を呼び出すことは**ありません** — [Python](/xdks/python/overview) または [TypeScript](/xdks/typescript/overview) **XDK**、あるいはユーザーアクセストークンを使った HTTPS と組み合わせてください。 アプリのウォークスルー: [Getting Started](/xchat/getting-started)。サンプルボット: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples)。 @@ -32,7 +32,7 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] [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 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] com.x chatxdk - 0.2.1 + 0.4.0 ``` @@ -70,31 +70,37 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] ## クイックスタート -バックログを復号し、鍵をキャッシュし、1 つのイベントを復号し、返信を暗号化します。[Getting Started](/xchat/getting-started) のように、送信ボディを [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) に接続します。 +鍵をロードし、identity を一度設定し、バックログを復号し、1 つのライブイベントを復号し、メッセージを暗号化します。[Getting Started](/xchat/getting-started) のように送信ボディを [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) に接続します。 + +スニペットは、最短の呼び出し形式のために 2 つの**オプションの**セッションストアを使用します: `set_signing_keys` は他の参加者の公開鍵を保持し([public-keys エンドポイント](/x-api/chat/get-user-public-keys)から取得)、復号呼び出しは呼び出しごとの引数なしで送信者を検証でき、`set_cache_keys(true)` は SDK に各会話の検証済み鍵を記憶させるので、暗号化呼び出しは会話 ID とテキストだけを必要とします。どちらかをスキップして代わりに呼び出しごとに同じ値を渡してください — 両方のスタイルは同じように検証します;[復号](#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 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] 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 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] 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 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] ## ライフサイクルと鍵 -SDK を構築し、秘密鍵を保存し(パスコード保護された安全な鍵バックアップまたはローカル鍵 blob)、**公開**鍵を Chat API に登録し、アンロックまたはインポート後に登録済みの**公開鍵バージョン**を設定します。安全な鍵バックアップは **Juicebox** で実装されており、関連する設定フィールドがその名前を持つのはこのためです。デバイス/アプリ ID ごとに `generate_keypairs` を 1 回呼び出します;登録ペイロードを公開鍵エンドポイントに投稿します。すべてのバインディングで安全な鍵バックアップには `setup` / `unlock`(および関連するパスコードヘルパー)を使用します。`export_keys` / `import_keys`(ボットとサーバー用の未加工鍵 blob 永続化)は、**ネイティブバインディングのみ** — Python、Go、.NET、JVM、Rust — で利用できます。JS/WASM バインディングは未加工の鍵のエクスポートやインポートを公開しません: ブラウザではインスタンスに到達するスクリプトは identity を流出できるため、JS は鍵を安全な鍵バックアップ内に保持します。リクエストごとのバックアップ realm ラウンドトリップを避けたい JS サーバーは、リクエスト間で 1 つのアンロック済み `Chat` インスタンスを再利用するか、鍵 blob がサポートされているネイティブバインディングを実行するべきです。 +SDK を構築し、秘密鍵を保存し(パスコード保護された安全な鍵バックアップまたはローカル鍵 blob)、Chat API で**公開**鍵を登録し、unlock または import の後に **`set_identity(user_id, signing_key_version)`** を呼び出します — これは、すべての署名付きアクションがデフォルトで使用する送信者と署名鍵バージョンを設定するため、encrypt および prepare メソッドは呼び出しごとの identity 引数なしで動作します。デバイス/アプリの identity ごとに `generate_keypairs` を一度呼び出します;登録ペイロードを public-keys エンドポイントに POST します。すべてのバインディングで安全な鍵バックアップに `setup` / `unlock`(および関連するパスコードヘルパー)を使用します。`export_keys` / `import_keys`(ボットとサーバー向けの生の鍵 blob 永続化)は**ネイティブバインディングのみ**で利用可能です — Python、Go、.NET、JVM、Rust。JS/WASM バインディングは生の鍵のエクスポートやインポートを公開しません: ブラウザではインスタンスに到達するあらゆるスクリプトが identity を流出させる可能性があるため、JS は鍵を安全な鍵バックアップ内に保持します。リクエストごとのバックアップ realm ラウンドトリップを避けたい JS サーバーは、リクエスト間でアンロック済みの 1 つの `Chat` インスタンスを再利用するか、鍵 blob がサポートされているネイティブバインディングを実行してください。 + +SDK は、登録済み公開鍵について X API が報告するバージョンも必要とします。これにより、他のバージョンを対象とする鍵変更エントリはスキップされます。`set_identity` は、これをユーザー ID と一緒に記録します;`import_keys` はオプション引数として直接受け付けます(Rust と Go は `import_keys_with_version` / `ImportKeysWithVersion` を使用します)。 @@ -217,13 +272,13 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 }); 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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 // 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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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(); } @@ -295,29 +350,29 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 -安全な鍵バックアップ設定は 3 つの形を受け付けます: X API の `juicebox_config` オブジェクト(推奨 — そのまま渡す)、完全な `sdk_config` ラッパー、または裸の `token_map`。 +安全な鍵バックアップの設定は 3 つの形式を受け付けます: X API の `juicebox_config` オブジェクト(推奨 — そのまま渡します)、完全な `sdk_config` ラッパー、または裸の `token_map`。 -オプション: 署名検証は**デフォルトで有効**(`reject_unverified = true`)— 無効にするには `set_reject_unverified(false)` を呼び出します(推奨されません);バックアップ realm 設定が変更された場合は `update_config`;UI 状態には `is_unlocked` / `has_identity_key`。完全なフィールドリストは [chat-xdk リポジトリ](https://github.com/xdevplatform/chat-xdk) のスタブにあります。 +オプション: 署名検証はデフォルトで**オン**です(`reject_unverified = true`) — 無効にするには `set_reject_unverified(false)` を呼び出します(推奨されません);バックアップ realm 設定が変更された場合は `update_config`;UI の状態のために `is_unlocked` / `has_identity_key`。完全なフィールドリストは [chat-xdk リポジトリ](https://github.com/xdevplatform/chat-xdk) のスタブにあります。 --- ## 会話鍵 -3 つの **prepare** メソッドは、それぞれ 1 回の呼び出しで鍵変更に必要なすべて(新しい会話鍵の生成、渡された公開鍵からすべての参加者向けに暗号化、変更への署名)を行います。すべて同じ **`PreparedConversationChange`** 形を返し、POST の準備が整います — SDK フィールドの `encrypted_key` を `conversation_participant_keys` の中で **`encrypted_conversation_key`** に名前を変え、アクション署名を必須の **`action_signatures`** ボディフィールドにマップしてください。 +3 つの **prepare** メソッドは、鍵変更に必要なすべてを 1 回の呼び出しで実行します: 新しい会話鍵を生成し、(渡された公開鍵から)各参加者に対して暗号化し、変更に署名します。送信者 ID と署名鍵バージョンはセッション(`set_identity`)から取得されます;params 上で `sender_id` / `signing_key_version` を設定して上書きします。すべては同じ **`PreparedConversationChange`** 形状を返し、POST の準備ができています — `conversation_participant_keys` の SDK フィールド `encrypted_key` を **`encrypted_conversation_key`** にリネームし、アクション署名を必要な **`action_signatures`** ボディフィールドにマップします。 | シナリオ | メソッド | 返されるアクション署名 | |:---------|:-------|:---------------------------| | 1:1 を開始(会話 ID を省略 — SDK が導出)または任意の会話の鍵をローテーション(ID を渡す) | `prepare_conversation_key_change` | 1 | -| グループの作成(`POST /2/chat/conversations/group/initialize` によって発行される ID) | `prepare_group_create` | 2 — 両方送信 | -| グループへのメンバー追加 | `prepare_group_members_change` | 2 — 両方送信 | +| グループを作成(`POST /2/chat/conversations/group/initialize` で発行された ID) | `prepare_group_create` | 2 — 両方を送信 | +| グループにメンバーを追加 | `prepare_group_members_change` | 2 — 両方を送信 | -**未加工の**鍵バイトを `encrypt_message` およびメディア用に保持してください;API の暗号化エンベロープを encrypt に渡してはいけません。 +`encrypt_message` およびメディア用に**未加工の**鍵バイトを保持してください;API の暗号化エンベロープを暗号化に渡さないでください。 -**ラップする前に取得した鍵を検証してください。** prepare メソッドは、渡された任意の公開鍵に対して新しい会話鍵を暗号化します。渡す前に、各取得済みレコードに対して `verify_key_binding(identity, signing, signature)` を呼び出してください — public-keys API のレコードの `public_key`、`signing_public_key`、`identity_public_key_signature` フィールド — 置き換えられた identity 鍵が会話鍵を受け取れないようにするためです。 +**ラップする前に取得した鍵を検証してください。** prepare メソッドは、渡された任意の公開鍵に対して新しい会話鍵を暗号化します。渡す前に、各取得済みレコードに対して `verify_key_binding(identity, signing, signature)` を呼び出します — public-keys API のレコードの `public_key`、`signing_public_key`、`identity_public_key_signature` フィールドを渡します — 置き換えられた identity 鍵が会話鍵を受け取れないようにします。 -鍵変更イベントペイロードで `extract_conversation_keys` を使い、`{ keys, latest_version }` を再構築します。`decrypt_conversation_key` は単一の ECIES blob をアンラップします。 +鍵変更イベントペイロードに対して `extract_conversation_keys` を使い、`{ keys, latest_version }` を再構築します。`decrypt_conversation_key` は 1 つの ECIES blob をアンラップします。 @@ -327,7 +382,7 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 # {"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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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); @@ -404,15 +452,24 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 -グループの作成とメンバー追加については、各メソッドが必要とするパラメータを渡します(`prepare_group_create` にはメンバー/管理者 ID リスト;`prepare_group_members_change` には新規プラス現在のロスター) — サンプルは [Groups](/xchat/groups#create-the-group-and-establish-keys) を参照してください。両方とも**2 つ**のアクション署名を返します;POST には両方含める必要があります。 +グループの作成とメンバー追加については、各メソッドが必要とする params を渡します(`prepare_group_create` にはメンバー/管理者 ID リスト;`prepare_group_members_change` には新規プラス現在のロスター) — サンプルは [Groups](/xchat/groups#create-the-group-and-establish-keys) を参照。両方とも**2 つ**のアクション署名を返します;POST には両方を含める必要があります。 --- ## 復号 -**`decrypt_events`** は履歴とバックログ用です: ストリームから会話鍵を取り出し、復号済みメッセージを返し、バッチ全体を失敗させる代わりにイベントごとのエラーを**収集**します。**`decrypt_event`** は、鍵キャッシュをすでに持っている場合の単一のライブイベント用です。失敗時に raise/throw します。 +**`decrypt_events`** は履歴とバックログ用です: ストリームから会話鍵を取り出し、復号済みメッセージを返し、バッチ全体を失敗させる代わりにイベントごとのエラーを**収集**します。**`decrypt_event`** は 1 つのライブイベント用です;失敗時に raise/throw します。 + +SDK が送信者を検証できるように**署名鍵**を渡します。API の public-key フィールドを `SigningKeyEntry` にマップします: `public_key_version` → `public_key_version`(同じ名前)、`signing_public_key` → `public_key`、`public_key` → `identity_public_key`、加えて `identity_public_key_signature` と `user_id`。 + +2 つのオプトインセッションストアを使用すると、呼び出しごとの鍵引数を省略できます: -送信者を SDK が検証できるように**署名鍵**を渡します。API の公開鍵フィールドを `SigningKeyEntry` にマップします: `public_key_version` → `public_key_version`(同じ名前)、`signing_public_key` → `public_key`、`public_key` → `identity_public_key`、加えて `identity_public_key_signature` と `user_id`。検証はデフォルトで必須です: 空の署名鍵リストを省略または渡してもスキップされ**ません** — 署名付きイベントは失敗します(`decrypt_events` では `errors` に収集され、`decrypt_event` ではスローされます)。実際に検証をスキップするには、まず `set_reject_unverified(false)` を呼び出す必要があります(本番環境では推奨されません)。 +- **`set_signing_keys(entries)`** は参加者の署名鍵を保存します;署名鍵引数を省略(または空を渡す)した復号呼び出しは、代わりにストアを使用します。検証自体は変わりません — 鍵は復号中のイベントからではなく、この呼び出しを通してのみストアに入ります。各呼び出しは前のセットを置き換えます。 +- **`set_cache_keys(true)`** は会話鍵キャッシュを有効にします(デフォルトではオフ)。有効な間、`decrypt_events` は、鍵変更が有効な署名を持っていた最新の鍵を会話ごとにキャッシュします;`decrypt_event` は会話鍵引数が省略されるとそこにフォールバックし、encrypt ヘルパーは省略された会話鍵をそこから解決します。無効にするとキャッシュはクリアされます。 + +明示的な空でない引数は常にストアより優先されます。明示的な呼び出しごとの引数はファーストクラスのままで — サーバーレスやマルチインスタンスデプロイでは正しい選択です。そこではリクエストが、ストアが空のフレッシュなインスタンスに着地することがあります。 + +検証はデフォルトで必須です: 署名鍵を省略してもそれをスキップしません。何も渡されず、何も保存されていない場合、署名付きイベントは失敗します(`decrypt_events` の場合は `errors` に収集され、`decrypt_event` の場合はスローされます)。実際に検証をスキップするには、最初に `set_reject_unverified(false)` を呼び出す必要があります(本番では推奨されません)。 @@ -430,11 +487,11 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ## 暗号化と送信ヘルパー -**`encrypt_message`** は、テキストメッセージ用の署名済み暗号文を構築します(オプションの entities、`media_hash_key` 経由の attachments、TTL、通知フラグ)。返されたペイロードを送信メッセージボディにマップします: `encrypted_content` → **`encoded_message_create_event`**、`encoded_event_signature` → **`encoded_message_event_signature`**、および **`message_id`**。 +**`encrypt_message(conversation_id, text)`** はテキストメッセージ用の署名付き暗号文を構築します;オプションで `entities`、`attachments`(`media_hash_key` 経由)、`should_notify`、`ttl_msec`。送信者 ID はセッション(`set_identity`)から、会話鍵はオプトインの鍵キャッシュ(`set_cache_keys`)から解決されます — または `sender_id` / `signing_key_version` と `conversation_key` + `conversation_key_version` を明示的に渡します。SDK は **`message_id`**(署名付きイベントに埋め込まれた UUID)を生成し、ペイロードで返します — 自分で生成しないでください;再試行時には同じペイロードを再利用して、ID が二度と生成されないようにしてください。ペイロードを送信メッセージボディにマップします: `message_id` → **`message_id`**、`encrypted_content` → **`encoded_message_create_event`**、`encoded_event_signature` → **`encoded_message_event_signature`**。 + +**返信はイベントベースです。** `encrypt_reply(conversation_id, text, reply_to_event)` は返信対象の base64 未加工イベントを取ります。SDK はそこから引用プレビュー(シーケンス ID、送信者、テキスト、エンティティ、添付)を導出し、署名済みオリジナルを送信メッセージに埋め込むので、受信者は引用を検証できます。オリジナルが返信より古い鍵バージョンで暗号化された場合は `reply_to_ckces` — 未加工の鍵変更イベント — を渡してください。オリジナルが**編集された**場合は、未加工の編集イベントを `reply_to_edit_event` として渡します: プレビューは現在メッセージが述べている内容を引用し(テキストとエンティティは編集から来ます)、編集はオリジナルとともに受信者がチェックできるように移動します。明示的な `reply_to_*` フィールドは、未加工のイベントをもう保持していない呼び出し元の上書きとして残ります。 -返信とリアクション(`sequence_id` は親をターゲットにします)には、**`encrypt_reply`**、**`encrypt_add_reaction`**、および **`encrypt_remove_reaction`** を使用します。**`encrypt` / `decrypt`** は、会話鍵の下での UTF-8 メタデータ(たとえば暗号化されたグループ名)用です — メッセージエンベロープ用ではありません。**`encrypt_stream` / `decrypt_stream`** は添付バイトを暗号化します;[Media](/xchat/media) を参照してください。低レベルの **`sign` / `verify` / `verify_key_binding`** は高度なフローをサポートします;会話鍵の変更、グループの作成、およびメンバーの追加は [prepare メソッド](#conversation-keys) によって署名されます。 +**リアクションもイベントベースです。** `encrypt_add_reaction(target_event, emoji)` と `encrypt_remove_reaction(...)` は、リアクション対象の未加工イベントから会話 ID とターゲットシーケンス ID を導出します;同じ params でリアクションを追加し、後で削除できます。未加工のイベントをもう保持していない場合のみ、`conversation_id` と `target_message_sequence_id` を明示的に設定してください。 + +受信側では、返信を引用する復号済みメッセージには **`reply_preview_validation`**(`"Valid"` / `"Invalid"`;JS バインディングは `'valid'` / `'invalid'` を使用)が付与されます: SDK は、埋め込まれたオリジナルの署名を、イベントに含まれる鍵ではなく、あなたの署名鍵に対して検証し、復号し、引用コンテンツと作者をそれと比較しました。プレビューが編集イベントを埋め込む場合、SDK は編集を同じ方法で検証し(同じ会話、同じ作者)、引用テキストを編集前のテキストではなく編集済みコンテンツと照合します。メッセージにプレビューがない、またはプレビューにオリジナルが埋め込まれていない場合、フィールドは存在しません。`Invalid` プレビューは信頼できないものとして扱ってください: メッセージ自体は真正ですが、引用素材はそうではありません — 引用は検証済みオリジナルからのみレンダリングしてください。 + +**`encrypt` / `decrypt`** は、会話鍵を使った UTF-8 メタデータ用です(たとえば暗号化されたグループ名) — メッセージエンベロープではありません。**`encrypt_stream` / `decrypt_stream`** は添付バイトを暗号化します;[Media](/xchat/media) を参照。低レベルの **`sign` / `verify` / `verify_key_binding`** は高度なフローをサポートします;会話鍵変更、グループ作成、メンバー追加は [prepare メソッド](#conversation-keys) が署名します。 `encrypt_message` / `encrypt_reply` に渡す会話 ID は、保持している任意の形式で構いません — イベントからの `A:B`、リストや URL パスからの `A-B`(どちらの順序でも)、または受信者のユーザー ID だけ — SDK は署名する前に正規化します。グループ ID(`g` プレフィックス付き)はそのまま渡されます。 @@ -508,22 +571,24 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ```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); @@ -611,7 +726,7 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ## メディアストリーム -テキストに使用されたものと**同じ**会話鍵でファイルバイトを暗号化し、Chat メディア API 経由でアップロードし、`encrypt_message` で **`media_hash_key`** を添付します。これは Posts メディアモデル(`expansions=attachments.media_keys`)ではありません。完全なアップロード/ダウンロードフロー: [Media](/xchat/media)。 +テキストで使用したのと**同じ**会話鍵でファイルバイトを暗号化し、Chat メディア API 経由でアップロードし、`encrypt_message` で **`media_hash_key`** を添付します。これは Posts メディアモデル(`expansions=attachments.media_keys`)ではありません。完全なアップロード/ダウンロードフロー: [Media](/xchat/media)。 @@ -661,10 +776,10 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ### 大きなメディア向けのインクリメンタルストリーミング -大きなファイルの場合、ペイロード全体をメモリに保持しないでください: `stream_encryptor()` / `stream_decryptor()` は、`StreamEncryptor` / `StreamDecryptor` を返します。`push(chunk)` でチャンク(約 1 MB ずつ)を供給し、最後に `finish()` を 1 回呼び出します。復号では、`finish()` は切り詰められたストリームを検出します(最終フレームより前に入力が終わった場合は失敗します)ので、成功するまでプッシュされた平文を完全なものとして扱わないでください。 +大きなファイルの場合、ペイロード全体をメモリに保持するのを避けます: `stream_encryptor()` / `stream_decryptor()` は `StreamEncryptor` / `StreamDecryptor` を返し、`push(chunk)` でチャンク(約 1 MB ずつ)を供給し、最後に `finish()` を 1 回呼び出します。復号時、`finish()` は切り詰められたストリームを検出します(入力が最終フレームより前に終了した場合失敗します)ので、成功するまでプッシュ済み平文を完全とみなさないでください。 -**JS/WASM のみ:** `finish()` は基になる WASM オブジェクトを消費して解放します — `finish()` の後には決して `free()` を呼び出さないでください(例外がスローされます)。`free()` は、finish の**前に**ストリームを放棄する場合(たとえばエラーパス)にのみ呼び出してください。 +**JS/WASM のみ:** `finish()` は基盤となる WASM オブジェクトを消費して解放します — `finish()` の後に決して `free()` を呼び出さないでください(スローします)。`free()` は、finish の**前**にストリームを放棄する場合(たとえばエラー経路)のみ呼び出してください。 @@ -701,7 +816,7 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 ## ユーティリティ -Base64/hex ヘルパー、MIME スニッフィング、および画像寸法は、モジュールレベルの関数(Python/JS/Rust/Go)または `ChatXdkUtilities`(C#/Java)として利用できます — 追加のライブラリを取り込まずに添付メタデータを構築するときに便利です。 +Base64/hex ヘルパー、MIME 検出、および画像寸法は、モジュールレベルの関数(Python/JS/Rust/Go)または `ChatXdkUtilities`(C#/Java)として利用できます — 追加のライブラリを引き込むことなく添付メタデータを構築するのに便利です。 @@ -792,13 +907,13 @@ Base64/hex ヘルパー、MIME スニッフィング、および画像寸法は ## 重要な型 -これらの概念的な型は、複数の言語にわたって現れます(正確なフィールド名は異なります;JS では `message` のように camelCase のイベント識別子を使うことが多いです): +これらの概念的な型は言語をまたがって出現します(正確なフィールド名は異なります;JS はしばしば `message` のような camelCase のイベント識別子を使います): -- **SendPayload** — `encrypt_message` および関連する暗号化ヘルパーの戻り値;Chat API 送信ボディにマップします。 +- **SendPayload** — `encrypt_message` およびその他の暗号化ヘルパーの戻り値: SDK 生成の **`message_id`**(署名付きイベントに埋め込まれた UUID — メッセージの `message_id` として送信し、重複排除のために保持)、`encrypted_content`、`encoded_event_signature`、署名メタデータ、`conversation_key_version`、および `should_notify`。Chat API 送信ボディにマップします。 - **PublicKeyRegistrationPayload** — add-public-key API 用の `generate_keypairs` / 公開鍵ゲッターの出力。 -- **SigningKeyEntry** — 署名検証のために decrypt に渡される送信者の公開素材。 -- **PreparedConversationChange** — 3 つの prepare メソッドの出力: 導出されたまたは渡された `conversation_id`、未加工の `conversation_key` バイト、`conversation_key_version`、`participant_keys`(`user_id`、`encrypted_key`、`public_key_version`)、および `action_signatures`(`message_id`、`encoded_message_event_detail`、`signature`、`signature_version`、`public_key_version`、オプションの `signature_payload` — そのペイロードが平文の鍵を埋め込むため鍵変更署名では省略されます)。 -- **DecryptEventsResult** — メッセージ、オプションのエラー、抽出された `conversation_keys`。 +- **SigningKeyEntry** — 署名検証のために decrypt に渡す、または `set_signing_keys` 経由で保存する送信者の公開素材。 +- **PreparedConversationChange** — 3 つの prepare メソッドの出力: 導出または渡された `conversation_id`、未加工の `conversation_key` バイト、`conversation_key_version`、`participant_keys`(`user_id`、`encrypted_key`、`public_key_version`)、および `action_signatures`(`message_id`、`encoded_message_event_detail`、`signature`、`signature_version`、`public_key_version`、オプションの `signature_payload` — 鍵変更署名では、そのペイロードが平文の鍵を埋め込んでいるため省略されます)。 +- **DecryptEventsResult** — メッセージ、オプションのエラー、および抽出された `conversation_keys`。返信を引用する復号済みメッセージには `reply_preview_validation` が付与されます([暗号化と送信ヘルパー](#encrypt-and-send-helpers) を参照)。 完全なフィールドリストについては、[chat-xdk リポジトリ](https://github.com/xdevplatform/chat-xdk) の言語スタブ(`docs/API.md`、`*.pyi`、`index.d.ts`)を使用してください。 @@ -806,9 +921,9 @@ Base64/hex ヘルパー、MIME スニッフィング、および画像寸法は ## エラー -Python は通常、記述的なメッセージ(たとえば無効なパスコード)で **`ValueError`** を発生させます。TypeScript/JavaScript は **`Error`** をスローします。Go は `(value, error)` を返します。履歴には **`decrypt_events`** を優先してください、そうすれば 1 つの不良イベントがバッチを中止しません;部分的な失敗については errors コレクションを検査してください。 +Python は通常、記述的なメッセージ付きの **`ValueError`** を発生させます(たとえば無効なパスコード)。TypeScript/JavaScript は **`Error`** をスローします。Go は `(value, error)` を返します。1 つの不正なイベントがバッチを中止しないよう、履歴には **`decrypt_events`** を優先してください;部分的な失敗については errors コレクションを検査します。 -一部の検証エラーは**恒久的**です。署名は不変であり、イベント自体から署名済みペイロードを再構築することで検証されるため、`signature missing or no matching signing key` または ECDSA の不一致で失敗する古いイベントは、将来のすべてのロードで失敗します — 再試行、鍵の更新、または API 呼び出しでは治せません。これらは一時的なエラーではなく、トゥームストーンとして扱ってください。会話鍵をローテーションすると、その時点からクリーンで検証可能な履歴が始まります。 +一部の検証エラーは**恒久的**です。署名は不変であり、イベント自体から署名済みペイロードを再構築することで検証されるため、`signature missing or no matching signing key` や ECDSA 不一致で失敗する古いイベントは、将来のすべてのロードで失敗します — 再試行、鍵の更新、または API 呼び出しでは治せません。これらはトゥームストーンとして扱い、一時的なエラーとしないでください。会話鍵をローテーションすると、その時点からクリーンで検証可能な履歴が始まります。 --- diff --git a/ko/xchat/cryptography-primer.mdx b/ko/xchat/cryptography-primer.mdx index e50a9fe48..415a38b41 100644 --- a/ko/xchat/cryptography-primer.mdx +++ b/ko/xchat/cryptography-primer.mdx @@ -1,25 +1,25 @@ --- title: 암호화 기초 sidebarTitle: 암호화 기초 -description: 구현 세부 사항 없이 X Chat의 종단 간 암호화를 뒷받침하는 ECDH, 공개 키 암호화, 디지털 서명 개념을 학습합니다. +description: 구현 세부 사항 없이 X Chat 종단 간 암호화의 배경이 되는 ECDH, 공개 키 암호화, 디지털 서명 개념을 학습하세요. keywords: ["X Chat cryptography", "E2EE primer", "encryption basics", "public key encryption", "ECDH", "digital signatures", "conversation keys"] --- import { Button } from '/snippets/button.mdx'; -이 기초 문서는 X Chat의 뒤에 있는 암호화 아이디어를 개념적 수준에서 설명합니다. 개발을 위해 이 정도의 깊이가 반드시 필요한 것은 아닙니다—[Chat XDK](/xchat/xchat-xdk)가 암호화, 복호화, 서명, 키 저장을 대신 수행합니다—하지만 앱을 설계하거나 동작을 디버그할 때 이러한 멘탈 모델이 도움이 됩니다. +이 기초 문서는 X Chat의 배경에 있는 암호화 개념을 개념적 수준에서 설명합니다. 개발을 위해 이 정도의 깊이가 반드시 필요한 것은 아닙니다—[Chat XDK](/xchat/xchat-xdk)가 암호화, 복호화, 서명, 키 저장을 대신 수행합니다—하지만 이러한 멘탈 모델은 앱을 설계하거나 동작을 디버그할 때 도움이 됩니다. -구현할 준비가 되면 전체 안내를 위해 [시작하기](/xchat/getting-started)를, 개별 경로를 위해 사이드바의 [API 참조](/x-api/chat/get-chat-conversations)를 사용하세요. +구현할 준비가 되면 전체 안내는 [시작하기](/xchat/getting-started)를 사용하고, 개별 경로에 대해서는 사이드바의 [API 참조](/x-api/chat/get-chat-conversations)를 참고하세요. -**이 암호화를 직접 구현하지 않습니다.** Chat XDK가 처리합니다. 이 페이지는 이해를 위한 것이며 API 체크리스트가 아닙니다. +**이 암호화를 직접 구현하지 않습니다.** Chat XDK가 처리합니다. 이 페이지는 이해를 돕기 위한 것이며 API 체크리스트가 아닙니다. --- ## 큰 그림 -X Chat은 계층화된 암호화 시스템을 사용하며, 다음과 같이 동작합니다: +X Chat은 계층화된 암호화 시스템을 사용하며 다음과 같이 동작합니다: 1. **메시지**는 **대화 키**로 암호화됩니다 (빠른 대칭 암호화) 2. **대화 키**는 각 참가자의 **신원 공개 키**로 암호화됩니다 (비대칭 키 교환) @@ -45,7 +45,7 @@ flowchart TB end ``` -제품 흐름상 X는 읽을 수 있는 메시지 내용이나 원시 대화 키가 아닌 **암호문과 키 봉투**를 전송합니다. 앱은 암호화에 Chat XDK를, 키 등록 및 이 암호화된 페이로드의 송수신에는 [Chat API](/xchat/introduction)(Python/TypeScript의 XDK 또는 HTTPS 사용)를 사용합니다. 이러한 구성 요소가 어떻게 조합되는지는 [시작하기](/xchat/getting-started)를 참조하세요. +제품 흐름상 X는 읽을 수 있는 메시지 내용이나 원시 대화 키가 아닌 **암호문과 키 봉투**를 전송합니다. 앱은 암호화에 Chat XDK를, 키 등록 및 이러한 암호화된 페이로드의 송수신에는 [Chat API](/xchat/introduction)(Python/TypeScript에서는 XDK, 그 외에는 HTTPS)를 사용합니다. 이러한 구성 요소가 어떻게 조합되는지는 [시작하기](/xchat/getting-started)를 참조하세요. --- @@ -62,20 +62,20 @@ X Chat은 각각 특정한 목적을 가진 세 가지 유형의 키 재료를 | **신원 공개 키** | 다른 사람과 공유되며, 대화 키를 사용자에게 *암호화*하는 데 사용됩니다 | | **신원 개인 키** | 비밀로 유지되며, 사용자에게 전송된 대화 키를 복호화하는 데 사용됩니다 | -누군가 사용자를 대화에 추가하면 사용자의 신원 공개 키를 사용하여 대화 키를 암호화합니다. 오직 사용자의 신원 개인 키만이 이를 복호화할 수 있습니다. +누군가가 사용자를 대화에 추가하면 사용자의 신원 공개 키를 사용해 대화 키를 암호화합니다. 오직 사용자의 신원 개인 키만이 이를 복호화할 수 있습니다. -공개 키는 플랫폼의 **공개 키** API를 통해 등록 및 조회됩니다 (API 참조의 암호화 키 참조). 개인 키는 Chat XDK 내부에 유지됩니다 (예: [보안 키 백업](#보안-키-백업-분산-키-저장) 또는 신중하게 보호된 키 blob). +공개 키는 플랫폼의 **공개 키** API를 통해 등록 및 조회됩니다 (API 참조의 암호화 키 항목 참조). 개인 키는 Chat XDK 내부에 유지됩니다 (예: [보안 키 백업](#보안-키-백업-분산-키-저장) 또는 신중하게 보호된 키 blob 사용). ### 2. 서명 키페어 -**용도:** 메시지를 사용자가 작성했음을 증명 +**용도:** 사용자가 메시지를 작성했음을 증명 | 구성 요소 | 설명 | |:----------|:------------| | **서명 공개 키** | 다른 사람과 공유되며, 사용자의 서명을 검증하는 데 사용됩니다 | | **서명 개인 키** | 비밀로 유지되며, 사용자의 메시지에 서명하는 데 사용됩니다 | -메시지를 보내면 사용자의 서명 개인 키로 서명됩니다. 수신자는 사용자의 서명 공개 키(공개 키 API를 통해서도 게시됨)를 사용하여 검증합니다. Chat XDK는 메시지를 암호화하는 과정에서 서명하며, 발신자의 공개 키 자료를 제공하면 복호화 시 검증할 수 있습니다. +메시지를 보내면 사용자의 서명 개인 키로 서명됩니다. 수신자는 (공개 키 API를 통해서도 게시되는) 사용자의 서명 공개 키로 검증합니다. Chat XDK는 메시지를 암호화하는 과정에서 서명하며, 발신자의 공개 키 자료를 제공하면 복호화 시 검증할 수 있습니다. ### 3. 대화 키 @@ -88,7 +88,7 @@ X Chat은 각각 특정한 목적을 가진 세 가지 유형의 키 재료를 | **참가자 간 공유** | 대화를 읽어야 하는 모든 참가자가 사본을 가짐 | | **버전 관리됨** | 키는 교체될 수 있으며, 앱은 시간 경과에 따라 버전을 추적해야 함 | -대화 키는 대화가 설정될 때 또는 키가 교체될 때 생성됩니다. 각 참가자는 자신의 신원 공개 키로 만들어진 **암호화된 사본**을 받습니다. 자신의 사본을 한 번 복호화하면 **원시** 대화 키를 보관해 두고 빠른 메시지 (및 [미디어](/xchat/media)) 암호화에 사용합니다. 대화의 이러한 사본 설정은 Chat XDK와 대화 **키** 엔드포인트를 함께 사용하여 수행되며, [시작하기](/xchat/getting-started#4-set-up-conversation-keys)에서 자세히 다룹니다. +대화 키는 대화가 설정될 때 또는 키가 교체될 때 생성됩니다. 각 참가자는 자신의 신원 공개 키로 만들어진 **암호화된 사본**을 받습니다. 자신의 사본을 한 번 복호화하면 **원시** 대화 키를 보관해 두고 빠른 메시지 (및 [미디어](/xchat/media)) 암호화에 사용합니다. 대화의 이러한 사본 설정은 Chat XDK와 대화 **키** 엔드포인트를 함께 사용해 수행되며, [시작하기](/xchat/getting-started#4-set-up-conversation-keys)에서 자세히 다룹니다. --- @@ -118,13 +118,13 @@ X Chat은 각각 특정한 목적을 가진 세 가지 유형의 키 재료를 - 앱은 [웹훅 또는 활동 스트림](/xchat/real-time-events)을 통해, 또는 이력을 위한 대화 **이벤트**를 읽어 X로부터 암호문을 수신합니다. + 앱은 [웹훅 또는 활동 스트림](/xchat/real-time-events)을 통해, 또는 이력을 위한 대화 **이벤트**를 읽어서 X로부터 암호문을 수신합니다. - 캐시된 원시 키를 사용하거나, 이것이 신규이거나 교체된 경우 키 배포(키 변경) 이벤트에서 사본을 복호화하여 획득합니다. + 캐시된 원시 키를 사용하거나, 신규이거나 교체된 경우 키 배포(키 변경) 이벤트에서 사본을 복호화해 획득합니다. - Chat XDK가 발신자의 서명 공개 키(및 관련 신원 바인딩)를 사용하여 서명을 확인하므로, 누가 보냈고 수정되지 않았음을 알 수 있습니다. + Chat XDK가 발신자의 서명 공개 키(및 관련 신원 바인딩)를 사용해 서명을 확인하므로, 누가 보냈고 수정되지 않았음을 알 수 있습니다. Chat XDK가 대화 키로 복호화합니다. 이제 "Hello, how are you?"를 읽을 수 있습니다. @@ -143,9 +143,9 @@ X Chat은 각각 특정한 목적을 가진 세 가지 유형의 키 재료를 메시지 전달을 위해 대화가 준비될 때: -1. 임의의 대화 키가 (Chat XDK 내에서) 생성됩니다 -2. **각 참가자**에 대해 해당 키는 참가자의 **신원 공개 키**로 암호화됩니다 -3. 이 암호화된 사본은 X의 Chat API를 통해 저장 및 전송됩니다 +1. Chat XDK가 임의의 대화 키를 생성합니다 +2. Chat XDK가 그 키를 **각 참가자의 신원 공개 키**로 암호화합니다 +3. 앱은 이러한 암호화된 사본을 X의 Chat API를 통해 게시합니다 4. 각 참가자는 (Chat XDK 내에서) 자신의 신원 개인 키로 **자신의** 사본을 복호화합니다 X는 원시 대화 키가 아닌 **래핑된** 사본만 처리합니다. @@ -166,7 +166,7 @@ X는 원시 대화 키가 아닌 **래핑된** 사본만 처리합니다. ## 보안 키 백업: 분산 키 저장 -**개인** 신원 및 서명 키는 신중하게 저장되어야 합니다. X Chat에는 **보안 키 백업** 시스템(Juicebox로 구현됨)이 포함되어 있어, 어떤 단일 서버에도 전체 비밀을 제공하지 않고도 여러 기기에서 패스코드로 키를 복구할 수 있습니다. +**개인** 신원 및 서명 키는 신중하게 저장되어야 합니다. X Chat에는 **보안 키 백업** 시스템이 포함되어 있어, 어떤 단일 서버에도 전체 비밀을 제공하지 않고도 여러 기기에서 패스코드로 키를 복구할 수 있습니다. ### 전통적인 키 저장의 문제 @@ -201,7 +201,7 @@ flowchart LR 단일 주체가 전체 비밀을 보유하지 않고도 복구 가능성(새 기기 + 패스코드)을 얻을 수 있습니다. -일반적인 경로에서는 키 백업 서버를 직접 구성하지 않습니다. Chat XDK에는 백업 클라이언트가 포함되어 있으며, realm 구성은 공개 키 레코드의 **`juicebox_config`**로 X API에서 반환됩니다 (이 필드 이름은 기반 구현인 Juicebox에서 따온 것입니다). 최초 패스코드 저장과 이후 잠금 해제는 Chat XDK 호출입니다—시작하기의 [기존 키로 초기화](/xchat/getting-started#2-initialize-the-chat-xdk-with-existing-keys) 및 [키 생성 및 등록](/xchat/getting-started#3-create-and-register-keys-first-time-setup)을 참조하세요. 일부 앱(특히 서버와 봇)은 보안 키 백업 대신 내보낸 키 blob을 사용합니다. 그 자료는 비밀번호처럼 보호하세요. +일반적인 경로에서는 키 백업 서버를 직접 구성하지 않습니다. Chat XDK에는 백업 클라이언트가 포함되어 있으며, realm 구성은 공개 키 레코드의 **`juicebox_config`** 필드로 X API에서 제공됩니다. 최초 패스코드 저장과 이후 잠금 해제는 Chat XDK 호출입니다—시작하기의 [기존 키로 초기화](/xchat/getting-started#2-initialize-the-chat-xdk-with-existing-keys)와 [키 생성 및 등록](/xchat/getting-started#3-create-and-register-keys-first-time-setup)을 참조하세요. 일부 앱(특히 서버와 봇)은 보안 키 백업 대신 내보낸 키 blob을 사용합니다. 그 자료는 비밀번호처럼 보호하세요. --- @@ -211,7 +211,7 @@ flowchart LR 모든 X Chat 메시지에는 다음을 지원하는 **디지털 서명**이 포함됩니다: 1. **진위성** — 발신자의 서명 개인 키로 생성되었음 -2. **무결성** — 서명 후에 암호화된 내용이 수정되지 않았음 +2. **무결성** — 서명 이후 암호화된 내용이 수정되지 않았음 ### 서명이 개념적으로 작동하는 방식 @@ -230,7 +230,7 @@ Chat XDK는 발신 메시지를 암호화할 때 서명하고, 수신 메시지 메시지만 서명되는 자료가 아닙니다. 대화 상태를 변경하는 모든 호출(대화 키 추가 또는 교체, 그룹 생성, 멤버 추가)에는 하나 이상의 **action signatures**가 포함되어야 합니다: 발신자는 변경 사항이 정확히 무엇을 하는지 설명하는 페이로드에 서명하고(키 변경의 경우 이 페이로드에는 새 대화 키 자체가 포함됨), API는 서명이 누락되거나 잘못된 형식이면 요청을 거부합니다. -서버는 평문 대화 키를 보유하지 않으므로 키 변경의 서명을 암호학적으로 검사할 수 없습니다. 대신 서명된, 인코딩된 변경 설명이 받은 요청과 일치하는지 검증합니다. **암호학적** 검사는 가장자리에서 발생합니다: 각 수신자의 Chat XDK가 키 변경 이벤트를 복호화할 때 발신자의 서명 공개 키에 대해 서명을 검증합니다. Chat XDK의 `prepare` 메서드는 이러한 서명을 자동으로 생성합니다—그룹 생성 및 멤버 추가는 **두 개**(키 변경과 그룹 액션)를 반환하며, 둘 다 전송되어야 합니다. +서버는 평문 대화 키를 보유하지 않으므로 키 변경의 서명을 암호학적으로 검사할 수 없습니다. 대신 서명된, 인코딩된 변경 설명이 받은 요청과 일치하는지 검증합니다. **암호학적** 검사는 가장자리에서 발생합니다: 각 수신자의 Chat XDK가 키 변경 이벤트를 복호화할 때 발신자의 서명 공개 키에 대해 서명을 검증합니다. Chat XDK의 `prepare` 메서드는 이러한 서명을 대신 생성합니다—그룹 생성과 멤버 추가는 **두 개**(키 변경과 그룹 액션)를 반환하며, 둘 다 전송되어야 합니다. 서명은 이벤트 내용에 바인딩되며 불변입니다: 서명이 검증되지 않는 이벤트는 나중에 유효해질 수 없습니다. 처리 방법은 [문제 해결](/xchat/troubleshooting)을 참조하세요. @@ -264,15 +264,15 @@ Chat XDK는 발신 메시지를 암호화할 때 서명하고, 수신 메시지 | 용어 | 정의 | |:-----|:-----------| | **대칭 암호화** | 동일한 키로 암호화 및 복호화 (메시지 및 미디어 스트림에 사용) | -| **비대칭 암호화** | 암호화와 복호화를 위한 서로 다른 키 (대화 키 래핑에 사용) | +| **비대칭 암호화** | 암호화와 복호화에 서로 다른 키 (대화 키 교환에 사용) | | **공개 키** | 공유해도 안전; 누군가에게 *암호화*하거나 그들의 서명을 검증하는 데 사용 | | **개인 키** | 비밀로 유지되어야 함; 복호화 또는 서명에 사용 | | **키페어** | 연결된 공개 키와 개인 키 | -| **ECDH / ECIES** | 대화 키를 신원 키로 래핑할 때 사용되는 알고리즘 | +| **ECDH / ECIES** | 신원 키를 통해 대화 키를 교환할 때 사용되는 알고리즘 | | **ECDSA** | 메시지 작성자 확인에 사용되는 서명 알고리즘 | | **P-256** | X Chat에서 사용되는 타원 곡선 (secp256r1) | -| **대화 키** | 하나의 대화 참가자가 공유하는 대칭 키 (시간 경과에 따라 버전 관리됨) | -| **비밀 공유** | 재구성하기 위해 여러 조각이 필요하도록 비밀을 분할하는 것 | +| **대화 키** | 하나의 대화에서 참가자가 공유하는 대칭 키 (시간 경과에 따라 버전 관리됨) | +| **비밀 공유** | 재구성하려면 여러 조각이 필요하도록 비밀을 분할하는 것 | | **Realm** | 키 자료의 한 조각을 보관하는 독립적인 보안 키 백업 서버 | --- diff --git a/ko/xchat/getting-started.mdx b/ko/xchat/getting-started.mdx index 4d504f255..702bee77e 100644 --- a/ko/xchat/getting-started.mdx +++ b/ko/xchat/getting-started.mdx @@ -45,11 +45,10 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: ```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] @@ -68,14 +67,14 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: dotnet add package XDevPlatform.ChatXdk ``` - 패키지는 자체 완결형입니다. macOS (arm64, x64), Linux (x64), Windows (x64)용 네이티브 라이브러리가 포함되어 있습니다. .NET 8+ 필요. + 패키지는 자체 완결형입니다: macOS (arm64, x64), Linux (x64), Windows (x64)용 네이티브 라이브러리가 내부에 포함되어 있습니다. .NET 8+ 필요. ```xml com.x chatxdk - 0.2.1 + 0.4.0 ``` @@ -136,9 +135,9 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 이 단계는 **이미 보유한 키를 로드**합니다—이 신원이 이전에 최초 설정을 완료한 경우 사용하세요: - **보안 키 백업:** 공개 키 레코드의 `juicebox_config`로 SDK를 구성한 다음, 패스코드로 `unlock`하여 개인 키를 복구합니다 (예: 새 기기에서). -- **키 blob:** 이전에 `export_keys`로 내보낸 blob으로 `import_keys`를 호출합니다. +- **키 blob:** 이전에 `export_keys`로 내보낸 blob으로 `import_keys`를 호출하고, 등록된 키 버전을 함께 전달합니다 (Rust와 Go에서는 이 변형을 `import_keys_with_version` / `ImportKeysWithVersion`이라고 부릅니다). -그런 다음 등록된 공개 키 버전(레코드의 `public_key_version`)을 설정합니다. +그런 다음 사용자 ID와 레코드의 `public_key_version`을 함께 **`set_identity(user_id, signing_key_version)`**를 한 번 호출합니다. 이는 세션 신원을 저장하므로, 이후의 모든 encrypt 및 prepare 호출은 이 신원으로 서명합니다. 따라서 호출마다 발신자 ID나 서명 키 버전을 전달할 필요가 없습니다. **처음 설정하시나요?** 동일한 방식으로 SDK를 구성하되 `unlock`/`import_keys`는 건너뛰고, [3단계](#3-키-생성-및-등록-최초-설정)로 계속 진행하여 키를 생성, 백업, 등록하세요. @@ -160,7 +159,9 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 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 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: getAuthToken: async (realmId) => getRealmTokenFromYourBackend(realmId), }); await chat.unlock('YOUR_PASSCODE'); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity('YOUR_USER_ID', signingKeyVersion); ``` @@ -189,11 +190,11 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 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 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 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 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 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 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: String signingKeyVersion = Optional.ofNullable(System.getenv("SIGNING_KEY_VERSION")).orElse("1"); try (Chat chat = new Chat()) { - chat.importKeys(Base64.getDecoder().decode(System.getenv("PRIVATE_KEYS_B64"))); - chat.setKeyVersion(signingKeyVersion); + chat.importKeys(Base64.getDecoder().decode(System.getenv("PRIVATE_KEYS_B64")), signingKeyVersion); + chat.setIdentity(myUserId, signingKeyVersion); } ``` @@ -254,8 +257,14 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: [2단계](#2-기존-키로-chat-xdk-초기화)에서 기존 키를 로드했다면 이 단계를 건너뛰세요. 그렇지 않은 경우, 새 신원에 대한 일회성 설정은 **세 가지 작업**을 수행합니다: 1. **키페어 생성** — `generate_keypairs`가 신원 및 서명 키페어를 생성합니다. -2. **공개 키 등록** — 다른 사람이 사용자에게 암호화하고 서명을 검증할 수 있도록 등록 페이로드를 공개 키 추가 엔드포인트에 POST합니다. -3. **개인 키 저장** — 패스코드로 `setup`하면 보안 키 백업에 기록됩니다(클라이언트). 또는 `export_keys`가 안전하게 저장할 키 blob을 반환합니다(서버 및 봇). +2. **개인 키 저장** — 패스코드로 `setup`하면 보안 키 백업에 기록됩니다(클라이언트). 또는 `export_keys`가 안전하게 저장할 키 blob을 반환합니다(서버 및 봇). +3. **공개 키 등록** — 다른 사람이 사용자에게 암호화하고 서명을 검증할 수 있도록 등록 페이로드를 공개 키 추가 엔드포인트에 POST합니다. + +마지막으로 등록의 키 버전으로 `set_identity`를 호출하여 이 세션이 새 신원으로 서명하도록 합니다. + + +모든 바인딩(Python, TypeScript, Go, Rust, C#, Java)에서 바로 실행할 수 있는 일회성 등록 스크립트는 [`chat-xdk/examples`](https://github.com/xdevplatform/chat-xdk/tree/main/examples)에 있습니다. 새 신원을 온보딩하기만 하면 되는 경우, 아래의 흐름을 직접 작성하는 대신 이 스크립트를 사용하세요. + @@ -280,7 +289,7 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: ), ) 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")) ``` @@ -300,7 +309,7 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: generate_version: registration.generateVersion, }); await chat.setup('YOUR_PASSCODE'); - chat.setKeyVersion(String(registration.version ?? signingKeyVersion)); + chat.setIdentity('YOUR_USER_ID', String(registration.version ?? '1')); ``` @@ -316,6 +325,8 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 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); ``` @@ -335,9 +346,15 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 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) + } ``` @@ -349,7 +366,7 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: $"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"); ``` @@ -367,7 +384,7 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 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"); ``` @@ -380,12 +397,12 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: ## 4. 대화 키 설정 -사용자 ID, 서명 키 버전, 그리고 모든 참가자의 신원 공개 키와 함께 **`prepare_conversation_key_change`**를 호출합니다. 한 번의 호출로 새로운 대화 키가 생성되고, 각 참가자에 대해 암호화되며, 변경 사항이 서명됩니다. 결과를 **대화 키 추가** 엔드포인트(`POST /2/chat/conversations/{id}/keys`)에 POST합니다—본문에는 `conversation_key_version`, `conversation_participant_keys`(SDK `encrypted_key` → API `encrypted_conversation_key`), 그리고 **`action_signatures`**(필수; 없으면 API가 호출을 거부함)가 필요합니다. 전송에 사용할 **원시** 대화 키를 보관하세요. +모든 참가자의 신원 공개 키와 함께 **`prepare_conversation_key_change`**를 호출하세요. 발신자 신원은 2단계에서 설정한 세션에서 가져옵니다. 한 번의 호출로 새 대화 키가 생성되고, 각 참가자에 대해 암호화되며, 변경 사항이 서명됩니다. 결과를 **대화 키 추가** 엔드포인트(`POST /2/chat/conversations/{id}/keys`)에 POST하세요—본문에는 `conversation_key_version`, `conversation_participant_keys`(SDK `encrypted_key` → API `encrypted_conversation_key`), 그리고 **`action_signatures`**(필수; 없으면 API가 호출을 거부함)가 필요합니다. 전송에 사용할 **원시** 대화 키를 보관하세요. -응답은 표준 대화 ID(`data.conversation_id`—1:1의 경우 하이픈으로 연결된 쌍, 또는 그룹의 경우 g- 접두사가 붙은 ID)와 키 변경의 `data.sequence_id`를 반환합니다. 클라이언트 측에서 재구성하는 대신 이후 요청에 반환된 ID를 사용하세요. 같은 호출은 나중에 키를 **교체**하기도 합니다: 기존 대화 ID를 `prepare_conversation_key_change`에 전달하고 최신 키 버전과 함께 POST합니다. 대화 키가 노출되었다고 의심될 때 교체하세요—교체는 **향후** 메시지만 보호합니다. 이전 키 버전으로 암호화된 메시지는 그 버전을 보유한 사람이라면 누구나 계속 읽을 수 있습니다. +응답은 표준 대화 ID(`data.conversation_id`—1:1의 경우 하이픈으로 연결된 쌍, 그룹의 경우 g- 접두사가 붙은 ID)와 키 변경의 `data.sequence_id`를 반환합니다. 클라이언트 측에서 재구성하는 대신 이후 요청에는 반환된 ID를 사용하세요. 같은 호출은 나중에 키를 **교체**하기도 합니다: 기존 대화 ID를 `prepare_conversation_key_change`에 전달하고 최신 키 버전으로 POST합니다. 대화 키가 노출된 것으로 의심될 때 교체하세요—교체는 **향후** 메시지만 보호합니다. 이전 키 버전으로 암호화된 메시지는 그 버전을 보유한 사람이라면 누구나 계속 읽을 수 있습니다. -**래핑하기 전에 가져온 키를 검증하세요.** `prepare_conversation_key_change`는 새 대화 키를 전달된 공개 키로 암호화합니다. 대체된 신원 키가 대화 키를 받지 못하도록, 각 가져온 레코드를 먼저 `verify_key_binding(identity, signing, signature)`로 확인하세요—공개 키 API의 `public_key`, `signing_public_key`, `identity_public_key_signature` 필드를 전달합니다. +**래핑하기 전에 가져온 키를 검증하세요.** `prepare_conversation_key_change`는 전달된 공개 키로 새 대화 키를 암호화합니다. 각 가져온 레코드를 먼저 `verify_key_binding(identity, signing, signature)`로 확인하세요—공개 키 API의 `public_key`, `signing_public_key`, `identity_public_key_signature` 필드를 전달하면 대체된 신원 키가 대화 키를 받는 것을 방지할 수 있습니다. @@ -398,8 +415,6 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 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 ) @@ -446,8 +461,6 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: // 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'), @@ -480,9 +493,9 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: ```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 @@ -532,8 +545,6 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: ```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}, @@ -572,21 +583,18 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 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 { @@ -625,12 +633,9 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: 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> parts = new ArrayList<>(); for (var pk : prepared.participantKeys) { @@ -673,36 +678,32 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: ## 5. 메시지 보내기 -**원시** 대화 키 바이트로 암호화합니다. 전송 요청에서 다음과 같이 매핑합니다: +4단계의 **원시** 대화 키로 암호화합니다. SDK가 메시지 ID(UUID)를 생성하여 서명된 이벤트에 삽입하고 페이로드에 반환합니다—직접 만들지 마세요. 전송 요청에서 다음과 같이 매핑합니다: | Chat XDK 필드 | 요청 본문 필드 | |:---------------|:-------------------| | `encrypted_content` / `encryptedContent` / `EncryptedContent` | `encoded_message_create_event` | | `encoded_event_signature` / `encodedEventSignature` / `EncodedEventSignature` | `encoded_message_event_signature` | -| 생성한 ID | `message_id` | +| 페이로드 `message_id` / `messageId` / `MessageId` | `message_id` | -API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 사용하세요(`:` → `-`). SDK 자체는 유연합니다: `encrypt_message`와 `encrypt_reply`는 보유하고 있는 어떤 형태의 ID든 허용합니다—이벤트의 `A:B`, 목록이나 URL 경로의 `A-B`(어떤 순서든), 또는 그저 수신자의 사용자 ID—그리고 서명 전에 정규화합니다. 그룹 ID(`g` 접두사)는 그대로 통과됩니다. +API가 요구할 때 URL 경로에는 **하이픈으로 연결된** 대화 ID를 사용하세요(`:` → `-`). SDK 자체는 유연합니다: `encrypt_message`와 `encrypt_reply`는 보유하고 있는 어떤 형태의 ID든 허용합니다—이벤트의 `A:B`, 목록이나 URL 경로의 `A-B`(어떤 순서든), 또는 그저 수신자의 사용자 ID—그리고 서명 전에 정규화합니다. 그룹 ID(`g` 접두사)는 그대로 통과됩니다. ```python - import uuid from xdk.chat.models import SendMessageRequest - message_id = str(uuid.uuid4()) + # Sender identity resolves from set_identity (step 2) payload = chat.encrypt_message( - message_id, - "YOUR_USER_ID", "CONVERSATION_ID", - conv_key, "Hello!", - conv_key_version, - signing_key_version, + conversation_key=conv_key, + conversation_key_version=conv_key_version, ) client.chat.send_message( "RECIPIENT_USER_ID", SendMessageRequest( - message_id=message_id, + message_id=payload.message_id, # SDK-generated, embedded in the signed event encoded_message_create_event=payload.encrypted_content, encoded_message_event_signature=payload.encoded_event_signature, ), @@ -711,20 +712,15 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 ```typescript - import { randomUUID } from 'crypto'; - - const messageId = randomUUID(); + // Sender identity resolves from setIdentity (step 2) const payload = chat.encryptMessage({ - messageId, - senderId: 'YOUR_USER_ID', conversationId: 'CONVERSATION_ID', - conversationKey: convKey, text: 'Hello!', + conversationKey: convKey, conversationKeyVersion: convKeyVersion, - signingKeyVersion, }); await client.chat.sendMessage('RECIPIENT_USER_ID', { - message_id: messageId, + message_id: payload.messageId, // SDK-generated, embedded in the signed event encoded_message_create_event: payload.encryptedContent, encoded_message_event_signature: payload.encodedEventSignature, }); @@ -734,18 +730,14 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 ```rust use chat_xdk_core::EncryptMessageParams; - let message_id = uuid::Uuid::new_v4().to_string(); - let payload = chat.encrypt_message(EncryptMessageParams::new( - &message_id, - &sender_id, - &conversation_id, - conv_key, - "Hello!", - &conv_key_version, - &signing_key_version, - ))?; + // Sender identity resolves from set_identity (step 2) + let payload = chat.encrypt_message( + EncryptMessageParams::new(&conversation_id, "Hello!") + .with_conversation_key(conv_key, &conv_key_version), + )?; let body = serde_json::json!({ - "message_id": message_id, + // SDK-generated, embedded in the signed event + "message_id": payload.message_id, "encoded_message_create_event": payload.encrypted_content, "encoded_message_event_signature": payload.encoded_event_signature, }); @@ -758,18 +750,19 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 ```go - messageID := uuid.NewString() + // Sender identity resolves from SetIdentity (step 2) payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{ - MessageID: messageID, - SenderID: senderID, ConversationID: conversationID, - ConversationKey: convKey, Text: "Hello!", + ConversationKey: convKey, ConversationKeyVersion: convKeyVersion, - SigningKeyVersion: signingKeyVersion, }) + if err != nil { + log.Fatal(err) + } body, _ := json.Marshal(map[string]string{ - "message_id": messageID, + // SDK-generated, embedded in the signed event + "message_id": payload.MessageID, "encoded_message_create_event": payload.EncryptedContent, "encoded_message_event_signature": payload.EncodedEventSignature, }) @@ -785,18 +778,14 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 ```csharp - var messageId = Guid.NewGuid().ToString(); - var payload = chat.EncryptMessage(new EncryptMessageParams { - MessageId = messageId, - SenderId = senderId, - ConversationId = conversationId, + // Sender identity resolves from SetIdentity (step 2) + var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hello!") { ConversationKey = convKey, - Text = "Hello!", ConversationKeyVersion = convKeyVersion, - SigningKeyVersion = signingKeyVersion, }); var sendJson = System.Text.Json.JsonSerializer.Serialize(new Dictionary { - ["message_id"] = messageId, + // SDK-generated, embedded in the signed event + ["message_id"] = payload.MessageId, ["encoded_message_create_event"] = payload.EncryptedContent, ["encoded_message_event_signature"] = payload.EncodedEventSignature, }); @@ -810,19 +799,16 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 ```java - EncryptMessageParams params = new EncryptMessageParams(); - params.messageId = UUID.randomUUID().toString(); - params.senderId = senderId; - params.conversationId = conversationId; + // Sender identity resolves from setIdentity (step 2) + EncryptMessageParams params = new EncryptMessageParams(conversationId, "Hello!"); params.conversationKey = convKey; - params.text = "Hello!"; params.conversationKeyVersion = convKeyVersion; - params.signingKeyVersion = signingKeyVersion; SendPayload payload = chat.encryptMessage(params); String pathId = conversationId.replace(':', '-'); String sendJson = new ObjectMapper().writeValueAsString(Map.of( - "message_id", params.messageId, + // SDK-generated, embedded in the signed event + "message_id", payload.messageId, "encoded_message_create_event", payload.encryptedContent, "encoded_message_event_signature", payload.encodedEventSignature)); HttpRequest req = HttpRequest.newBuilder() @@ -836,6 +822,10 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 + +위 스니펫은 방금 4단계에서 만든 대화 키를 명시적으로 전달합니다. 키 캐시가 켜져 있고 `decrypt_events` 처리 과정에서 해당 대화의 키가 검증되면([6단계](#6-수신-및-복호화)), `encrypt_message(conversation_id, text)`만으로도 충분합니다—SDK가 검증된 최신 키를 자동으로 채웁니다. 재시도 시에는 **동일한** 암호화 페이로드를 다시 보내야 ID가 두 번 생성되지 않습니다. + + --- ## 6. 수신 및 복호화 @@ -843,15 +833,15 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 실시간 트래픽에는 [웹훅 또는 활동 스트림](/xchat/real-time-events)을, 이력 조회에는 대화 **이벤트**의 페이지 처리를 사용하세요. - 실시간 페이로드 필드: `encoded_event`, 선택적 `conversation_key_change_event` -- 이력: `GET /2/chat/conversations/{id}/events` — 모든 이벤트에 대해 **`decrypt_events`**와 `meta.conversation_key_events`를 함께 사용하는 것이 좋음 -- 서명 검증을 위해 발신자의 공개 키를 복호화에 전달합니다 (API 필드를 `SigningKeyEntry`로 매핑; [Chat XDK](/xchat/xchat-xdk) 참조) -- JavaScript는 camelCase 이벤트 유형(`message`)을 사용합니다; 다른 언어는 `"Message"`와 JSON의 snake_case 필드를 사용합니다 +- 이력: `GET /2/chat/conversations/{id}/events` — 모든 이벤트에 대해 **`decrypt_events`**를 실행하고 `meta.conversation_key_events`를 함께 사용하는 것이 좋습니다 +- 복호화하려면 발신자의 **서명 키**가 필요합니다. 그래야 SDK가 각 메시지의 작성자를 검증할 수 있습니다. 이는 다른 참가자들의 *공개* 키입니다—4단계에서 사용한 동일한 공개 키 엔드포인트에서 가져와 필드를 `SigningKeyEntry`로 매핑하세요(아래 스니펫에 매핑이 포함되어 있습니다) +- 서명 키(및 `decrypt_event`의 경우 대화 키)를 매 호출마다 전달하거나, **또는** 두 개의 선택적 세션 저장소를 한 번 설정하고 짧은 호출 형식을 사용할 수 있습니다. 아래 스니펫은 저장소를 사용합니다: `set_signing_keys(entries)`는 참가자의 키를 보관하고, `set_cache_keys(true)`(기본값 꺼짐)는 각 대화의 **서명 검증된** 최신 키를 유지하여 이후 호출에서 키 인수를 생략할 수 있게 합니다. 두 방식 모두 동일하게 검증합니다 +- JavaScript는 camelCase 이벤트 유형(`message`)을 사용합니다. 다른 언어는 `"Message"`와 JSON의 snake_case 필드를 사용합니다 ```python - conversation_keys = {} # conversation_id -> { version: key_bytes } - + # Once per process: fill the signing-key store and enable the key cache def signing_keys_for(user_id: str) -> list[dict]: resp = client.chat.get_user_public_keys( user_id, @@ -870,25 +860,34 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 for r in resp.data ] + chat.set_signing_keys( + signing_keys_for("YOUR_USER_ID") + signing_keys_for("RECIPIENT_USER_ID") + ) + chat.set_cache_keys(True) + + # Initial load or pagination: batch decrypt. Conversation keys are + # extracted from the KeyChange events in the batch; per-event failures + # are collected in result["errors"], never raised. + result = chat.decrypt_events(all_events_b64) + for dm in result["messages"]: + event = dm["event"] + if event["type"] == "Message" and event["content"]["content_type"] == "Text": + print(event["sender_id"], event["content"]["text"], event["verified"]) + + # Live traffic: one event at a time def handle_payload(payload: dict): - cid = payload["conversation_id"] if payload.get("conversation_key_change_event"): - conversation_keys[cid] = chat.extract_conversation_keys( - [payload["conversation_key_change_event"]] - )["keys"] - event = chat.decrypt_event( - payload["encoded_event"], - conversation_keys.get(cid, {}), - signing_keys_for(payload["sender_id"]), - ) - if event.get("type") == "Message" and event.get("content", {}).get("content_type") == "Text": - print(event["sender_id"], event["content"]["text"], event.get("verified")) + # A rotation enters the key cache only after its signature + # verifies, which is what decrypt_events does + chat.decrypt_events([payload["conversation_key_change_event"]]) + event = chat.decrypt_event(payload["encoded_event"]) # raises on failure + if event["type"] == "Message" and event["content"]["content_type"] == "Text": + print(event["sender_id"], event["content"]["text"], event["verified"]) ``` ```typescript - const conversationKeys = new Map>(); - + // Once per process: fill the signing-key store and enable the key cache async function signingKeysFor(userId: string) { const resp = await client.chat.getUserPublicKeys(userId, { publicKeyFields: [ @@ -909,24 +908,33 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 })); } - async function handlePayload(payload: { - conversation_id: string; + chat.setSigningKeys([ + ...(await signingKeysFor('YOUR_USER_ID')), + ...(await signingKeysFor('RECIPIENT_USER_ID')), + ]); + chat.setCacheKeys(true); + + // Initial load or pagination: batch decrypt. Conversation keys are + // extracted from the KeyChange events in the batch; per-event failures + // are collected in result.errors, never thrown. + const result = chat.decryptEvents(allEventsB64); + for (const dm of result.messages) { + if (dm.event.type === 'message' && dm.event.content?.contentType === 'text') { + console.log(dm.event.senderId, dm.event.content.text, dm.event.verified); + } + } + + // Live traffic: one event at a time + function handlePayload(payload: { encoded_event: string; - sender_id: string; conversation_key_change_event?: string; }) { - const cid = payload.conversation_id; if (payload.conversation_key_change_event) { - conversationKeys.set( - cid, - chat.extractConversationKeys([payload.conversation_key_change_event]).keys, - ); + // A rotation enters the key cache only after its signature + // verifies, which is what decryptEvents does + chat.decryptEvents([payload.conversation_key_change_event]); } - const event = chat.decryptEvent( - payload.encoded_event, - conversationKeys.get(cid) ?? {}, - await signingKeysFor(payload.sender_id), - ); + const event = chat.decryptEvent(payload.encoded_event); // throws on failure if (event.type === 'message' && event.content?.contentType === 'text') { console.log(event.senderId, event.content.text, event.verified); } @@ -935,25 +943,48 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 ```rust - // Build Vec from GET /2/users/{id}/public_keys - // (public_key_version, public_key, signing_public_key, identity_public_key_signature) + // Once per instance: fill the signing-key store (Vec + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.set_signing_keys(participant_signing_keys); + chat.set_cache_keys(true); + + // Initial load: batch decrypt — per-event failures land in result.errors + let result = chat.decrypt_events(&all_events_b64, &[]); + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what decrypt_events does if let Some(kc) = key_change_b64.as_deref() { - let extracted = chat.extract_conversation_keys(&[kc]); - conv_keys.extend(extracted.keys); + chat.decrypt_events(&[kc], &[]); } - let event = chat.decrypt_event(&encoded_event, &conv_keys, &sender_signing_keys)?; + let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?; ``` ```go - if keyChange != "" { - extracted, _ := chat.ExtractConversationKeys([]string{keyChange}) - for v, k := range extracted.Keys { - convKeys[v] = k + // Once per instance: fill the signing-key store ([]SigningKeyEntry + // from GET /2/users/{id}/public_keys) and enable the key cache + if err := chat.SetSigningKeys(participantSigningKeys); err != nil { + log.Fatal(err) + } + chat.SetCacheKeys(true) + + // Initial load: batch decrypt — per-event failures land in result.Errors + result, err := chat.DecryptEvents(allEventsB64, nil) + if err != nil { + log.Fatal(err) + } + for _, dm := range result.Messages { + if dm.Event.Type == "Message" { + fmt.Println(dm.Event.AsMessage().Text()) } } - event, err := chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys) + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what DecryptEvents does + if keyChange != "" { + chat.DecryptEvents([]string{keyChange}, nil) + } + event, err := chat.DecryptEvent(encodedEvent, nil, nil) if err == nil && event.Type == "Message" { fmt.Println(event.AsMessage().Text()) } @@ -961,24 +992,49 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 ```csharp - if (!string.IsNullOrEmpty(keyChangeB64)) + // Once per instance: fill the signing-key store (SigningKeyEntry list + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.SetSigningKeys(participantSigningKeys); + chat.SetCacheKeys(true); + + // Initial load: batch decrypt — per-event failures land in result.Errors + var result = chat.DecryptEvents(allEventsB64); + foreach (var dm in result.Messages) { - var extracted = chat.ExtractConversationKeys(new[] { keyChangeB64 }); - foreach (var kv in extracted.Keys) - convKeys[kv.Key] = kv.Value; + if (dm.Event.GetProperty("type").GetString() == "Message") + Console.WriteLine(dm.Event.GetProperty("content").GetProperty("text").GetString()); } - var evt = chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys); + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what DecryptEvents does + if (!string.IsNullOrEmpty(keyChangeB64)) + chat.DecryptEvents(new[] { keyChangeB64 }); + var evt = chat.DecryptEvent(encodedEvent); // throws on failure if (evt.GetProperty("type").GetString() == "Message") Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString()); ``` ```java + // Once per instance: fill the signing-key store (SigningKeyEntry list + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.setSigningKeys(participantSigningKeys); + chat.setCacheKeys(true); + + // Initial load: batch decrypt — per-event failures land in result.errors + DecryptEventsResult result = chat.decryptEvents(allEventsB64, null); + for (DecryptedMessage dm : result.messages) { + if ("Message".equals(dm.event.path("type").asText())) { + System.out.println(dm.event.path("content").path("text").asText()); + } + } + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what decryptEvents does if (keyChangeB64 != null && !keyChangeB64.isEmpty()) { - var extracted = chat.extractConversationKeys(List.of(keyChangeB64)); - convKeys.putAll(extracted.keys); + chat.decryptEvents(List.of(keyChangeB64), null); } - JsonNode evt = chat.decryptEvent(encodedEvent, convKeys, senderSigningKeys); + JsonNode evt = chat.decryptEvent(encodedEvent, (Map) null, null); if ("Message".equals(evt.path("type").asText())) { System.out.println(evt.path("content").path("text").asText()); } @@ -986,33 +1042,15 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 + +**서버리스 또는 다중 인스턴스인가요?** 서명 키 저장소와 키 캐시는 SDK 인스턴스의 메모리에 존재합니다. 이러한 환경에 맞지 않는 경우—한 호출은 복호화하고 다른 호출은 전송하는 경우—키를 명시적으로 전달하세요: `decrypt_events(events, signing_keys)`, `decrypt_event(event_b64, conversation_keys, signing_keys)`, 그리고 encrypt 메서드의 `conversation_key`/`conversation_key_version` 재정의를 사용합니다. `decrypt_events`가 반환하는 `conversation_keys`를 직접 유지하고 다시 전달하세요. + + 모든 언어에 대한 전체 폴링-응답 봇 예제: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). --- ## 모범 사례 -- 원시 대화 키와 발신자 공개 키를 캐시하고, 서명 검증 실패 시 갱신하세요 +- 서명 키 저장소를 최신 상태로 유지하세요: 발신자가 새 키 버전을 등록하면 전체 참가자 세트로 `set_signing_keys`를 다시 호출하고, 서명 검증 실패 시 갱신하세요 - `event_uuid`로 실시간 전달을 중복 제거하세요 -- 페이지네이션이 완료될 때까지 이벤트 이력을 페이지 처리하여 키 변경 메타데이터를 놓치지 마세요 -- 프로덕션에서 패스코드, 개인 키, 메시지 평문을 로그에 남기지 마세요 -- 웹 앱에서는 OAuth 토큰(및 키 백업 realm 토큰 발급)을 서버에 유지하세요; 개인 키는 클라이언트 Chat XDK에만 보관하는 것이 좋습니다 - ---- - -## 다음 단계 - - - - 모든 언어 바인딩의 메서드 및 타입 - - - 암호화된 이미지 및 파일 첨부 - - - 다자간 대화 및 메타데이터 - - - 웹훅 및 활동 전달 - - diff --git a/ko/xchat/groups.mdx b/ko/xchat/groups.mdx index d47f18c6a..f4b8b2048 100644 --- a/ko/xchat/groups.mdx +++ b/ko/xchat/groups.mdx @@ -1,11 +1,11 @@ --- title: 그룹 대화 sidebarTitle: 그룹 -description: 공유 대화 키, 암호화된 제목, 멤버 관리, 서명된 메시지를 갖춘 다자간 X Chat 그룹 대화를 생성합니다. +description: 공유 대화 키, 암호화된 제목, 멤버 관리, 서명된 메시지를 갖춘 다자간 X Chat 그룹 대화를 생성하세요. keywords: ["X Chat groups", "group DM", "conversation keys", "group name encryption"] --- -그룹 채팅은 1:1 X Chat과 **동일한 암호화 모델**을 사용합니다: 멤버가 공유하는 하나의 **대화 키**를 각 멤버의 **신원 공개 키**로 래핑하고, Chat XDK가 메시지를 암호화하고 서명합니다. 달라지는 것은 **멤버십**, **대화를 생성하는 방식**, 그리고 종종 대화의 **암호화된 제목/아바타** 필드입니다. +그룹 채팅은 1:1 X Chat과 **동일한 암호화 모델**을 사용합니다: 멤버가 공유하는 하나의 **대화 키**를 각 멤버의 **신원 공개 키**로 래핑하고, Chat XDK가 메시지를 암호화하고 서명합니다. 달라지는 것은 **멤버십**, **대화를 생성하는 방식**, 그리고 흔히 대화의 **암호화된 제목/아바타** 필드입니다. 1:1 흐름은 [시작하기](/xchat/getting-started)에 있습니다. 엔드포인트 세부 사항은 **API 참조 → 대화 및 메시지** 아래에 있습니다. @@ -18,7 +18,7 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt | 식별 | 경로에서 종종 상대방 사용자 ID로 지정 | 대화 ID가 일반적으로 `g`로 시작 | | 생성 | 사용자에 대한 키 + 메시징 | 그룹 생성 / 초기화 API, 그런 다음 키 | | 참가자 | 나 + 상대방 한 명 | 여러 사용자; 멤버십이 변경 가능 | -| 메타데이터 | 최소 | 이름, 아바타 등은 **암호문**일 수 있음 (대화 키로 복호화) | +| 메타데이터 | 최소 | 이름, 아바타 등이 **암호문**일 수 있음 (대화 키로 복호화) | | 키 교체 | 덜 빈번 | 사람이 참여하거나 떠날 때 흔함 | 암호화는 여전히: 키와 페이로드에는 **Chat XDK**를, 그룹 생성, 참가자 키 래핑 게시, 메시지 전송, 이벤트 로드에는 **X API**를 사용합니다. @@ -27,19 +27,20 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ## 그룹 생성 및 키 설정 -1. `POST /2/chat/conversations/group/initialize`로 그룹 ID를 발급합니다 — 응답의 `data.conversation_id`가 이후 모든 곳에서 사용하는 g-접두사 ID입니다. +1. `POST /2/chat/conversations/group/initialize`로 그룹 ID를 발급받습니다 — 응답의 `data.conversation_id`가 이후 모든 곳에서 사용하는 g-접두사 ID입니다. 2. 각 멤버의 신원 공개 키와 `public_key_version`을 로드합니다 (**암호화 키** 아래 `GET` 공개 키 경로; [`GET /2/users/public_keys`](/x-api/users/get-public-keys-for-multiple-users)는 한 요청으로 여러 사용자를 가져옴). 사용하기 전에 `verify_key_binding`으로 각 레코드를 검증하세요 ([시작하기](/xchat/getting-started#4-set-up-conversation-keys)의 경고 참조). -3. **모든** 멤버(자신 포함), g-접두사 ID, 멤버/관리자 ID 목록으로 **`prepare_group_create`**를 한 번 실행합니다. 한 번의 호출로 대화 키를 생성하고, 모든 멤버에 대해 래핑하고, 생성에 서명합니다 — **두 개**의 action signature(대화 키 변경과 그룹 생성)를 반환합니다. +3. **모든** 멤버(자신 포함), g-접두사 ID, 멤버/관리자 ID 목록으로 **`prepare_group_create`**를 한 번 실행합니다. 한 번의 호출로 대화 키를 생성하고, 모든 멤버에게 래핑하고, `set_identity`의 세션 신원으로 생성에 서명합니다 — **두 개**의 action signature(대화 키 변경과 그룹 생성)를 반환합니다. 4. 그룹 멤버/관리자, `conversation_key_version`, `conversation_participant_keys`(SDK **`encrypted_key`** → API **`encrypted_conversation_key`**), 그리고 **두 개 모두**의 `action_signatures`와 함께 `POST /2/chat/conversations/group`을 호출합니다. 검증 실패는 안정적이고 사람이 읽을 수 있는 메시지로 반환됩니다. 예: `"Too many members: adding these members would exceed the allowed group size."` 또는 `"Cannot add all members: one or more of the requested members cannot be added to this conversation."`. 5. 암호화/복호화를 위해 **원시** 대화 키와 **버전**을 보관하세요. -`prepare_group_create`에 전달하는 `title`과 `avatar_url`은 서명되어 그룹 생성 이벤트에 그대로 임베드되며, 서버는 이를 요청과 대조합니다 — 그래서 POST 본문의 `group_name` / `group_avatar_url` 값은 SDK에 전달한 것과 **바이트 단위로 동일**해야 합니다. 그렇지 않으면 호출이 서명 검증에 실패합니다. +`prepare_group_create`는 사용자가 전달하는 `title`과 `avatar_url`에 서명하고 그룹 생성 이벤트에 그대로 임베드합니다. 서버는 이를 요청과 대조하므로, POST 본문의 `group_name` / `group_avatar_url` 값은 SDK에 전달한 것과 **바이트 단위로 동일**해야 합니다 — 그렇지 않으면 호출이 서명 검증에 실패합니다. ```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", ) @@ -50,8 +51,9 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ```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', }); @@ -60,9 +62,9 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ```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)?; @@ -71,8 +73,8 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ```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", }) @@ -83,23 +85,20 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ```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 ``` ```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 @@ -109,7 +108,7 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt 참가자 키와 action signature의 본문 매핑(`message_id`, `encoded_message_event_detail`, 중첩된 `message_event_signature`)은 [시작하기 — 대화 키](/xchat/getting-started#4-set-up-conversation-keys)의 키 POST와 동일합니다. -멤버십이 변경될 때, 새 멤버 ID와 함께 현재 로스터(멤버, 관리자, 대기 중인 멤버, 설정된 경우 현재 제목/아바타/TTL)를 사용하여 **`prepare_group_members_change`**를 호출합니다. 대화 키를 교체하고, 그룹 생성처럼 **두 개**의 action signature를 반환합니다 — 모두 **멤버 추가**(`POST /2/chat/conversations/{id}/members`)에 POST하세요. 그런 다음 **키 변경** 트래픽이 예상됩니다: 이를 [시작하기의 키 교체](/xchat/getting-started#6-receive-and-decrypt)처럼 처리하세요(`extract_conversation_keys` / `decrypt_events`, 그런 다음 최신 버전으로 암호화). +멤버십이 변경되면, 새 멤버 ID와 함께 현재 로스터(멤버, 관리자, 대기 중인 멤버, 설정된 경우 현재 제목/아바타/TTL)를 사용해 **`prepare_group_members_change`**를 호출하세요. 대화 키를 교체하며, 그룹 생성처럼 **두 개**의 action signature를 반환합니다 — 모두 **멤버 추가**(`POST /2/chat/conversations/{id}/members`)에 POST하세요. 그런 다음 **키 변경** 트래픽이 예상됩니다: 이를 [시작하기의 키 교체](/xchat/getting-started#6-receive-and-decrypt)처럼 처리하세요(`extract_conversation_keys` / `decrypt_events`, 그런 다음 최신 버전으로 암호화). `prepare_group_members_change`는 전달한 로스터에게만 래핑된 **새로운** 대화 키를 생성하므로, 새 멤버는 새 키 버전을 받고 이전 버전으로 전송된 메시지를 복호화할 수 없습니다. 반대로는 성립하지 않습니다: 교체는 **이전** 버전에 대한 접근을 결코 취소하지 않습니다 — 이미 이전 키를 보유한 사람은 누구나 그 키로 암호화된 메시지를 계속 읽을 수 있습니다. 대화 키가 노출되었다고 의심되면 `prepare_conversation_key_change`로 교체하세요. 이는 향후 메시지만 보호합니다. @@ -117,9 +116,9 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt ## 암호화된 그룹 메타데이터 -일부 대화 필드(예: 표시 **이름** 또는 **아바타 URL**)는 대화 키 아래 **암호화된** 상태로 도착할 수 있습니다. 이것은 `encrypt_message`가 **아닙니다**; 일반 Chat XDK의 **`encrypt` / `decrypt`** 쌍입니다 (UTF-8 문자열 입력, base64 암호문 출력, **원시** 대화 키 사용). +일부 대화 필드(예: 표시 **이름** 또는 **아바타 URL**)는 대화 키로 **암호화된** 상태로 도착할 수 있습니다. 이것은 `encrypt_message`가 **아닙니다**; 일반 Chat XDK의 **`encrypt` / `decrypt`** 쌍입니다 (UTF-8 문자열 입력, base64 암호문 출력, **원시** 대화 키 사용). -특정 필드가 암호화되어 저장되는지 여부는 그것을 쓰는 클라이언트가 결정합니다: `prepare_group_create`는 제공한 그대로 제목에 서명하고 전송합니다(대화 키는 그 호출이 생성하기 전까지 존재하지 않으므로, 생성 시점의 제목은 그것으로 암호화될 수 없습니다). 필드가 암호문인 대화를 읽을 때, 필드가 쓰여진 시점에 활성화되어 있던 키 버전과 `decrypt`로 복호화하세요. +특정 필드가 암호화되어 저장되는지 여부는 그것을 쓰는 클라이언트가 결정합니다: `prepare_group_create`는 제공한 그대로 제목에 서명하고 전송합니다(대화 키는 그 호출이 생성하기 전까지 존재하지 않으므로, 생성 시점의 제목은 그것으로 암호화될 수 없습니다). 필드가 암호문인 대화를 읽을 때는 필드가 쓰여진 시점에 활성화되어 있던 키 버전과 `decrypt`로 복호화하세요. @@ -166,7 +165,7 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt -해당 메타데이터에 적용되는 **현재** 대화 키 버전을 사용하세요. 키가 교체되었다면, 필드가 쓰여진 시점에 활성화되어 있던 버전으로 복호화하세요(또는 메타데이터가 항상 교체 시 재작성되는 경우 제품 규칙을 따르세요). +해당 메타데이터에 적용되는 **현재** 대화 키 버전을 사용하세요. 키가 교체되었다면 필드가 쓰여진 시점에 활성화되어 있던 버전으로 복호화하세요(또는 메타데이터가 교체 시 항상 재작성되는 경우 제품 규칙을 따르세요). --- @@ -180,13 +179,24 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt 멤버십 기반 교체 이후에는 항상 **최신** 키 버전으로 암호화하세요. +### 떠난 멤버로 인한 키 변경 + +그룹의 키 변경 이벤트는 이를 수행한 사람 — 종종 생성자나 관리자 — 이 서명합니다. 그 멤버가 이후에 **그룹을 떠나거나**(또는 계정을 비활성화하면), 공개 키 엔드포인트가 그들의 키 반환을 중단하므로 검증된 복호화 경로(서명 키를 사용한 `decrypt_events`)는 해당 키 변경 이벤트에서 `signature missing or no matching signing key`로 실패합니다. 이벤트가 손상된 것이 아니라, 검증 자료가 더 이상 제공되지 않을 뿐입니다. + +수명이 긴 그룹은 이를 예상하고, 검증할 수 없는 키 변경 이벤트에 대해서는 **`extract_conversation_keys`**로 폴백해야 합니다. 이 경로는 서명 검사를 건너뛰고, 당신의 신원 키로 복호화하여 대화 키를 복구합니다. 다음 이유로 보안 모델은 유지됩니다: + +- **당신의 신원 키로 암호화된** 키 자료만 복구할 수 있습니다 — 제3자가 당신이 읽을 수 있는 키를 주입할 수 없습니다 +- 모든 **메시지**는 여전히 각 발신자에 대해 서명 검증되므로, 메시지 작성자 정보에는 영향이 없습니다 + +검증된 경로를 우선하세요: `decrypt_events`(`set_cache_keys(true)`가 활성화된 경우 SDK의 키 캐시에도 공급됨)를 사용하고, 그것이 거부한 키 변경 이벤트에 대해서만 `extract_conversation_keys`를 사용하세요. + --- ## 체크리스트 -1. `POST /2/chat/conversations/group/initialize`로 g-접두사 ID를 발급합니다 -2. **모든** 멤버로 `prepare_group_create`를 호출; 참가자 키 래핑과 **두 개 모두**의 action signature를 `POST /2/chat/conversations/group`에 POST -3. 원시 키 + 버전을 캐시; 키 변경 이벤트 시 업데이트 -4. 멤버십 변경 시, `prepare_group_members_change`(두 개의 서명) → `POST /2/chat/conversations/{id}/members` -5. 필드가 암호문일 때 `decrypt`로 그룹 메타데이터를 복호화 -6. 1:1과 동일한 패턴으로 송수신 +1. `POST /2/chat/conversations/group/initialize`로 g-접두사 ID를 발급받습니다 +2. **모든** 멤버로 `prepare_group_create`를 호출; 참가자 키 래핑과 **두 개 모두**의 action signature를 `POST /2/chat/conversations/group`에 POST +3. 원시 키 + 버전을 캐시; 키 변경 이벤트 시 업데이트 +4. 멤버십 변경 시 `prepare_group_members_change`(두 개의 서명) → `POST /2/chat/conversations/{id}/members` +5. 필드가 암호문일 때 `decrypt`로 그룹 메타데이터를 복호화 +6. 1:1과 동일한 패턴으로 송수신 diff --git a/ko/xchat/media.mdx b/ko/xchat/media.mdx index 430d39b1a..a8a2f8d12 100644 --- a/ko/xchat/media.mdx +++ b/ko/xchat/media.mdx @@ -1,15 +1,15 @@ --- title: 미디어 및 첨부 파일 sidebarTitle: 미디어 -description: Chat XDK 스트림 암호화와 미디어 업로드 엔드포인트로 X Chat에서 이미지와 파일 첨부를 암호화, 업로드, 전송, 다운로드, 복호화합니다. +description: Chat XDK 스트림 암호화와 미디어 업로드 엔드포인트를 사용해 X Chat에서 이미지와 파일 첨부 파일을 암호화, 업로드, 전송, 다운로드, 복호화하세요. keywords: ["X Chat media", "encrypted images", "attachments", "encrypt_stream", "media upload"] --- -이미지 및 기타 파일은 텍스트와 **동일한 대화 키**를 사용합니다. Chat XDK로 바이트를 암호화하고(`encrypt_stream` / `decrypt_stream`), **`/2/chat/media/upload`** 경로(사이드바 **API 참조 → 미디어**)로 업로드한 다음, `encrypt_message`에 **`media_hash_key`**를 첨부합니다. +이미지와 기타 파일은 텍스트와 **동일한 대화 키**를 사용합니다. Chat XDK로 바이트를 암호화하고(`encrypt_stream` / `decrypt_stream`), **`/2/chat/media/upload`** 경로(사이드바 **API 참조 → 미디어**)로 업로드한 다음, `encrypt_message`에 **`media_hash_key`**를 첨부합니다. 업로드 시 DM 스코프와 함께 **`media.write`**를 포함하세요. 경로에는 하이픈으로 연결된 대화 ID를 사용하세요(`:` → `-`). MIME/치수는 **복호화된** 바이트에서 가져오는 것이 좋습니다. -이 경로는 Posts 미디어 모델(`expansions=attachments.media_keys`, `media.fields=variants` 등)이 **아닙니다**. 이러한 매개변수는 **Posts**에 적용됩니다; E2EE X Chat blob은 **`media_hash_key`**와 X Chat 미디어 다운로드로 지정됩니다. +이 경로는 Posts 미디어 모델(`expansions=attachments.media_keys`, `media.fields=variants` 등)이 **아닙니다**. 해당 매개변수는 **Posts**에 적용됩니다; E2EE X Chat blob은 **`media_hash_key`**와 X Chat 미디어 다운로드로 지정됩니다. ```mermaid flowchart LR @@ -104,7 +104,7 @@ flowchart LR -`encrypt_stream` / `decrypt_stream`은 전체 페이로드를 메모리에서 처리합니다. 큰 파일의 경우 `stream_encryptor()` / `stream_decryptor()`가 증분 객체(`StreamEncryptor` / `StreamDecryptor`)를 반환합니다: `push`로 청크를 공급한 다음 `finish`를 한 번 호출하세요—`finish`는 스트림이 잘렸으면 오류를 냅니다. +`encrypt_stream` / `decrypt_stream`은 전체 페이로드를 메모리에서 처리합니다. 큰 파일의 경우 `stream_encryptor()` / `stream_decryptor()`가 증분 객체(`StreamEncryptor` / `StreamDecryptor`)를 반환합니다: `push`로 청크를 공급한 다음 `finish`를 한 번 호출하세요—스트림이 잘렸으면 `finish`가 오류를 냅니다. --- @@ -122,23 +122,19 @@ flowchart LR ## 첨부 파일과 함께 전송 -미디어 첨부와 함께 암호화한 다음, 메시지 전송 본문을 POST합니다([시작하기](/xchat/getting-started#5-send-a-message)와 동일한 필드 매핑). +미디어 첨부와 함께 암호화한 다음 메시지 전송 본문을 POST하세요([시작하기](/xchat/getting-started#5-send-a-message)와 동일한 필드 매핑). SDK가 `message_id`를 생성해 페이로드에 반환합니다—그 값을 전송하고, 재시도 시에도 동일한 페이로드를 재사용하여 ID가 두 번 발급되지 않도록 하세요. ```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, @@ -151,7 +147,7 @@ flowchart LR 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, ), @@ -160,26 +156,23 @@ flowchart LR ```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, }); @@ -187,10 +180,23 @@ flowchart LR ```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, }); @@ -203,10 +209,12 @@ flowchart LR ```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, @@ -216,49 +224,51 @@ flowchart LR 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 ``` ```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 ``` ```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 ``` +대화 키 페어는 완전히 생략할 수도 있습니다: `set_cache_keys(true)`가 활성화되어 있으면, `encrypt_message`는 대화의 최신 검증된 키 변경에서 키와 버전을 자동으로 확인합니다([시작하기](/xchat/getting-started) 참조). + --- ## 다운로드 및 복호화 경로: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/download-chat-media). 응답 본문은 암호문입니다. 수신 메시지에서는 복호화된 첨부 파일 / `media_hashes`에서 `media_hash_key`를 읽습니다. -**이벤트의 키 버전으로 키를 선택하세요.** 각 복호화된 메시지 이벤트에는 콘텐츠가 암호화된 `keyVersion`(JS; 다른 바인딩은 `key_version`)이 포함됩니다. 최신이 아닌 **해당** 버전의 대화 키—`conversationKeys.keys[event.keyVersion]`—로 첨부 파일을 복호화하세요. 키 교체 이후(예: 멤버 추가)에는 최신 키가 오래된 메시지에 첨부된 미디어를 복호화할 수 없습니다. +**이벤트의 키 버전으로 키를 선택하세요.** 각 복호화된 메시지 이벤트에는 콘텐츠가 암호화된 `keyVersion`(JS; 다른 바인딩은 `key_version`)이 포함됩니다. 최신이 아닌 **해당** 버전의 대화 키—`conversationKeys.keys[event.keyVersion]`—로 첨부 파일을 복호화하세요. 키 교체 이후(예: 멤버 추가)에는 최신 키가 이전 메시지에 첨부된 미디어를 복호화할 수 없습니다. diff --git a/ko/xchat/real-time-events.mdx b/ko/xchat/real-time-events.mdx index 693b01a5c..98c0c4923 100644 --- a/ko/xchat/real-time-events.mdx +++ b/ko/xchat/real-time-events.mdx @@ -10,9 +10,9 @@ X는 페이로드에 **암호문**을 포함하여 **`chat.received`**, **`chat. |:------|:-----| | **X Activity API** | `GET /2/activity/stream`; `POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions` (작업별 OpenAPI 보안 참조) | | **웹훅** | 자체 HTTPS URL에서 종료하는 경우 선택적 `POST` / `GET` `/2/webhooks` 및 `PUT` / `DELETE` `/2/webhooks/{webhook_id}` 경로 | -| **Chat XDK** | `extract_conversation_keys`, `decrypt_event` / `decrypt_events` | +| **Chat XDK** | `decrypt_event` / `decrypt_events`, 그리고 `set_signing_keys` / `set_cache_keys` 세션 저장소 | -비공개 X Chat 이벤트 유형은 모니터링하는 사용자에 대한 승인이 필요합니다. 암호화된 X Chat 파일 첨부는 **`media_hash_key`**와 X Chat 미디어 다운로드를 사용합니다—Post API의 `expansions=attachments.media_keys` / `media.fields=variants`가 아닙니다. +비공개 X Chat 이벤트 유형은 모니터링하는 사용자에 대한 승인이 필요합니다. 암호화된 X Chat 파일 첨부 파일은 **`media_hash_key`**와 X Chat 미디어 다운로드를 사용합니다—Post API의 `expansions=attachments.media_keys` / `media.fields=variants`가 아닙니다. --- @@ -32,14 +32,14 @@ X는 페이로드에 **암호문**을 포함하여 **`chat.received`**, **`chat. **활동 구독:** 다음으로 지속적인 구독을 관리합니다: -- `POST /2/activity/subscriptions` — 생성 -- `GET /2/activity/subscriptions` — 목록 (페이지네이션됨) -- `PUT /2/activity/subscriptions/{subscription_id}` — 업데이트 -- `DELETE /2/activity/subscriptions/{subscription_id}` 또는 `DELETE /2/activity/subscriptions?ids=` — 삭제 +- `POST /2/activity/subscriptions` — 생성 +- `GET /2/activity/subscriptions` — 목록 (페이지네이션됨) +- `PUT /2/activity/subscriptions/{subscription_id}` — 업데이트 +- `DELETE /2/activity/subscriptions/{subscription_id}` 또는 `DELETE /2/activity/subscriptions?ids=` — 삭제 -요청 본문과 필요한 스코프는 각 경로의 OpenAPI 작업에 정의되어 있습니다. X Activity API (XAA) 구독을 생성하려면 모니터링하는 사용자에 대한 **사용자 컨텍스트 승인**(관련 스코프를 포함한 OAuth 2.0 사용자 컨텍스트, 예: 채팅 이벤트의 경우 `dm.read`)이 필요합니다. +요청 본문과 필요한 스코프는 각 경로의 OpenAPI 작업에 정의되어 있습니다. X Activity API (XAA) 구독을 생성하려면 모니터링하는 사용자의 활동에 대한 **사용자 컨텍스트 승인**(관련 스코프를 포함한 OAuth 2.0 사용자 컨텍스트, 예: 채팅 이벤트의 경우 `dm.read`)이 필요합니다. -**웹훅:** HTTPS 엔드포인트에서 이벤트를 종료하는 경우, `POST /2/webhooks`로 웹훅을 등록하고 CRC 챌린지를 통과한 다음 `webhook_id`를 참조하여 `POST /2/activity/subscriptions`로 활동 구독을 생성합니다 (OpenAPI의 Webhooks 및 Activity 작업 참조). Python/TypeScript XDK는 SDK 버전에 포함되어 있을 때 웹훅 및 활동에 대한 헬퍼를 노출할 수 있습니다. +**웹훅:** HTTPS 엔드포인트에서 이벤트를 종료하는 경우, `POST /2/webhooks`로 웹훅을 등록하고 CRC 챌린지를 통과한 다음 `webhook_id`를 참조하여 `POST /2/activity/subscriptions`로 활동 구독을 생성합니다 (OpenAPI의 Webhooks 및 Activity 작업 참조). Python/TypeScript XDK는 해당 SDK 버전에 포함된 경우 웹훅과 활동을 위한 헬퍼를 노출할 수 있습니다. @@ -86,118 +86,76 @@ X는 페이로드에 **암호문**을 포함하여 **`chat.received`**, **`chat. ## 3. Chat XDK로 복호화 -실시간 필드: **`payload.encoded_event`**, 선택적 **`payload.conversation_key_change_event`**. **`event_uuid`**로 중복을 제거하세요. +실시간 필드: **`payload.encoded_event`**, 선택적 **`payload.conversation_key_change_event`**. **`event_uuid`**로 전달 중복을 제거하고, 복호화된 이벤트에 포함된 **`message_id`**로 메시지 중복을 제거하세요—`message_id`는 서명된 콘텐츠의 일부인 반면, 시퀀스 ID는 백엔드가 할당하는 서명되지 않은 메타데이터입니다. + +아래 스니펫은 가장 짧은 핸들러를 위해 두 개의 **선택적** 세션 저장소를 사용합니다: `set_signing_keys`는 참가자의 공개 키(한 번만 [public-keys 엔드포인트](/x-api/chat/get-user-public-keys)에서 가져옴)를 보관하고, `set_cache_keys(true)`는 각 대화의 검증된 키를 유지하므로 `decrypt_event`는 이벤트만 있으면 됩니다. 페이로드가 `conversation_key_change_event`를 포함하는 경우 먼저 `decrypt_events`로 통과시키세요: 그러면 키 변경이 검증되고, 캐싱이 켜져 있으면 해당 키가 `decrypt_event` 호출을 위해 유지됩니다. 인스턴스 상태를 두지 않는 편을 선호하시나요? 대신 호출마다 키를 전달하세요—이 섹션 끝의 참고를 확인하세요. JavaScript는 camelCase 이벤트 유형(`message`)을 사용합니다; 다른 바인딩은 `"Message"`와 snake_case 필드를 사용합니다. ```python - from chat_xdk import Chat - - chat = Chat(JUICEBOX_CONFIG_JSON) - chat.unlock("YOUR_PASSCODE") - chat.set_key_version(SIGNING_KEY_VERSION) - conversation_keys = {} - - def signing_keys(user_id: str): - resp = api_client.chat.get_user_public_keys( - user_id, - public_key_fields=[ - "public_key_version", "public_key", "signing_public_key", "identity_public_key_signature", - ], - ) - return [ - { - "user_id": user_id, - "public_key_version": r["public_key_version"], - "public_key": r["signing_public_key"], - "identity_public_key": r["public_key"], - "identity_public_key_signature": r["identity_public_key_signature"], - } - for r in resp.data - ] + # chat has keys loaded and set_identity called (see Getting Started) + chat.set_cache_keys(True) + chat.set_signing_keys(participant_signing_keys) # all participants, from the public-key routes data = body.get("data") or {} if data.get("event_type") in ("chat.received", "chat.sent"): p = data.get("payload") or {} - cid = p.get("conversation_id") if p.get("conversation_key_change_event"): - conversation_keys[cid] = chat.extract_conversation_keys( - [p["conversation_key_change_event"]] - )["keys"] - ev = chat.decrypt_event( - p["encoded_event"], - conversation_keys.get(cid, {}), - signing_keys(p["sender_id"]), - ) + # Verify the key change and retain its key in the cache + chat.decrypt_events([p["conversation_key_change_event"]]) + ev = chat.decrypt_event(p["encoded_event"]) + if ev["type"] == "Message": + print(ev["sender_id"], ev["content"]["text"]) ``` ```typescript - import { createChat } from '@xdevplatform/chat-xdk'; - - const chat = await createChat({ - juiceboxConfig: JUICEBOX_CONFIG_JSON, - getAuthToken: async (realmId) => getRealmToken(realmId), - }); - await chat.unlock('YOUR_PASSCODE'); - chat.setKeyVersion(SIGNING_KEY_VERSION); - const conversationKeys = new Map>(); - - async function signingKeys(userId: string) { - const resp = await apiClient.chat.getUserPublicKeys(userId, { - publicKeyFields: [ - 'public_key_version', 'public_key', 'signing_public_key', 'identity_public_key_signature', - ], - }); - return resp.data.map((r: any) => ({ - userId, - publicKeyVersion: r.public_key_version, - publicKey: r.signing_public_key, - identityPublicKey: r.public_key, - identityPublicKeySignature: r.identity_public_key_signature, - })); - } + // chat has keys loaded and setIdentity called (see Getting Started) + chat.setCacheKeys(true); + chat.setSigningKeys(participantSigningKeys); // all participants, from the public-key routes const data = body?.data ?? {}; if (data.event_type === 'chat.received' || data.event_type === 'chat.sent') { const p = data.payload ?? {}; - const cid = p.conversation_id as string; if (p.conversation_key_change_event) { - conversationKeys.set( - cid, - chat.extractConversationKeys([p.conversation_key_change_event]).keys, - ); + // Verify the key change and retain its key in the cache + chat.decryptEvents([p.conversation_key_change_event]); + } + const ev = chat.decryptEvent(p.encoded_event); + if (ev.type === 'message') { + console.log(ev.senderId, ev.content.text); } - const ev = chat.decryptEvent( - p.encoded_event, - conversationKeys.get(cid) ?? {}, - await signingKeys(p.sender_id), - ); } ``` ```rust - // chat: ChatCore or Chat, already unlocked / keys imported + // chat has keys loaded and set_identity called (see Getting Started) + chat.set_cache_keys(true); + chat.set_signing_keys(participant_signing_keys); // all participants + if let Some(kc) = key_change.as_deref() { - let extracted = chat.extract_conversation_keys(&[kc]); - conv_keys.extend(extracted.keys); + // Verify the key change and retain its key in the cache + let _ = chat.decrypt_events(&[kc], &[]); } - // sender_signing_keys from GET /2/users/{sender_id}/public_keys - let event = chat.decrypt_event(&encoded_event, &conv_keys, &sender_signing_keys)?; + // Decrypt with the cached conversation key; verify against the stored signing keys + let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?; ``` ```go + // chat has keys loaded and SetIdentity called (see Getting Started) + chat.SetCacheKeys(true) + _ = chat.SetSigningKeys(participantSigningKeys) // all participants + if keyChange != "" { - extracted, _ := chat.ExtractConversationKeys([]string{keyChange}) - for v, k := range extracted.Keys { - convKeys[v] = k - } + // Verify the key change and retain its key in the cache + _, _ = chat.DecryptEvents([]string{keyChange}, nil) } - event, err := chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys) + // Decrypt with the cached conversation key; verify against the stored signing keys + event, err := chat.DecryptEvent(encodedEvent, nil, nil) if err == nil && event.Type == "Message" { fmt.Println(event.AsMessage().Text()) } @@ -205,24 +163,33 @@ JavaScript는 camelCase 이벤트 유형(`message`)을 사용합니다; 다른 ```csharp + // chat has keys loaded and SetIdentity called (see Getting Started) + chat.SetCacheKeys(true); + chat.SetSigningKeys(participantSigningKeys); // all participants + if (!string.IsNullOrEmpty(keyChangeB64)) { - var extracted = chat.ExtractConversationKeys(new[] { keyChangeB64 }); - foreach (var kv in extracted.Keys) - convKeys[kv.Key] = kv.Value; + // Verify the key change and retain its key in the cache + chat.DecryptEvents(new[] { keyChangeB64 }); } - var evt = chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys); + // Decrypt with the cached conversation key; verify against the stored signing keys + var evt = chat.DecryptEvent(encodedEvent); if (evt.GetProperty("type").GetString() == "Message") Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString()); ``` ```java + // chat has keys loaded and setIdentity called (see Getting Started) + chat.setCacheKeys(true); + chat.setSigningKeys(participantSigningKeys); // all participants + if (keyChangeB64 != null && !keyChangeB64.isEmpty()) { - var extracted = chat.extractConversationKeys(List.of(keyChangeB64)); - convKeys.putAll(extracted.keys); + // Verify the key change and retain its key in the cache + chat.decryptEvents(List.of(keyChangeB64), null); } - JsonNode evt = chat.decryptEvent(encodedEvent, convKeys, senderSigningKeys); + // Decrypt with the cached conversation key; verify against the stored signing keys + JsonNode evt = chat.decryptEvent(encodedEvent, (Map) null, null); if ("Message".equals(evt.path("type").asText())) { System.out.println(evt.path("content").path("text").asText()); } @@ -230,6 +197,8 @@ JavaScript는 camelCase 이벤트 유형(`message`)을 사용합니다; 다른 +키 맵을 직접 관리하려면 `extract_conversation_keys`가 `conversation_key_change_event`에서 키를 복호화하고, `decrypt_event`는 이를 (그리고 발신자의 서명 키를) 명시적 인자로 받아들입니다—명시적으로 전달된 비어 있지 않은 인자는 언제나 저장소보다 우선합니다. + 이력: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — [시작하기](/xchat/getting-started#6-receive-and-decrypt)를 참조하세요. --- @@ -257,6 +226,6 @@ JavaScript는 camelCase 이벤트 유형(`message`)을 사용합니다; 다른 ## 관행 - 플랫폼 요구 사항에 따라 웹훅 서명을 검증하세요 -- 대화 키와 발신자 공개 키를 캐시하세요 -- 의존 메시지를 복호화하기 전에 키 변경 blob을 적용하세요 -- `event_uuid`로 중복을 제거하세요 +- 세션 저장소를 한 번만 설정하세요: 모든 참가자에 대해 `set_signing_keys`, 대화 키에 대해 `set_cache_keys(true)` +- 의존 메시지를 복호화하기 전에 (`decrypt_events`를 통해) 키 변경 blob을 적용하세요 +- `event_uuid`로 전달 중복을 제거하고, 서명된 `message_id`로 메시지 중복을 제거하세요 diff --git a/ko/xchat/troubleshooting.mdx b/ko/xchat/troubleshooting.mdx index 49d7e280b..d159f10ae 100644 --- a/ko/xchat/troubleshooting.mdx +++ b/ko/xchat/troubleshooting.mdx @@ -1,17 +1,17 @@ --- title: 문제 해결 sidebarTitle: 문제 해결 -description: Chat XDK 오류, 안전한 키 백업 복구, 복호화 실패 등 X Chat 암호화와 관련된 흔한 문제를 진단합니다. +description: Chat XDK 오류, 안전한 키 백업 복구, 복호화 실패, 서명된 전송 페이로드 구축 등 흔한 X Chat 암호화 문제를 진단합니다. keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encryption"] --- -이 페이지는 **X Chat 암호화와 Chat XDK에 특화된** 문제—키, 보안 키 백업, 복호화/검증, 그리고 암호화된 전송 페이로드 구축—를 다룹니다. +이 페이지는 **X Chat 암호화와 Chat XDK에 특화된** 문제—키, 안전한 키 백업, 복호화/검증, 그리고 암호화된 전송 페이로드 구축—를 다룹니다. 웹훅, OAuth, HTTP 상태 코드 및 속도 제한은 일반 [X API](/x-api/introduction) 및 [인증](/fundamentals/authentication/overview) 문서를 사용하세요. --- -## 키 및 보안 키 백업 +## 키 및 안전한 키 백업 ### 잠금 해제 실패 (잘못된 패스코드) @@ -62,60 +62,127 @@ keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encr -### 키가 로드되지 않아 암호화 또는 복호화 실패 +### 키 또는 신원이 설정되지 않아 암호화 또는 복호화 실패 -먼저 개인 키를 로드한 다음, X의 레코드에서 공개 키 **버전**을 설정하세요. +먼저 개인 키를 로드한 다음 **세션 신원**—당신의 사용자 ID와 X의 레코드에 있는 `public_key_version`—을 설정하세요. `encrypt_*` 및 `prepare_*` 메서드는 이를 사용해 서명합니다; 세션 신원 없이 (그리고 호출별 명시적 오버라이드도 없이) 이들을 호출하는 것은 오류입니다. ```python chat.unlock(passcode) # or: chat.import_keys(blob) - chat.set_key_version(signing_key_version) + chat.set_identity(my_user_id, signing_key_version) ``` ```typescript await chat.unlock(passcode); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity(myUserId, signingKeyVersion); ``` ```rust chat.import_keys(&blob)?; - chat.set_key_version(&signing_key_version); + chat.set_identity(&my_user_id, &signing_key_version); ``` ```go blob, _ := chatxdk.Base64ToBytes(privateKeysB64) _ = chat.ImportKeys(blob) - chat.SetKeyVersion(signingKeyVersion) + _ = chat.SetIdentity(myUserID, signingKeyVersion) ``` ```csharp chat.ImportKeys(blobBytes); - chat.SetKeyVersion(signingKeyVersion); + chat.SetIdentity(myUserId, signingKeyVersion); ``` ```java chat.importKeys(blobBytes); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity(myUserId, signingKeyVersion); ``` +### 로컬 공개 키가 계정에 등록된 키와 절대 일치하지 않음 + +클라이언트는 종종 *"이 기기에 있는 키가 이 계정에 등록된 키 중 하나입니까?"* 라는 질문에 답해야 합니다 — 복원이나 임포트 후에 올바른 `public_key_version`을 채택하기 위해, 또는 온보딩이 이미 완료되었는지 결정하기 위해서입니다. Chat XDK의 `get_public_keys` 출력을 API의 `public_key` 필드와 **문자열로 비교하면 같은 키에 대해서도 항상 실패합니다**. 두 값이 서로 다른 인코딩을 사용하기 때문입니다: + +- **API**는 등록에서 업로드한 대로 키를 저장하고 반환합니다: DER (SPKI) 인코딩 — 고정된 알고리즘 식별자 접두사 뒤의 원시 키 +- **Chat XDK**의 `get_public_keys`는 그 접두사 없이 원시 키만 반환합니다 + +같은 키, 두 가지 표기입니다. 비교하려면 양쪽을 base64로 디코딩하고 API 바이트가 SDK 바이트로 **끝나는지** 확인하세요 (양쪽이 언젠가 같은 인코딩을 가지는 경우를 대비해 동일한 바이트도 일치로 간주합니다): + + + + ```python + import base64 + + def same_key(local_b64: str, server_b64: str) -> bool: + local = base64.b64decode(local_b64) # chat.get_public_keys()["identity"] + server = base64.b64decode(server_b64) # API row's "public_key" + return local == server or (len(server) > len(local) and server.endswith(local)) + ``` + + + ```typescript + const sameKey = (localB64: string, serverB64: string): boolean => { + const local = Buffer.from(localB64, 'base64'); // chat.getPublicKeys().identity + const server = Buffer.from(serverB64, 'base64'); // API row's public_key + return local.equals(server) || + (server.length > local.length && server.subarray(server.length - local.length).equals(local)); + }; + ``` + + + ```rust + fn same_key(local: &[u8], server: &[u8]) -> bool { + local == server || (server.len() > local.len() && server.ends_with(local)) + } + ``` + + + ```go + func sameKey(local, server []byte) bool { + return bytes.Equal(local, server) || + (len(server) > len(local) && bytes.HasSuffix(server, local)) + } + ``` + + + ```csharp + static bool SameKey(byte[] local, byte[] server) => + local.SequenceEqual(server) || + (server.Length > local.Length && + server.AsSpan(server.Length - local.Length).SequenceEqual(local)); + ``` + + + ```java + static boolean sameKey(byte[] local, byte[] server) { + if (Arrays.equals(local, server)) return true; + if (server.length <= local.length) return false; + byte[] tail = Arrays.copyOfRange(server, server.length - local.length, server.length); + return Arrays.equals(tail, local); + } + ``` + + + +일치하면 해당 행의 `public_key_version`을 `set_identity`에 채택하세요. 버전을 비교할 때(예: 최신 키를 선택하기 위해)는 **숫자로** 비교하세요 — 버전은 문자열 길이가 다양한 밀리초 타임스탬프이므로 사전식 비교는 잘못된 것을 선택합니다. + ### 메시지에 대한 대화 키 누락 -해당 메시지의 `conversation_key_version`에 대한 **원시** 키가 없습니다. +`Message encrypted with key version '…' but no matching key found`와 같은 오류는 해당 메시지의 `conversation_key_version`에 대한 **원시** 키가 없다는 뜻입니다. -1. `extract_conversation_keys`로 `conversation_key_change_event`(실시간 이벤트) 또는 `meta.conversation_key_events`(이력)의 키 자료를 복호화하거나, **또는** `decrypt_events`에 그 blob을 포함하세요 +1. `extract_conversation_keys`로 `conversation_key_change_event`(실시간 이벤트) 또는 `meta.conversation_key_events`(이력)의 키 자료를 복호화하거나, **또는** `decrypt_events`에 그 blob을 포함하세요—`set_cache_keys(true)`가 활성화된 상태에서는 `decrypt_events`가 각 대화의 최신 검증된 키도 유지하므로 이후의 `decrypt_event` 및 `encrypt_*` 호출에서는 이를 생략할 수 있습니다 2. 해당 버전에 대해 대화 키가 추가되었고 여전히 참가자인지 확인하세요 ([시작하기](/xchat/getting-started#4-set-up-conversation-keys) 참조) ### 상대방에게 공개 키가 없음 -아직 온보딩을 마치지 않았을 수 있습니다. 그들이 등록한 후, **API 참조 → 암호화 키**에서 `public_key`, `signing_public_key`, `identity_public_key_signature`, `public_key_version`을 로드하세요. +아직 온보딩을 마치지 않았을 수 있습니다. 그들이 등록한 후, **API 참조 → Encryption keys**에서 `public_key`, `signing_public_key`, `identity_public_key_signature`, `public_key_version`을 로드하세요. --- @@ -131,9 +198,11 @@ keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encr 검증은 **기본적으로 실패-폐쇄**입니다(`reject_unverified = true`): SDK는 이미 검증되지 않은 서명 이벤트를 거부하므로, 여기서 실패가 발생한다면 검사를 켜야 한다는 뜻이 아니라 검증 입력이 잘못되었다는 뜻입니다. 일반적인 원인: -- **발신자**에 대한 서명 키 항목이 누락되었거나 불완전함 (Chat XDK가 요구하는 모든 필드—[Chat XDK](/xchat/xchat-xdk) 참조 참조) +- **발신자**에 대한 서명 키 항목이 누락되었거나 불완전함 (Chat XDK가 요구하는 모든 필드—[Chat XDK](/xchat/xchat-xdk) 참조) +- 호출에서 서명 키가 전달되지 않았고 `set_signing_keys`로 저장된 것도 없음 - 발신자가 버전을 교체함—공개 키를 다시 가져오세요 - 허용된 최소값보다 낮은 키 버전은 절대 검증되지 않습니다 +- **그룹 키 변경 이벤트**에서 서명자가 이미 그룹을 떠났다면 그들의 키가 더 이상 제공되지 않습니다 — [떠난 멤버로 인한 키 변경](/xchat/groups#떠난-멤버로-인한-키-변경) 참조 `set_reject_unverified` 세터는 이 기본값을 **비활성화**(`false`, 권장하지 않음)하기 위해 존재합니다. 이전에 비활성화했다면 실패-폐쇄 기본값으로 복원하세요: @@ -170,6 +239,10 @@ keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encr +### 답장에 `reply_preview_validation: "Invalid"`가 포함됨 + +복호화된 답장은 `reply_preview_validation`(`"Valid"` / `"Invalid"`; JavaScript는 `'valid'` / `'invalid'`)을 포함할 수 있습니다. `Invalid`는 메시지 내부의 인용된 미리보기가 임베드된 서명 원본 이벤트와 일치하지 않는다는 뜻입니다—인용을 신뢰할 수 없는 것으로 취급하고 인용 내용은 검증된 원본에서만 렌더링하세요. 메시지 자체는 별도로 검증되며 여전히 진본입니다; 미리보기가 유효하지 않다고 해서 예외가 발생하지는 않습니다. + ### 오래된 이벤트가 영구적으로 검증에 실패함 **오래된** 이벤트에서 `signature missing or no matching signing key`나 ECDSA 불일치 같은 오류는 영구적입니다. 서명은 불변이며 이벤트 자체에서 서명된 페이로드를 재구축하여 검증되므로, 다른 바이트로 서명된 (또는 서명되지 않은) 이벤트는 이후 로드 시마다 실패합니다—재시도, 키 갱신, 또는 API 호출로도 치유할 수 없습니다. 이러한 이벤트를 재시도 가능한 오류가 아닌 툼스톤으로 취급하세요. 대화 키를 교체하면 그 시점 이후부터 깨끗하고 검증 가능한 이력이 시작됩니다; 새 메시지는 영향을 받지 않습니다. @@ -178,15 +251,15 @@ keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encr ## 전송 페이로드 구축 -이 실수들은 X Chat 암호화에 특화된 것입니다 (일반 HTTP 오류가 아님): +이러한 실수들은 X Chat 암호화에 특화된 것입니다 (일반 HTTP 오류가 아님): | 이슈 | 해결 | |:------|:----| | 잘못된 키 바이트 | API의 암호화된 키 문자열이 아니라 **원시** 대화 키 바이트를 Chat XDK에 전달하세요 | | 잘못된 JSON 필드 이름 | `encrypted_content` → `encoded_message_create_event` 및 `encoded_event_signature` → `encoded_message_event_signature`로 매핑하세요 | -| 메시지 ID 누락 | `message_id`를 직접 생성하고 요청 본문에 같은 값을 보내세요 | -| 버전 불일치 | `conversation_key_version`을 사용하는 키에 맞추고; 서명 키 버전을 `set_key_version` / 공개 키 레코드에 맞추세요 | -| 경로 ID 형식 | URL 경로에는 여전히 하이픈으로 연결된 대화 ID가 필요합니다(`:` → `-`), 하지만 서명 시 SDK는 어떤 형태든 허용합니다: `A:B`, `A-B`(둘 중 어떤 순서든), 또는 그저 수신자 사용자 ID—모두 동일한 서명된 바이트로 정규화됩니다 | +| 잘못된 메시지 ID | 반환된 페이로드의 `message_id`를 그대로 전송하세요—SDK가 이를 생성해 서명된 이벤트에 포함하므로, 다른 값을 쓰면 실패합니다. 재시도 시에는 동일한 암호화된 페이로드를 재사용하여 ID가 두 번 생성되지 않도록 하세요 | +| 버전 불일치 | `conversation_key_version`을 사용하는 키에 맞추고; `set_identity`에 전달하는 서명 키 버전을 공개 키 레코드와 일치시키세요 | +| 경로 ID 형식 | URL 경로에는 여전히 하이픈으로 연결된 대화 ID가 필요합니다(`:` → `-`), 하지만 서명 시 SDK는 어떤 형태든 허용합니다: `A:B`, `A-B`(둘 중 어떤 순서든), 또는 단순한 수신자 사용자 ID—모두 동일한 서명된 바이트로 정규화됩니다 | ### 상태 변경 호출에서 API가 400을 반환함 @@ -210,5 +283,5 @@ keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encr - 대화 ID, 이벤트 ID, 키 **버전**만 로그에 남기세요 - 평문, 패스코드, 개인 키, 또는 전체 키 blob을 로그에 **남기지 마세요** -- `set_key_version`이 공개 키 레코드의 `public_key_version`과 일치하는지 확인하세요 +- `set_identity`에 전달한 서명 키 버전이 공개 키 레코드의 `public_key_version`과 일치하는지 확인하세요 - 불완전한 이력의 경우, 복호화하기 전에 키 변경 메타데이터를 건너뛰지 않도록 **모든** 이벤트 페이지를 페이지 처리하세요 diff --git a/ko/xchat/xchat-xdk.mdx b/ko/xchat/xchat-xdk.mdx index 694e20fea..992faea36 100644 --- a/ko/xchat/xchat-xdk.mdx +++ b/ko/xchat/xchat-xdk.mdx @@ -5,7 +5,7 @@ description: 지원 언어 전반에서 X Chat의 키 관리, 암호화, 복호 keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] --- -**Chat XDK**는 X Chat을 위한 키 관리, 암호화, 복호화 및 서명을 처리합니다. X HTTP API를 호출하지 **않습니다**—[Python](/xdks/python/overview) 또는 [TypeScript](/xdks/typescript/overview) **XDK**와 함께 사용하거나, 사용자 액세스 토큰과 HTTPS와 함께 사용하세요. +**Chat XDK**는 X Chat을 위한 키 관리, 암호화, 복호화 및 서명을 처리합니다. X HTTP API를 호출하지 **않습니다**—[Python](/xdks/python/overview) 또는 [TypeScript](/xdks/typescript/overview) **XDK**와 함께 사용하거나, HTTPS와 사용자 액세스 토큰과 함께 사용하세요. 앱 안내: [시작하기](/xchat/getting-started). 샘플 봇: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). @@ -32,7 +32,7 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] [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] @@ -51,14 +51,14 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] dotnet add package XDevPlatform.ChatXdk ``` - 패키지는 자체 완결형입니다. macOS (arm64, x64), Linux (x64), Windows (x64)용 네이티브 라이브러리가 포함되어 있습니다. .NET 8+ 필요. + 패키지는 자체 완결형입니다: macOS (arm64, x64), Linux (x64), Windows (x64)용 네이티브 라이브러리가 내부에 포함되어 있습니다. .NET 8+ 필요. ```xml com.x chatxdk - 0.2.1 + 0.4.0 ``` @@ -70,31 +70,37 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] ## 빠른 시작 -백로그를 복호화하고, 키를 캐시하고, 이벤트 하나를 복호화하고, 회신을 암호화합니다. [시작하기](/xchat/getting-started)에서와 같이 전송 본문을 [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message)에 연결하세요. +키를 로드하고, 신원을 한 번 설정하고, 백로그를 복호화하고, 라이브 이벤트 하나를 복호화한 후, 메시지를 암호화합니다. [시작하기](/xchat/getting-started)에서와 같이 전송 본문을 [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message)에 연결하세요. + +스니펫은 **선택적** 세션 저장소 두 개를 사용하여 가장 짧은 호출 형식을 취합니다: `set_signing_keys`는 다른 참가자의 공개 키([공개 키 엔드포인트](/x-api/chat/get-user-public-keys)에서 가져옴)를 보관하여 복호화 호출이 호출별 인수 없이 발신자를 검증할 수 있게 하고, `set_cache_keys(true)`는 SDK가 각 대화의 검증된 키를 기억하도록 하여 암호화 호출에 대화 ID와 텍스트만 필요하도록 합니다. 둘 중 하나를 건너뛰고 호출마다 동일한 값을 전달할 수도 있습니다—두 방식 모두 동일하게 검증합니다. [복호화](#복호화)를 참고하세요. ```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 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] 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 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] 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 } ``` @@ -204,9 +257,11 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] --- -## 라이프사이클 및 키 +## 라이프사이클과 키 + +SDK를 구성하고, 개인 키를 저장하고(패스코드로 보호되는 보안 키 백업 또는 로컬 키 blob), Chat API에 **공개** 키를 등록한 다음, unlock이나 import 이후에 **`set_identity(user_id, signing_key_version)`**를 호출하세요—이는 모든 서명 작업이 기본적으로 사용할 발신자와 서명 키 버전을 설정하므로, encrypt 및 prepare 메서드가 호출별 신원 인수 없이도 동작합니다. `generate_keypairs`는 기기/앱 신원당 한 번 호출하세요. 등록 페이로드를 공개 키 엔드포인트에 게시하세요. 모든 바인딩에서 보안 키 백업에는 `setup` / `unlock`(및 관련 패스코드 헬퍼)을 사용하세요. `export_keys` / `import_keys`(봇과 서버를 위한 원시 키 blob 영속화)는 **네이티브 바인딩에서만** 사용 가능합니다—Python, Go, .NET, JVM, Rust. JS/WASM 바인딩은 원시 키 내보내기 또는 가져오기를 노출하지 않습니다: 브라우저에서 인스턴스에 접근할 수 있는 스크립트라면 무엇이든 신원을 유출할 수 있기 때문에, JS는 키를 보안 키 백업 안에 보관합니다. 요청당 백업 realm 왕복을 피하려는 JS 서버는 요청 간에 잠금 해제된 `Chat` 인스턴스 하나를 재사용하거나, 키 blob이 지원되는 네이티브 바인딩을 실행해야 합니다. -SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보안 키 백업 또는 로컬 키 blob), Chat API에 **공개** 키를 등록하고, 잠금 해제 또는 가져오기 후 등록된 **공개 키 버전**을 설정하세요. 보안 키 백업은 **Juicebox**로 구현되어 있으며, 관련 구성 필드가 그 이름을 갖는 것도 그 때문입니다. 기기/앱 신원당 한 번 `generate_keypairs`를 호출하고, 등록 페이로드를 공개 키 엔드포인트에 POST하세요. 모든 바인딩에서 보안 키 백업을 위해 `setup` / `unlock`(및 관련 패스코드 헬퍼)을 사용하세요. `export_keys` / `import_keys`(봇 및 서버용 원시 키 blob 지속)는 **네이티브 바인딩에서만** 사용 가능합니다—Python, Go, .NET, JVM, Rust. JS/WASM 바인딩은 원시 키 내보내기 또는 가져오기를 노출하지 않습니다: 브라우저에서 인스턴스에 접근하는 모든 스크립트가 신원을 유출할 수 있으므로, JS는 키를 보안 키 백업 내부에 유지합니다. 요청당 백업 realm 왕복을 피하고자 하는 JS 서버는 요청 전반에 걸쳐 잠금 해제된 하나의 `Chat` 인스턴스를 재사용하거나, 키 blob이 지원되는 네이티브 바인딩을 실행해야 합니다. +SDK는 또한 등록된 공개 키에 대해 X API가 보고하는 버전이 필요합니다. 그래야 다른 버전을 대상으로 하는 키 변경 항목이 건너뛰어집니다. `set_identity`는 이를 사용자 ID와 함께 기록합니다. `import_keys`는 이를 선택적 인수로 직접 받습니다(Rust와 Go는 `import_keys_with_version` / `ImportKeysWithVersion`을 사용). @@ -217,13 +272,13 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 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 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 }); 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 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 ```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 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 // 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 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 ```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 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 ```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(); } @@ -295,29 +350,29 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 -보안 키 백업 구성은 세 가지 형태를 허용합니다: X API의 `juicebox_config` 객체(권장—그대로 전달), 전체 `sdk_config` 래퍼, 또는 순수한 `token_map`. +보안 키 백업 구성은 세 가지 형태를 허용합니다: X API `juicebox_config` 객체(권장—그대로 전달), 전체 `sdk_config` 래퍼, 또는 순수 `token_map`. -선택 사항: 서명 검증은 **기본적으로 켜져 있습니다**(`reject_unverified = true`)—비활성화하려면 `set_reject_unverified(false)`를 호출하세요(권장하지 않음); 백업 realm 구성이 변경되면 `update_config`; UI 상태를 위해 `is_unlocked` / `has_identity_key`. 전체 필드 목록은 [chat-xdk 저장소](https://github.com/xdevplatform/chat-xdk)의 스텁에 있습니다. +선택 사항: 서명 검증은 **기본적으로 켜져** 있습니다(`reject_unverified = true`). 이를 비활성화하려면 `set_reject_unverified(false)`를 호출하세요(권장하지 않음). 백업 realm 구성이 변경되면 `update_config`; UI 상태를 위해 `is_unlocked` / `has_identity_key`. 전체 필드 목록은 [chat-xdk 저장소](https://github.com/xdevplatform/chat-xdk) 스텁에서 확인할 수 있습니다. --- ## 대화 키 -세 가지 **prepare** 메서드는 각각 하나의 호출로 키 변경에 필요한 모든 것을 수행합니다: 새 대화 키를 생성하고, 각 참가자(전달한 공개 키에서)에 대해 암호화하고, 변경 사항에 서명합니다. 모두 동일한 **`PreparedConversationChange`** 형태를 반환하며, POST할 준비가 되어 있습니다—`conversation_participant_keys`에서 SDK 필드 `encrypted_key`를 **`encrypted_conversation_key`**로 이름을 바꾸고, action signature를 필수 **`action_signatures`** 본문 필드로 매핑하세요. +세 개의 **prepare** 메서드는 각각 한 번의 호출로 키 변경에 필요한 모든 작업을 수행합니다: 새 대화 키를 생성하고, 각 참가자에 대해 암호화하며(전달한 공개 키로), 변경 사항을 서명합니다. 발신자 신원과 서명 키 버전은 세션(`set_identity`)에서 가져옵니다. 재정의하려면 params에 `sender_id` / `signing_key_version`을 설정하세요. 모두 동일한 **`PreparedConversationChange`** 형태를 반환하며 POST할 준비가 되어 있습니다—`conversation_participant_keys`에서 SDK 필드 `encrypted_key`를 **`encrypted_conversation_key`**로 이름을 바꾸고, 액션 서명을 필수 **`action_signatures`** 본문 필드로 매핑하세요. -| 시나리오 | 메서드 | 반환되는 action signature | +| 시나리오 | 메서드 | 반환되는 액션 서명 | |:---------|:-------|:---------------------------| -| 1:1 시작(대화 ID 생략—SDK가 파생함) 또는 어떤 대화의 키든 교체(ID 전달) | `prepare_conversation_key_change` | 1 | -| 그룹 생성 (`POST /2/chat/conversations/group/initialize`로 발급된 ID) | `prepare_group_create` | 2—둘 다 전송 | +| 1:1 시작(대화 ID 생략—SDK가 유도) 또는 임의 대화의 키 교체(ID 전달) | `prepare_conversation_key_change` | 1 | +| 그룹 생성(`POST /2/chat/conversations/group/initialize`가 생성한 ID) | `prepare_group_create` | 2—둘 다 전송 | | 그룹에 멤버 추가 | `prepare_group_members_change` | 2—둘 다 전송 | -`encrypt_message`와 미디어를 위해 **원시** 키 바이트를 보관하세요; API의 암호화된 봉투를 암호화에 전달하지 마세요. +`encrypt_message` 및 미디어에 사용할 **원시** 키 바이트를 보관하세요. API의 암호화된 봉투를 encrypt에 전달하지 마세요. -**래핑하기 전에 가져온 키를 검증하세요.** prepare 메서드는 전달된 공개 키로 새 대화 키를 암호화합니다. 대체된 신원 키가 대화 키를 받지 못하도록, 전달하기 전에 각 가져온 레코드—공개 키 API의 `public_key`, `signing_public_key`, `identity_public_key_signature` 필드—에 대해 `verify_key_binding(identity, signing, signature)`를 호출하세요. +**래핑하기 전에 가져온 키를 검증하세요.** prepare 메서드는 전달된 공개 키로 새 대화 키를 암호화합니다. 전달하기 전에 각 가져온 레코드에 대해 `verify_key_binding(identity, signing, signature)`를 호출하세요—공개 키 API의 `public_key`, `signing_public_key`, `identity_public_key_signature` 필드를 전달하면 대체된 신원 키가 대화 키를 받는 것을 방지할 수 있습니다. -`{ keys, latest_version }`를 재구축하려면 키 변경 이벤트 페이로드에 `extract_conversation_keys`를 사용하세요. `decrypt_conversation_key`는 단일 ECIES blob을 언래핑합니다. +키 변경 이벤트 페이로드에 `extract_conversation_keys`를 사용하여 `{ keys, latest_version }`을 재구성하세요. `decrypt_conversation_key`는 단일 ECIES blob을 언랩합니다. @@ -327,7 +382,7 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 # {"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 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 ```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 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 ```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 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 ```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 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 ```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 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 ```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); @@ -404,15 +452,24 @@ SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보 -그룹 생성 및 멤버 추가의 경우, 각 메서드가 필요로 하는 매개변수를 전달하세요(`prepare_group_create`에는 멤버/관리자 ID 목록; `prepare_group_members_change`에는 새 로스터와 현재 로스터)—샘플은 [그룹](/xchat/groups#create-the-group-and-establish-keys)을 참조하세요. 둘 다 **두 개**의 action signature를 반환합니다; POST는 둘 다 포함해야 합니다. +그룹 생성 및 멤버 추가의 경우, 각 메서드에 필요한 params를 전달하세요(`prepare_group_create`의 경우 멤버/관리자 ID 목록; `prepare_group_members_change`의 경우 새 멤버와 현재 명단)—샘플은 [그룹](/xchat/groups#create-the-group-and-establish-keys)을 참고하세요. 둘 다 **두 개**의 액션 서명을 반환하며, POST에는 둘 다 포함해야 합니다. --- ## 복호화 -**`decrypt_events`**는 이력과 백로그용입니다: 스트림에서 대화 키를 가져오고, 복호화된 메시지를 반환하며, 전체 배치가 실패하는 대신 이벤트별 오류를 **수집**합니다. **`decrypt_event`**는 이미 키 캐시가 있을 때 단일 실시간 이벤트용입니다; 실패 시 예외를 발생/던집니다. +**`decrypt_events`**는 이력 및 백로그용입니다: 스트림에서 대화 키를 추출하고, 복호화된 메시지를 반환하며, 전체 배치를 실패시키는 대신 이벤트별 오류를 **수집**합니다. **`decrypt_event`**는 단일 라이브 이벤트용입니다. 실패 시 예외를 발생/던집니다. + +SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. API 공개 키 필드를 `SigningKeyEntry`로 매핑하세요: `public_key_version` → `public_key_version`(동일한 이름), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, 그리고 `identity_public_key_signature`와 `user_id`. + +두 개의 선택적 세션 저장소를 사용하면 호출별 키 인수를 생략할 수 있습니다: -SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. API 공개 키 필드를 `SigningKeyEntry`에 매핑합니다: `public_key_version` → `public_key_version`(같은 이름), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, 그리고 `identity_public_key_signature`와 `user_id`. 검증은 기본적으로 필수입니다: 서명 키 목록을 생략하거나 빈 목록을 전달해도 이를 건너뛰지 **않습니다**—서명된 이벤트는 실패합니다(`decrypt_events`의 경우 `errors`에 수집, `decrypt_event`의 경우 던져짐). 실제로 검증을 건너뛰려면 먼저 `set_reject_unverified(false)`를 호출해야 합니다(프로덕션에서는 권장하지 않음). +- **`set_signing_keys(entries)`**는 참가자의 서명 키를 저장합니다. 서명 키 인수를 생략하거나 빈 값을 전달하는 복호화 호출은 저장소를 대신 사용합니다. 검증 자체는 동일합니다—키는 이 호출을 통해서만 저장소에 진입하며, 복호화 대상 이벤트에서는 절대 들어오지 않습니다. 각 호출은 이전 세트를 대체합니다. +- **`set_cache_keys(true)`**는 대화 키 캐시를 활성화합니다(기본값 꺼짐). 활성화된 동안 `decrypt_events`는 대화별로, 키 변경이 유효한 서명을 담고 있던 최신 키를 캐시합니다. `decrypt_event`는 대화 키 인수가 생략되었을 때 여기에 폴백하고, encrypt 헬퍼는 생략된 대화 키를 여기에서 해석합니다. 비활성화하면 캐시가 지워집니다. + +명시적으로 비어 있지 않은 인수는 항상 저장소보다 우선합니다. 명시적인 호출별 인수는 여전히 일급이며—서버리스 또는 다중 인스턴스 배포에서는 요청이 저장소가 비어 있는 새 인스턴스에 도착할 수 있으므로 올바른 선택입니다. + +검증은 기본적으로 필수입니다: 서명 키를 생략해도 검증이 건너뛰어지지 않습니다. 아무것도 전달하지 않고 아무것도 저장되어 있지 않으면 서명된 이벤트는 실패합니다(`decrypt_events`의 경우 `errors`에 수집되고, `decrypt_event`의 경우 던져짐). 실제로 검증을 건너뛰려면 먼저 `set_reject_unverified(false)`를 호출해야 합니다(프로덕션에서는 권장하지 않음). @@ -430,11 +487,11 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A 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 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A 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 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A 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 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A 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 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A 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,32 +555,40 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A ## 암호화 및 전송 헬퍼 -**`encrypt_message`**는 텍스트 메시지를 위한 서명된 암호문을 만듭니다(선택적 엔티티, `media_hash_key`를 통한 첨부, TTL, 알림 플래그). 반환된 페이로드를 메시지 전송 본문에 매핑하세요: `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**, 그리고 **`message_id`**. +**`encrypt_message(conversation_id, text)`**는 텍스트 메시지에 대한 서명된 암호문을 생성합니다. 선택적으로 `entities`, `attachments`(`media_hash_key`를 통해), `should_notify`, `ttl_msec`을 사용할 수 있습니다. 발신자 신원은 세션(`set_identity`)에서, 대화 키는 선택적 키 캐시(`set_cache_keys`)에서 가져옵니다—또는 `sender_id` / `signing_key_version` 및 `conversation_key` + `conversation_key_version`을 명시적으로 전달하세요. SDK가 **`message_id`**(서명된 이벤트에 삽입되는 UUID)를 생성하여 페이로드에 반환합니다—직접 만들지 마세요. 재시도 시 동일한 페이로드를 재사용하여 ID가 두 번 생성되지 않도록 하세요. 페이로드를 send-message 본문에 매핑하세요: `message_id` → **`message_id`**, `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**. + +**답장은 이벤트 기반입니다.** `encrypt_reply(conversation_id, text, reply_to_event)`는 답장 대상이 되는 base64 원시 이벤트를 받습니다. SDK는 이로부터 인용된 미리보기(시퀀스 ID, 발신자, 텍스트, 엔티티, 첨부 파일)를 유도하고, 서명된 원본을 발신 메시지에 삽입하여 수신자가 인용을 검증할 수 있도록 합니다. 원본이 답장보다 이전 키 버전으로 암호화된 경우 `reply_to_ckces`—원시 키 변경 이벤트—를 전달하세요. 원본이 **편집된** 경우, 원시 편집 이벤트를 `reply_to_edit_event`로 전달하세요: 그러면 미리보기는 메시지가 현재 말하는 내용을 인용하고(텍스트와 엔티티는 편집본에서 가져옴), 편집본이 원본과 함께 전달되어 수신자가 확인할 수 있습니다. 명시적인 `reply_to_*` 필드는 원시 이벤트를 더 이상 보유하지 않은 호출자를 위한 재정의로 남아 있습니다. -회신과 반응에는 **`encrypt_reply`**, **`encrypt_add_reaction`**, **`encrypt_remove_reaction`**을 사용하세요(`sequence_id`는 부모를 대상으로 함). **`encrypt` / `decrypt`**는 대화 키 아래의 UTF-8 메타데이터용입니다(예: 암호화된 그룹 이름)—메시지 봉투용이 아닙니다. **`encrypt_stream` / `decrypt_stream`**은 첨부 바이트를 암호화합니다; [미디어](/xchat/media) 참조. 저수준 **`sign` / `verify` / `verify_key_binding`**은 고급 흐름을 지원합니다; 대화 키 변경, 그룹 생성, 멤버 추가는 [prepare 메서드](#conversation-keys)에 의해 서명됩니다. +**반응 또한 이벤트 기반입니다.** `encrypt_add_reaction(target_event, emoji)`와 `encrypt_remove_reaction(...)`은 반응 대상이 되는 원시 이벤트에서 대화 ID와 대상 시퀀스 ID를 유도합니다. 동일한 params로 반응을 추가하고 나중에 제거할 수 있습니다. 원시 이벤트를 더 이상 보유하지 않은 경우에만 `conversation_id`와 `target_message_sequence_id`를 명시적으로 설정하세요. -`encrypt_message` / `encrypt_reply`에 전달되는 대화 ID는 보유하고 있는 어떤 형태든 될 수 있습니다—이벤트의 `A:B`, 목록이나 URL 경로의 `A-B`(어떤 순서든), 또는 순수한 수신자 사용자 ID—SDK가 서명 전에 정규화합니다. 그룹 ID(`g` 접두사)는 그대로 통과됩니다. +수신 측에서는, 답장을 인용하는 복호화된 메시지가 **`reply_preview_validation`**(`"Valid"` / `"Invalid"`; JS 바인딩은 `'valid'` / `'invalid'` 사용)을 담고 있습니다: SDK는 삽입된 원본의 서명을 서명 키로 검증하고—이벤트에 담긴 키로는 절대 검증하지 않음—복호화한 뒤, 인용된 내용과 작성자를 원본과 비교했습니다. 미리보기에 편집 이벤트가 삽입되어 있으면 SDK는 동일한 방식으로 편집본을 검증하고(동일한 대화, 원본과 동일한 작성자), 편집 이전 텍스트가 아닌 편집된 내용과 인용 텍스트를 비교합니다. 메시지에 미리보기가 없거나 미리보기에 원본이 삽입되지 않은 경우 이 필드는 없습니다. `Invalid` 미리보기는 신뢰할 수 없는 것으로 간주하세요: 메시지 자체는 진짜이지만 인용된 자료는 그렇지 않습니다—인용은 오직 검증된 원본에서만 렌더링하세요. + +**`encrypt` / `decrypt`**는 대화 키로 UTF-8 메타데이터(예: 암호화된 그룹 이름)를 다룰 때 사용합니다—메시지 봉투용이 아닙니다. **`encrypt_stream` / `decrypt_stream`**은 첨부 파일 바이트를 암호화합니다. [미디어](/xchat/media)를 참고하세요. 저수준 **`sign` / `verify` / `verify_key_binding`**은 고급 흐름을 지원합니다. 대화 키 변경, 그룹 생성 및 멤버 추가는 [prepare 메서드](#대화-키)로 서명됩니다. + +`encrypt_message` / `encrypt_reply`에 전달하는 대화 ID는 보유하고 있는 어떤 형태든 가능합니다—이벤트의 `A:B`, 목록이나 URL 경로의 `A-B`(어떤 순서든), 또는 그저 수신자의 사용자 ID—SDK는 서명 전에 이를 정규화합니다. 그룹 ID(`g` 접두사)는 그대로 통과됩니다. ```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 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A ```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 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A ```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); @@ -611,7 +726,7 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A ## 미디어 스트림 -텍스트에 사용된 것과 **동일한** 대화 키로 파일 바이트를 암호화하고, Chat 미디어 API를 통해 업로드하며, `encrypt_message`에 **`media_hash_key`**를 첨부하세요. 이것은 Posts 미디어 모델(`expansions=attachments.media_keys`)이 아닙니다. 전체 업로드/다운로드 흐름: [미디어](/xchat/media). +파일 바이트를 텍스트에 사용한 **동일한** 대화 키로 암호화하고, Chat 미디어 API를 통해 업로드한 뒤, `encrypt_message`에 **`media_hash_key`**를 첨부하세요. 이는 게시물 미디어 모델(`expansions=attachments.media_keys`)이 아닙니다. 전체 업로드/다운로드 흐름: [미디어](/xchat/media). @@ -659,12 +774,12 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A -### 큰 미디어를 위한 증분 스트리밍 +### 대용량 미디어를 위한 점진적 스트리밍 -큰 파일의 경우, 전체 페이로드를 메모리에 보관하지 마세요: `stream_encryptor()` / `stream_decryptor()`는 청크(각 약 1 MB)로 `push(chunk)`를 통해 공급한 다음 마지막에 `finish()`를 한 번 호출하는 `StreamEncryptor` / `StreamDecryptor`를 반환합니다. 복호화 시 `finish()`는 잘린 스트림을 감지합니다(마지막 프레임 전에 입력이 끝났으면 실패), 그러므로 성공하기 전까지 푸시된 평문을 완료된 것으로 취급하지 마세요. +대용량 파일의 경우, 전체 페이로드를 메모리에 유지하지 않도록 하세요: `stream_encryptor()` / `stream_decryptor()`는 `StreamEncryptor` / `StreamDecryptor`를 반환하며, 청크(각 약 1 MB)로 `push(chunk)`를 통해 공급한 다음 마지막에 `finish()`를 한 번 호출합니다. 복호화 시 `finish()`는 잘린 스트림을 감지합니다(최종 프레임 전에 입력이 끝나면 실패). 따라서 성공하기 전까지는 push된 평문을 완전한 것으로 취급하지 마세요. -**JS/WASM 전용:** `finish()`는 기본 WASM 객체를 소비하고 해제합니다—`finish()` 후에는 절대 `free()`를 호출하지 마세요(예외를 던집니다). `finish()` *이전*에 스트림을 포기하는 경우에만 `free()`를 호출하세요(예: 오류 경로에서). +**JS/WASM에서만:** `finish()`는 기본 WASM 객체를 소비하고 해제합니다. `finish()` 이후에는 절대 `free()`를 호출하지 마세요(예외 발생). 완료 *전*에 스트림을 포기하는 경우에만(예: 오류 경로) `free()`를 호출하세요. @@ -701,7 +816,7 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A ## 유틸리티 -Base64/hex 헬퍼, MIME 감지, 이미지 치수는 모듈 수준 함수(Python/JS/Rust/Go) 또는 `ChatXdkUtilities`(C#/Java)로 사용할 수 있습니다—추가 라이브러리를 가져오지 않고 첨부 파일 메타데이터를 구축할 때 유용합니다. +Base64/hex 헬퍼, MIME 스니핑, 이미지 크기 감지는 모듈 수준 함수(Python/JS/Rust/Go) 또는 `ChatXdkUtilities`(C#/Java)로 제공됩니다—추가 라이브러리 없이 첨부 파일 메타데이터를 구성할 때 유용합니다. @@ -790,25 +905,25 @@ Base64/hex 헬퍼, MIME 감지, 이미지 치수는 모듈 수준 함수(Python/ --- -## 중요한 타입 +## 주요 타입 -이러한 개념적 타입은 언어 전반에 걸쳐 나타납니다(정확한 필드 이름은 다릅니다; JS는 종종 `message`와 같은 camelCase 이벤트 판별자를 사용합니다): +다음의 개념적 타입은 여러 언어에서 등장합니다(정확한 필드 이름은 다릅니다. JS는 종종 `message`와 같은 camelCase 이벤트 판별자를 사용합니다): -- **SendPayload** — `encrypt_message` 및 관련 암호화 헬퍼의 반환값; Chat API 전송 본문에 매핑. +- **SendPayload** — `encrypt_message` 및 다른 encrypt 헬퍼의 반환 값: SDK가 생성한 **`message_id`**(서명된 이벤트에 삽입된 UUID—메시지의 `message_id`로 전송하고 중복 제거를 위해 보관), `encrypted_content`, `encoded_event_signature`, 서명 메타데이터, `conversation_key_version`, `should_notify`. Chat API 전송 본문에 매핑하세요. - **PublicKeyRegistrationPayload** — 공개 키 추가 API를 위한 `generate_keypairs` / 공개 키 게터의 출력. -- **SigningKeyEntry** — 서명 검증을 위해 복호화에 전달되는 발신자 공개 자료. -- **PreparedConversationChange** — 세 가지 prepare 메서드의 출력: 파생되거나 전달된 `conversation_id`, 원시 `conversation_key` 바이트, `conversation_key_version`, `participant_keys`(`user_id`, `encrypted_key`, `public_key_version`), 그리고 `action_signatures`(`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, 선택적 `signature_payload`—키 변경 서명에서는 해당 페이로드가 평문 키를 포함하므로 생략됨). -- **DecryptEventsResult** — 메시지, 선택적 오류, 그리고 추출된 `conversation_keys`. +- **SigningKeyEntry** — 서명 검증을 위해 복호화에 전달하거나 `set_signing_keys`를 통해 저장하는 발신자 공개 자료. +- **PreparedConversationChange** — 세 prepare 메서드의 출력: 유도되거나 전달된 `conversation_id`, 원시 `conversation_key` 바이트, `conversation_key_version`, `participant_keys`(`user_id`, `encrypted_key`, `public_key_version`), `action_signatures`(`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, 선택적 `signature_payload`—키 변경 서명에서는 이 페이로드가 평문 키를 포함하기 때문에 생략됨). +- **DecryptEventsResult** — 메시지, 선택적 오류, 그리고 추출된 `conversation_keys`. 답장을 인용하는 복호화된 메시지는 `reply_preview_validation`을 담고 있습니다([암호화 및 전송 헬퍼](#암호화-및-전송-헬퍼) 참고). -전체 필드 목록은 [chat-xdk 저장소](https://github.com/xdevplatform/chat-xdk)의 언어 스텁(`docs/API.md`, `*.pyi`, `index.d.ts`)을 사용하세요. +전체 필드 목록은 [chat-xdk 저장소](https://github.com/xdevplatform/chat-xdk)의 언어별 스텁(`docs/API.md`, `*.pyi`, `index.d.ts`)을 사용하세요. --- ## 오류 -Python은 일반적으로 설명적인 메시지와 함께 **`ValueError`**를 발생시킵니다(예: 잘못된 패스코드). TypeScript/JavaScript는 **`Error`**를 던집니다. Go는 `(value, error)`를 반환합니다. 이력의 경우 하나의 잘못된 이벤트가 배치를 중단시키지 않도록 **`decrypt_events`**를 선호하세요; 부분적 실패에 대해서는 errors 컬렉션을 검사하세요. +Python은 일반적으로 설명이 포함된 메시지와 함께 **`ValueError`**를 발생시킵니다(예: 잘못된 패스코드). TypeScript/JavaScript는 **`Error`**를 던집니다. Go는 `(value, error)`를 반환합니다. 이력에는 **`decrypt_events`**를 선호하세요. 그러면 잘못된 이벤트 하나가 배치를 중단시키지 않습니다. 부분 실패는 errors 컬렉션을 검사하세요. -일부 검증 오류는 **영구적**입니다. 서명은 불변이며 이벤트 자체에서 서명된 페이로드를 재구축하여 검증되므로, `signature missing or no matching signing key` 또는 ECDSA 불일치로 실패하는 오래된 이벤트는 이후 로드 시마다 실패합니다—재시도, 키 갱신, 또는 API 호출로도 치유할 수 없습니다. 이러한 것을 일시적 오류가 아닌 툼스톤으로 취급하세요. 대화 키를 교체하면 그 시점 이후부터 깨끗하고 검증 가능한 이력이 시작됩니다. +일부 검증 오류는 **영구적**입니다. 서명은 불변이며 이벤트 자체에서 서명된 페이로드를 재구축하여 검증됩니다. 따라서 `signature missing or no matching signing key` 또는 ECDSA 불일치로 실패한 오래된 이벤트는 이후 모든 로드에서 실패합니다—재시도, 키 갱신, API 호출로도 치유할 수 없습니다. 이는 일시적 오류가 아닌 툼스톤으로 취급하세요. 대화 키를 교체하면 그 시점부터 깨끗하고 검증 가능한 이력이 시작됩니다. --- @@ -825,6 +940,6 @@ Python은 일반적으로 설명적인 메시지와 함께 **`ValueError`**를 웹훅 및 활동 전달 - 일반적인 실패 + 자주 발생하는 실패 diff --git a/pt/xchat/cryptography-primer.mdx b/pt/xchat/cryptography-primer.mdx index 00dd36511..618566a50 100644 --- a/pt/xchat/cryptography-primer.mdx +++ b/pt/xchat/cryptography-primer.mdx @@ -143,9 +143,9 @@ Um desafio central na criptografia de ponta a ponta é a **distribuição de cha Quando uma conversa é preparada para mensagens: -1. Uma chave de conversa aleatória é gerada (no Chat XDK) -2. Para **cada participante**, essa chave é criptografada para sua **chave pública de identidade** -3. Essas cópias criptografadas são armazenadas e entregues por meio das APIs de Chat do X +1. O Chat XDK gera uma chave de conversa aleatória +2. O Chat XDK criptografa essa chave para a **chave pública de identidade de cada participante** +3. Seu app publica essas cópias criptografadas por meio das APIs de Chat do X 4. Cada participante descriptografa **sua** cópia com sua chave privada de identidade (no Chat XDK) O X só lida com as cópias **envelopadas**, nunca com a chave de conversa em bruto. @@ -166,7 +166,7 @@ Seu app deve: ## Backup seguro de chaves: armazenamento distribuído de chaves -Suas chaves **privadas** de identidade e de assinatura precisam ser armazenadas com cuidado. O X Chat inclui um sistema de **backup seguro de chaves** (implementado com Juicebox) para que as chaves possam ser recuperadas com um código de acesso entre dispositivos sem dar a nenhum servidor o segredo completo. +Suas chaves **privadas** de identidade e de assinatura precisam ser armazenadas com cuidado. O X Chat inclui um sistema de **backup seguro de chaves** para que as chaves possam ser recuperadas com um código de acesso entre dispositivos sem dar a nenhum servidor o segredo completo. ### O problema com o armazenamento tradicional de chaves @@ -201,7 +201,7 @@ flowchart LR Você obtém recuperabilidade (novo dispositivo + código de acesso) sem que um único agente detenha o segredo inteiro. -Você não configura os servidores de backup de chaves à mão no fluxo normal. O Chat XDK inclui o cliente de backup; a configuração do realm vem da X API como **`juicebox_config`** no seu registro de chave pública (o campo tem esse nome por causa do Juicebox, a implementação subjacente). O armazenamento inicial do código de acesso e o desbloqueio posterior são chamadas do Chat XDK — veja [inicializar com chaves existentes](/xchat/getting-started#2-initialize-the-chat-xdk-with-existing-keys) e [criar e registrar chaves](/xchat/getting-started#3-create-and-register-keys-first-time-setup) no Guia de introdução. Alguns apps (especialmente servidores e bots) usam um blob de chave exportado em vez do backup seguro de chaves; proteja esse material como uma senha. +Você não configura os servidores de backup de chaves à mão no fluxo normal. O Chat XDK inclui o cliente de backup; a configuração do realm vem da X API como o campo **`juicebox_config`** no seu registro de chave pública. O armazenamento inicial do código de acesso e o desbloqueio posterior são chamadas do Chat XDK — veja [inicializar com chaves existentes](/xchat/getting-started#2-initialize-the-chat-xdk-with-existing-keys) e [criar e registrar chaves](/xchat/getting-started#3-create-and-register-keys-first-time-setup) no Guia de introdução. Alguns apps (especialmente servidores e bots) usam um blob de chave exportado em vez do backup seguro de chaves; proteja esse material como uma senha. --- @@ -224,7 +224,7 @@ Se algo no material assinado muda, a verificação falha. Somente alguém com a ### No seu app -O Chat XDK assina quando você criptografa mensagens de saída e verifica quando você descriptografa as de entrada, contra o material de chave pública do remetente (das APIs de chaves públicas). A verificação é **obrigatória por padrão**: o SDK rejeita eventos assinados não verificados a menos que você desative explicitamente a checagem (não recomendado). Detalhes na referência do [Chat XDK](/xchat/xchat-xdk). +O Chat XDK assina quando você criptografa mensagens de saída e verifica quando você descriptografa as de entrada contra o material de chave pública do remetente (das APIs de chaves públicas). A verificação é **obrigatória por padrão**: o SDK rejeita eventos assinados não verificados a menos que você desative explicitamente a checagem (não recomendado). Detalhes na referência do [Chat XDK](/xchat/xchat-xdk). ### Mudanças de estado assinadas (assinaturas de ação) @@ -243,7 +243,7 @@ Assinaturas são vinculadas ao conteúdo do evento e são imutáveis: um evento | Ameaça | Proteção | |:-------|:---------| | **X lendo corpos de mensagens** | O conteúdo é criptografado antes de ser enviado ao X | -| **Escutas na rede** | Segurança de transporte mais conteúdo criptografado de ponta a ponta | +| **Escutas na rede** | Segurança de transporte mais conteúdo com criptografia de ponta a ponta | | **Adulteração de mensagens** | Assinaturas detectam modificações | | **Personificação trivial de remetente** | Assinaturas válidas requerem a chave privada de assinatura do remetente | | **Roubo de chave em servidor único (com backup seguro de chaves)** | As partes são divididas entre realms e protegidas por código de acesso | @@ -264,11 +264,11 @@ Assinaturas são vinculadas ao conteúdo do evento e são imutáveis: um evento | Termo | Definição | |:------|:----------| | **Criptografia simétrica** | Mesma chave criptografa e descriptografa (usada para mensagens e streams de mídia) | -| **Criptografia assimétrica** | Chaves diferentes para criptografar vs. descriptografar (usada para envelopar chaves de conversa) | +| **Criptografia assimétrica** | Chaves diferentes para criptografar vs. descriptografar (usada para trocar chaves de conversa) | | **Chave pública** | Segura para compartilhar; usada para criptografar *para* alguém ou verificar suas assinaturas | | **Chave privada** | Deve permanecer secreta; usada para descriptografar ou assinar | | **Par de chaves** | Uma chave pública e uma chave privada vinculadas | -| **ECDH / ECIES** | Algoritmos usados ao envelopar chaves de conversa para chaves de identidade | +| **ECDH / ECIES** | Algoritmos usados ao trocar chaves de conversa via chaves de identidade | | **ECDSA** | Algoritmo de assinatura usado para autoria de mensagens | | **P-256** | Curva elíptica usada no X Chat (secp256r1) | | **Chave de conversa** | Chave simétrica compartilhada pelos participantes de uma conversa (versionada ao longo do tempo) | diff --git a/pt/xchat/getting-started.mdx b/pt/xchat/getting-started.mdx index 0cfc7ad86..560202d4b 100644 --- a/pt/xchat/getting-started.mdx +++ b/pt/xchat/getting-started.mdx @@ -45,11 +45,10 @@ Os apps do X Chat usam duas peças em conjunto: ```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 @@ Os apps do X Chat usam duas peças em conjunto: com.x chatxdk - 0.2.1 + 0.4.0 ``` @@ -136,11 +135,11 @@ Crie um cliente de API com seu token de acesso OAuth 2.0 de **usuário**: Este passo **carrega chaves que você já tem** — use-o quando esta identidade já concluiu a configuração inicial antes: - **Backup seguro de chaves:** construa o SDK com o `juicebox_config` do seu registro de chave pública e depois faça `unlock` com seu código de acesso para recuperar as chaves privadas (por exemplo, em um novo dispositivo). -- **Blob de chave:** `import_keys` com um blob que você exportou anteriormente via `export_keys`. +- **Blob de chave:** `import_keys` com um blob que você exportou anteriormente via `export_keys`, passando junto a versão de chave registrada (Rust e Go nomeiam essa variante como `import_keys_with_version` / `ImportKeysWithVersion`). -Em seguida, defina sua versão de chave pública registrada (`public_key_version` no seu registro). +Em seguida, chame **`set_identity(user_id, signing_key_version)`** uma vez, com seu ID de usuário e o `public_key_version` do seu registro. Isso armazena a identidade da sessão: toda chamada posterior de encrypt e prepare assina como essa identidade, então você nunca passa um ID de remetente ou versão de chave de assinatura por chamada. -**Configurando pela primeira vez?** Construa o SDK da mesma forma, mas pule `unlock`/`import_keys` e continue para o [passo 3](#3-criar-e-registrar-chaves-configuracao-inicial) para criar, fazer backup e registrar suas chaves. +**Configurando pela primeira vez?** Construa o SDK da mesma forma, mas pule `unlock`/`import_keys` e continue para o [passo 3](#3-create-and-register-keys-first-time-setup) para criar, fazer backup e registrar suas chaves. @@ -160,7 +159,9 @@ Em seguida, defina sua versão de chave pública registrada (`public_key_version chat = Chat(json.dumps(record["juicebox_config"])) chat.unlock("YOUR_PASSCODE") # recovers keys stored by setup() during first-time setup (step 3) - chat.set_key_version(signing_key_version) + # 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 @@ Em seguida, defina sua versão de chave pública registrada (`public_key_version getAuthToken: async (realmId) => getRealmTokenFromYourBackend(realmId), }); await chat.unlock('YOUR_PASSCODE'); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity('YOUR_USER_ID', signingKeyVersion); ``` @@ -189,11 +190,11 @@ Em seguida, defina sua versão de chave pública registrada (`public_key_version 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 @@ Em seguida, defina sua versão de chave pública registrada (`public_key_version 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 @@ Em seguida, defina sua versão de chave pública registrada (`public_key_version 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 @@ Em seguida, defina sua versão de chave pública registrada (`public_key_version 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); } ``` @@ -244,18 +247,24 @@ Em seguida, defina sua versão de chave pública registrada (`public_key_version Amostras de servidor e bots normalmente usam um **blob de chave** (`export_keys` / `import_keys`). Apps cliente frequentemente usam **backup seguro de chaves** (`setup` / `unlock` com um código de acesso). Veja a referência do [Chat XDK](/xchat/xchat-xdk) para ambos os caminhos. -**Trazendo suas próprias chaves?** `import_keys` só aceita o blob opaco produzido por `export_keys` do Chat XDK — é uma serialização privada e versionada do estado completo das chaves, não chaves P-256 em bruto ou codificadas em PEM. Você não pode construir esse blob por conta própria: gere as chaves com `generate_keypairs` ([passo 3](#3-criar-e-registrar-chaves-configuracao-inicial)), exporte o blob uma vez e armazene-o codificado em base64. Blobs criados à mão ou modificados falham na importação. +**Trazendo suas próprias chaves?** `import_keys` só aceita o blob opaco produzido por `export_keys` do Chat XDK — é uma serialização privada e versionada do estado completo das chaves, não chaves P-256 em bruto ou codificadas em PEM. Você não pode construir esse blob por conta própria: gere as chaves com `generate_keypairs` ([passo 3](#3-create-and-register-keys-first-time-setup)), exporte o blob uma vez e armazene-o codificado em base64. Blobs criados à mão ou modificados falham na importação. --- ## 3. Criar e registrar chaves (configuração inicial) -Pule este passo se você carregou chaves existentes no [passo 2](#2-inicializar-o-chat-xdk-com-chaves-existentes). Caso contrário, a configuração única de uma nova identidade faz **três coisas**: +Pule este passo se você carregou chaves existentes no [passo 2](#2-initialize-the-chat-xdk-with-existing-keys). Caso contrário, a configuração única de uma nova identidade faz **três coisas**: 1. **Criar os pares de chaves** — `generate_keypairs` produz os pares de chaves de identidade e de assinatura. -2. **Registrar as chaves públicas** — faça POST do payload de registro para o endpoint add-public-key para que outros possam criptografar para você e verificar suas assinaturas. -3. **Armazenar as chaves privadas** — `setup` com um código de acesso as grava no backup seguro de chaves (clientes), ou `export_keys` retorna um blob de chave para você armazenar com segurança (servidores e bots). +2. **Armazenar as chaves privadas** — `setup` com um código de acesso as grava no backup seguro de chaves (clientes), ou `export_keys` retorna um blob de chave para você armazenar com segurança (servidores e bots). +3. **Registrar as chaves públicas** — faça POST do payload de registro para o endpoint add-public-key para que outros possam criptografar para você e verificar suas assinaturas. + +Finalize chamando `set_identity` com a versão de chave do registro, para que esta sessão assine como a nova identidade. + + +Scripts de registro único prontos para rodar em cada binding vivem em [`chat-xdk/examples`](https://github.com/xdevplatform/chat-xdk/tree/main/examples) (Python, TypeScript, Go, Rust, C# e Java). Use-os em vez de escrever o fluxo abaixo à mão quando você precisa apenas onboardar uma nova identidade. + @@ -280,7 +289,7 @@ Pule este passo se você carregou chaves existentes no [passo 2](#2-inicializar- ), ) 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")) ``` @@ -300,7 +309,7 @@ Pule este passo se você carregou chaves existentes no [passo 2](#2-inicializar- generate_version: registration.generateVersion, }); await chat.setup('YOUR_PASSCODE'); - chat.setKeyVersion(String(registration.version ?? signingKeyVersion)); + chat.setIdentity('YOUR_USER_ID', String(registration.version ?? '1')); ``` @@ -316,6 +325,8 @@ Pule este passo se você carregou chaves existentes no [passo 2](#2-inicializar- 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); ``` @@ -335,9 +346,15 @@ Pule este passo se você carregou chaves existentes no [passo 2](#2-inicializar- 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) + } ``` @@ -349,7 +366,7 @@ Pule este passo se você carregou chaves existentes no [passo 2](#2-inicializar- $"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"); ``` @@ -367,7 +384,7 @@ Pule este passo se você carregou chaves existentes no [passo 2](#2-inicializar- 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"); ``` @@ -380,7 +397,7 @@ Use um código de acesso forte para o backup seguro de chaves. Perder o código ## 4. Configurar chaves de conversa -Chame **`prepare_conversation_key_change`** com seu ID de usuário, sua versão de chave de assinatura e cada chave pública de identidade dos participantes. Uma única chamada gera uma nova chave de conversa, criptografa-a para cada participante e assina a mudança. Faça POST do resultado no endpoint **add conversation keys** (`POST /2/chat/conversations/{id}/keys`) — o corpo precisa de `conversation_key_version`, `conversation_participant_keys` (SDK `encrypted_key` → API `encrypted_conversation_key`) e **`action_signatures`** (obrigatório; a API rejeita a chamada sem eles). Mantenha a chave de conversa **em bruto** para envio. +Chame **`prepare_conversation_key_change`** com a chave pública de identidade de cada participante; a identidade do remetente vem da sessão que você configurou no passo 2. Uma única chamada gera uma nova chave de conversa, criptografa-a para cada participante e assina a mudança. Faça POST do resultado no endpoint **add conversation keys** (`POST /2/chat/conversations/{id}/keys`) — o corpo precisa de `conversation_key_version`, `conversation_participant_keys` (SDK `encrypted_key` → API `encrypted_conversation_key`) e **`action_signatures`** (obrigatório; a API rejeita a chamada sem eles). Mantenha a chave de conversa **em bruto** para envio. A resposta retorna o ID canônico da conversa (`data.conversation_id` — o par unido por hífen para uma 1:1, ou o ID com prefixo g para um grupo) e o `data.sequence_id` da mudança de chave. Use esse ID retornado para requisições subsequentes em vez de reconstruí-lo no cliente. A mesma chamada também **rotaciona** chaves depois: passe o ID de conversa existente para `prepare_conversation_key_change` e faça POST com a nova versão de chave. Rotacione quando suspeitar que a chave de conversa foi exposta — a rotação protege apenas mensagens **futuras**; mensagens criptografadas sob versões de chave anteriores permanecem legíveis para quem possui essas versões. @@ -398,8 +415,6 @@ A resposta retorna o ID canônico da conversa (`data.conversation_id` — o par 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 ) @@ -446,8 +461,6 @@ A resposta retorna o ID canônico da conversa (`data.conversation_id` — o par // 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'), @@ -480,9 +493,9 @@ A resposta retorna o ID canônico da conversa (`data.conversation_id` — o par ```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 @@ -532,8 +545,6 @@ A resposta retorna o ID canônico da conversa (`data.conversation_id` — o par ```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}, @@ -572,21 +583,18 @@ A resposta retorna o ID canônico da conversa (`data.conversation_id` — o par 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 { @@ -625,12 +633,9 @@ A resposta retorna o ID canônico da conversa (`data.conversation_id` — o par 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> parts = new ArrayList<>(); for (var pk : prepared.participantKeys) { @@ -673,36 +678,32 @@ A resposta retorna o ID canônico da conversa (`data.conversation_id` — o par ## 5. Enviar uma mensagem -Criptografe com os bytes da chave de conversa **em bruto**. Na requisição de envio, faça o mapeamento: +Criptografe com a chave de conversa **em bruto** do passo 4. O SDK gera o ID da mensagem (um UUID), o embute no evento assinado e o retorna no payload — você nunca cria um por conta própria. Na requisição de envio, faça o mapeamento: | Campo Chat XDK | Campo do corpo da requisição | |:---------------|:------------------------------| | `encrypted_content` / `encryptedContent` / `EncryptedContent` | `encoded_message_create_event` | | `encoded_event_signature` / `encodedEventSignature` / `EncodedEventSignature` | `encoded_message_event_signature` | -| Seu ID gerado | `message_id` | +| Payload `message_id` / `messageId` / `MessageId` | `message_id` | Use um ID de conversa **com hífen** no path da URL quando a API exigir (`:` → `-`). O próprio SDK é flexível: `encrypt_message` e `encrypt_reply` aceitam o ID em qualquer forma que você tenha — `A:B` de eventos, `A-B` de listagens ou paths de URL (em qualquer ordem), ou apenas o ID de usuário do destinatário — e o canonicalizam antes de assinar. IDs de grupo (prefixados com `g`) passam sem alteração. ```python - import uuid from xdk.chat.models import SendMessageRequest - message_id = str(uuid.uuid4()) + # Sender identity resolves from set_identity (step 2) payload = chat.encrypt_message( - message_id, - "YOUR_USER_ID", "CONVERSATION_ID", - conv_key, "Hello!", - conv_key_version, - signing_key_version, + conversation_key=conv_key, + conversation_key_version=conv_key_version, ) client.chat.send_message( "RECIPIENT_USER_ID", SendMessageRequest( - message_id=message_id, + message_id=payload.message_id, # SDK-generated, embedded in the signed event encoded_message_create_event=payload.encrypted_content, encoded_message_event_signature=payload.encoded_event_signature, ), @@ -711,20 +712,15 @@ Use um ID de conversa **com hífen** no path da URL quando a API exigir (`:` → ```typescript - import { randomUUID } from 'crypto'; - - const messageId = randomUUID(); + // Sender identity resolves from setIdentity (step 2) const payload = chat.encryptMessage({ - messageId, - senderId: 'YOUR_USER_ID', conversationId: 'CONVERSATION_ID', - conversationKey: convKey, text: 'Hello!', + conversationKey: convKey, conversationKeyVersion: convKeyVersion, - signingKeyVersion, }); await client.chat.sendMessage('RECIPIENT_USER_ID', { - message_id: messageId, + message_id: payload.messageId, // SDK-generated, embedded in the signed event encoded_message_create_event: payload.encryptedContent, encoded_message_event_signature: payload.encodedEventSignature, }); @@ -734,18 +730,14 @@ Use um ID de conversa **com hífen** no path da URL quando a API exigir (`:` → ```rust use chat_xdk_core::EncryptMessageParams; - let message_id = uuid::Uuid::new_v4().to_string(); - let payload = chat.encrypt_message(EncryptMessageParams::new( - &message_id, - &sender_id, - &conversation_id, - conv_key, - "Hello!", - &conv_key_version, - &signing_key_version, - ))?; + // Sender identity resolves from set_identity (step 2) + let payload = chat.encrypt_message( + EncryptMessageParams::new(&conversation_id, "Hello!") + .with_conversation_key(conv_key, &conv_key_version), + )?; let body = serde_json::json!({ - "message_id": message_id, + // SDK-generated, embedded in the signed event + "message_id": payload.message_id, "encoded_message_create_event": payload.encrypted_content, "encoded_message_event_signature": payload.encoded_event_signature, }); @@ -758,18 +750,19 @@ Use um ID de conversa **com hífen** no path da URL quando a API exigir (`:` → ```go - messageID := uuid.NewString() + // Sender identity resolves from SetIdentity (step 2) payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{ - MessageID: messageID, - SenderID: senderID, ConversationID: conversationID, - ConversationKey: convKey, Text: "Hello!", + ConversationKey: convKey, ConversationKeyVersion: convKeyVersion, - SigningKeyVersion: signingKeyVersion, }) + if err != nil { + log.Fatal(err) + } body, _ := json.Marshal(map[string]string{ - "message_id": messageID, + // SDK-generated, embedded in the signed event + "message_id": payload.MessageID, "encoded_message_create_event": payload.EncryptedContent, "encoded_message_event_signature": payload.EncodedEventSignature, }) @@ -785,18 +778,14 @@ Use um ID de conversa **com hífen** no path da URL quando a API exigir (`:` → ```csharp - var messageId = Guid.NewGuid().ToString(); - var payload = chat.EncryptMessage(new EncryptMessageParams { - MessageId = messageId, - SenderId = senderId, - ConversationId = conversationId, + // Sender identity resolves from SetIdentity (step 2) + var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hello!") { ConversationKey = convKey, - Text = "Hello!", ConversationKeyVersion = convKeyVersion, - SigningKeyVersion = signingKeyVersion, }); var sendJson = System.Text.Json.JsonSerializer.Serialize(new Dictionary { - ["message_id"] = messageId, + // SDK-generated, embedded in the signed event + ["message_id"] = payload.MessageId, ["encoded_message_create_event"] = payload.EncryptedContent, ["encoded_message_event_signature"] = payload.EncodedEventSignature, }); @@ -810,19 +799,16 @@ Use um ID de conversa **com hífen** no path da URL quando a API exigir (`:` → ```java - EncryptMessageParams params = new EncryptMessageParams(); - params.messageId = UUID.randomUUID().toString(); - params.senderId = senderId; - params.conversationId = conversationId; + // Sender identity resolves from setIdentity (step 2) + EncryptMessageParams params = new EncryptMessageParams(conversationId, "Hello!"); params.conversationKey = convKey; - params.text = "Hello!"; params.conversationKeyVersion = convKeyVersion; - params.signingKeyVersion = signingKeyVersion; SendPayload payload = chat.encryptMessage(params); String pathId = conversationId.replace(':', '-'); String sendJson = new ObjectMapper().writeValueAsString(Map.of( - "message_id", params.messageId, + // SDK-generated, embedded in the signed event + "message_id", payload.messageId, "encoded_message_create_event", payload.encryptedContent, "encoded_message_event_signature", payload.encodedEventSignature)); HttpRequest req = HttpRequest.newBuilder() @@ -836,6 +822,10 @@ Use um ID de conversa **com hífen** no path da URL quando a API exigir (`:` → + +Os snippets passam a chave de conversa explicitamente porque neste fluxo você acabou de criá-la no passo 4. Depois que o cache de chaves está ativo e uma passagem de `decrypt_events` verificou a chave da conversa ([passo 6](#6-receive-and-decrypt)), `encrypt_message(conversation_id, text)` sozinho é suficiente — o SDK preenche a chave verificada mais recente. Reenvios devem reenviar o **mesmo** payload criptografado, para que um ID nunca seja cunhado duas vezes. + + --- ## 6. Receber e descriptografar @@ -844,14 +834,14 @@ Use [webhooks ou o stream de atividades](/xchat/real-time-events) para tráfego - Campos de payload ao vivo: `encoded_event`, opcional `conversation_key_change_event` - Histórico: `GET /2/chat/conversations/{id}/events` — prefira **`decrypt_events`** em todos os eventos, junto com `meta.conversation_key_events` -- Passe as chaves públicas do remetente para descriptografar para verificação de assinatura (mapeie campos da API para `SigningKeyEntry`; veja [Chat XDK](/xchat/xchat-xdk)) +- A descriptografia precisa das **chaves de assinatura** dos remetentes para que o SDK possa verificar quem escreveu cada mensagem. Essas são as chaves *públicas* dos outros participantes — busque-as no mesmo endpoint de chaves públicas que você usou no passo 4 e mapeie os campos para `SigningKeyEntry` (os snippets abaixo incluem o mapeamento) +- Você pode passar as chaves de assinatura (e, para `decrypt_event`, as chaves de conversa) em cada chamada, **ou** definir dois armazenamentos de sessão opcionais uma vez e usar as formas curtas de chamada. Os snippets abaixo usam os armazenamentos: `set_signing_keys(entries)` mantém as chaves dos participantes, e `set_cache_keys(true)` (desligado por padrão) mantém a chave **com assinatura verificada** mais recente de cada conversa para que chamadas posteriores possam omitir argumentos de chave. Os dois estilos verificam de forma idêntica - JavaScript usa tipos de evento em camelCase (`message`); outras linguagens usam `"Message"` e campos snake_case no JSON ```python - conversation_keys = {} # conversation_id -> { version: key_bytes } - + # Once per process: fill the signing-key store and enable the key cache def signing_keys_for(user_id: str) -> list[dict]: resp = client.chat.get_user_public_keys( user_id, @@ -870,25 +860,34 @@ Use [webhooks ou o stream de atividades](/xchat/real-time-events) para tráfego for r in resp.data ] + chat.set_signing_keys( + signing_keys_for("YOUR_USER_ID") + signing_keys_for("RECIPIENT_USER_ID") + ) + chat.set_cache_keys(True) + + # Initial load or pagination: batch decrypt. Conversation keys are + # extracted from the KeyChange events in the batch; per-event failures + # are collected in result["errors"], never raised. + result = chat.decrypt_events(all_events_b64) + for dm in result["messages"]: + event = dm["event"] + if event["type"] == "Message" and event["content"]["content_type"] == "Text": + print(event["sender_id"], event["content"]["text"], event["verified"]) + + # Live traffic: one event at a time def handle_payload(payload: dict): - cid = payload["conversation_id"] if payload.get("conversation_key_change_event"): - conversation_keys[cid] = chat.extract_conversation_keys( - [payload["conversation_key_change_event"]] - )["keys"] - event = chat.decrypt_event( - payload["encoded_event"], - conversation_keys.get(cid, {}), - signing_keys_for(payload["sender_id"]), - ) - if event.get("type") == "Message" and event.get("content", {}).get("content_type") == "Text": - print(event["sender_id"], event["content"]["text"], event.get("verified")) + # A rotation enters the key cache only after its signature + # verifies, which is what decrypt_events does + chat.decrypt_events([payload["conversation_key_change_event"]]) + event = chat.decrypt_event(payload["encoded_event"]) # raises on failure + if event["type"] == "Message" and event["content"]["content_type"] == "Text": + print(event["sender_id"], event["content"]["text"], event["verified"]) ``` ```typescript - const conversationKeys = new Map>(); - + // Once per process: fill the signing-key store and enable the key cache async function signingKeysFor(userId: string) { const resp = await client.chat.getUserPublicKeys(userId, { publicKeyFields: [ @@ -909,24 +908,33 @@ Use [webhooks ou o stream de atividades](/xchat/real-time-events) para tráfego })); } - async function handlePayload(payload: { - conversation_id: string; + chat.setSigningKeys([ + ...(await signingKeysFor('YOUR_USER_ID')), + ...(await signingKeysFor('RECIPIENT_USER_ID')), + ]); + chat.setCacheKeys(true); + + // Initial load or pagination: batch decrypt. Conversation keys are + // extracted from the KeyChange events in the batch; per-event failures + // are collected in result.errors, never thrown. + const result = chat.decryptEvents(allEventsB64); + for (const dm of result.messages) { + if (dm.event.type === 'message' && dm.event.content?.contentType === 'text') { + console.log(dm.event.senderId, dm.event.content.text, dm.event.verified); + } + } + + // Live traffic: one event at a time + function handlePayload(payload: { encoded_event: string; - sender_id: string; conversation_key_change_event?: string; }) { - const cid = payload.conversation_id; if (payload.conversation_key_change_event) { - conversationKeys.set( - cid, - chat.extractConversationKeys([payload.conversation_key_change_event]).keys, - ); + // A rotation enters the key cache only after its signature + // verifies, which is what decryptEvents does + chat.decryptEvents([payload.conversation_key_change_event]); } - const event = chat.decryptEvent( - payload.encoded_event, - conversationKeys.get(cid) ?? {}, - await signingKeysFor(payload.sender_id), - ); + const event = chat.decryptEvent(payload.encoded_event); // throws on failure if (event.type === 'message' && event.content?.contentType === 'text') { console.log(event.senderId, event.content.text, event.verified); } @@ -935,25 +943,48 @@ Use [webhooks ou o stream de atividades](/xchat/real-time-events) para tráfego ```rust - // Build Vec from GET /2/users/{id}/public_keys - // (public_key_version, public_key, signing_public_key, identity_public_key_signature) + // Once per instance: fill the signing-key store (Vec + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.set_signing_keys(participant_signing_keys); + chat.set_cache_keys(true); + + // Initial load: batch decrypt — per-event failures land in result.errors + let result = chat.decrypt_events(&all_events_b64, &[]); + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what decrypt_events does if let Some(kc) = key_change_b64.as_deref() { - let extracted = chat.extract_conversation_keys(&[kc]); - conv_keys.extend(extracted.keys); + chat.decrypt_events(&[kc], &[]); } - let event = chat.decrypt_event(&encoded_event, &conv_keys, &sender_signing_keys)?; + let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?; ``` ```go - if keyChange != "" { - extracted, _ := chat.ExtractConversationKeys([]string{keyChange}) - for v, k := range extracted.Keys { - convKeys[v] = k + // Once per instance: fill the signing-key store ([]SigningKeyEntry + // from GET /2/users/{id}/public_keys) and enable the key cache + if err := chat.SetSigningKeys(participantSigningKeys); err != nil { + log.Fatal(err) + } + chat.SetCacheKeys(true) + + // Initial load: batch decrypt — per-event failures land in result.Errors + result, err := chat.DecryptEvents(allEventsB64, nil) + if err != nil { + log.Fatal(err) + } + for _, dm := range result.Messages { + if dm.Event.Type == "Message" { + fmt.Println(dm.Event.AsMessage().Text()) } } - event, err := chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys) + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what DecryptEvents does + if keyChange != "" { + chat.DecryptEvents([]string{keyChange}, nil) + } + event, err := chat.DecryptEvent(encodedEvent, nil, nil) if err == nil && event.Type == "Message" { fmt.Println(event.AsMessage().Text()) } @@ -961,24 +992,49 @@ Use [webhooks ou o stream de atividades](/xchat/real-time-events) para tráfego ```csharp - if (!string.IsNullOrEmpty(keyChangeB64)) + // Once per instance: fill the signing-key store (SigningKeyEntry list + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.SetSigningKeys(participantSigningKeys); + chat.SetCacheKeys(true); + + // Initial load: batch decrypt — per-event failures land in result.Errors + var result = chat.DecryptEvents(allEventsB64); + foreach (var dm in result.Messages) { - var extracted = chat.ExtractConversationKeys(new[] { keyChangeB64 }); - foreach (var kv in extracted.Keys) - convKeys[kv.Key] = kv.Value; + if (dm.Event.GetProperty("type").GetString() == "Message") + Console.WriteLine(dm.Event.GetProperty("content").GetProperty("text").GetString()); } - var evt = chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys); + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what DecryptEvents does + if (!string.IsNullOrEmpty(keyChangeB64)) + chat.DecryptEvents(new[] { keyChangeB64 }); + var evt = chat.DecryptEvent(encodedEvent); // throws on failure if (evt.GetProperty("type").GetString() == "Message") Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString()); ``` ```java + // Once per instance: fill the signing-key store (SigningKeyEntry list + // from GET /2/users/{id}/public_keys) and enable the key cache + chat.setSigningKeys(participantSigningKeys); + chat.setCacheKeys(true); + + // Initial load: batch decrypt — per-event failures land in result.errors + DecryptEventsResult result = chat.decryptEvents(allEventsB64, null); + for (DecryptedMessage dm : result.messages) { + if ("Message".equals(dm.event.path("type").asText())) { + System.out.println(dm.event.path("content").path("text").asText()); + } + } + + // Live traffic: a rotation enters the key cache only after its + // signature verifies, which is what decryptEvents does if (keyChangeB64 != null && !keyChangeB64.isEmpty()) { - var extracted = chat.extractConversationKeys(List.of(keyChangeB64)); - convKeys.putAll(extracted.keys); + chat.decryptEvents(List.of(keyChangeB64), null); } - JsonNode evt = chat.decryptEvent(encodedEvent, convKeys, senderSigningKeys); + JsonNode evt = chat.decryptEvent(encodedEvent, (Map) null, null); if ("Message".equals(evt.path("type").asText())) { System.out.println(evt.path("content").path("text").asText()); } @@ -986,33 +1042,15 @@ Use [webhooks ou o stream de atividades](/xchat/real-time-events) para tráfego + +**Serverless ou multi-instância?** O armazenamento de chaves de assinatura e o cache de chaves vivem na memória da instância do SDK. Quando isso não se encaixa — uma invocação descriptografa, outra envia — passe as chaves explicitamente: `decrypt_events(events, signing_keys)`, `decrypt_event(event_b64, conversation_keys, signing_keys)`, e as sobrescritas `conversation_key`/`conversation_key_version` nos métodos de encrypt. Persista você mesmo as `conversation_keys` retornadas por `decrypt_events` e passe-as de volta. + + Bots completos de poll-and-reply para cada linguagem: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). --- ## Melhores práticas -- Faça cache das chaves de conversa em bruto e das chaves públicas do remetente; atualize em falhas de verificação de assinatura +- Mantenha o armazenamento de chaves de assinatura atualizado: chame `set_signing_keys` novamente com o conjunto completo de participantes quando um remetente registrar uma nova versão de chave, e atualize em falhas de verificação de assinatura - Deduplique entregas ao vivo com `event_uuid` -- Pagine o histórico de eventos até que a paginação esteja completa para não perder metadados de mudança de chave -- Não registre códigos de acesso, chaves privadas ou texto simples de mensagens em produção -- Em apps web, mantenha tokens OAuth (e a emissão do token do realm de backup de chaves) em um servidor; prefira manter chaves privadas apenas no Chat XDK do cliente - ---- - -## Próximos passos - - - - Métodos e tipos para todos os bindings de linguagem - - - Imagens criptografadas e anexos de arquivos - - - Conversas com múltiplos participantes e metadados - - - Entrega via webhooks e atividade - - diff --git a/pt/xchat/groups.mdx b/pt/xchat/groups.mdx index ce0a89213..3666160c8 100644 --- a/pt/xchat/groups.mdx +++ b/pt/xchat/groups.mdx @@ -1,7 +1,7 @@ --- title: Conversas em grupo sidebarTitle: Grupos -description: Crie conversas em grupo no X Chat com vários participantes, chaves de conversa compartilhadas, títulos criptografados e mensagens assinadas. +description: Crie conversas em grupo no X Chat com chaves de conversa compartilhadas, títulos criptografados, gestão de membros e mensagens assinadas. keywords: ["X Chat groups", "group DM", "conversation keys", "group name encryption"] --- @@ -29,17 +29,18 @@ A criptografia ainda é: **Chat XDK** para chaves e payloads; **X API** para cri 1. Emita o ID do grupo com `POST /2/chat/conversations/group/initialize` — o `data.conversation_id` da resposta é o ID prefixado com g que você usa em todos os passos a seguir. 2. Carregue a chave pública de identidade e o `public_key_version` de cada membro (rotas `GET` de chave pública em **Chaves de criptografia**; [`GET /2/users/public_keys`](/x-api/users/get-public-keys-for-multiple-users) busca vários usuários em uma única requisição). Verifique cada registro com `verify_key_binding` antes de usá-lo (veja o aviso em [Guia de introdução](/xchat/getting-started#4-set-up-conversation-keys)). -3. Execute **`prepare_group_create`** uma vez, com **todos** os membros (incluindo você mesmo), o ID prefixado com g e as listas de IDs de membros/admins. Uma chamada gera a chave de conversa, envelopa-a para cada membro e assina a criação — ela retorna **duas** assinaturas de ação (a mudança de chave de conversa e a criação do grupo). +3. Execute **`prepare_group_create`** uma vez, com **todos** os membros (incluindo você mesmo), o ID prefixado com g e as listas de IDs de membros/admins. Uma chamada gera a chave de conversa, envelopa-a para cada membro e assina a criação com a identidade da sessão de `set_identity` — ela retorna **duas** assinaturas de ação (a mudança de chave de conversa e a criação do grupo). 4. `POST /2/chat/conversations/group` com os members/admins do grupo, `conversation_key_version`, `conversation_participant_keys` (SDK **`encrypted_key`** → API **`encrypted_conversation_key`**) e **ambas** as `action_signatures`. Falhas de validação retornam mensagens estáveis e legíveis por humanos, por exemplo `"Too many members: adding these members would exceed the allowed group size."` ou `"Cannot add all members: one or more of the requested members cannot be added to this conversation."`. 5. Mantenha a chave de conversa **em bruto** e a **versão** para criptografar/descriptografar. -O `title` e o `avatar_url` que você passa para `prepare_group_create` são assinados e embutidos literalmente no evento de criação do grupo, e o servidor os compara com a requisição — portanto os valores `group_name` / `group_avatar_url` no corpo do POST precisam ser **byte a byte idênticos** ao que você passou para o SDK, ou a chamada falha na validação de assinatura. +`prepare_group_create` assina o `title` e o `avatar_url` que você passa e os embute literalmente no evento de criação do grupo. O servidor os compara com sua requisição, portanto os valores `group_name` / `group_avatar_url` no corpo do POST precisam ser **byte a byte idênticos** ao que você passou para o SDK — caso contrário, a chamada falha na validação de assinatura. ```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", ) @@ -50,8 +51,9 @@ O `title` e o `avatar_url` que você passa para `prepare_group_create` são assi ```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', }); @@ -60,9 +62,9 @@ O `title` e o `avatar_url` que você passa para `prepare_group_create` são assi ```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)?; @@ -71,8 +73,8 @@ O `title` e o `avatar_url` que você passa para `prepare_group_create` são assi ```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", }) @@ -83,23 +85,20 @@ O `title` e o `avatar_url` que você passa para `prepare_group_create` são assi ```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 ``` ```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 @@ -180,6 +179,17 @@ Enviar e receber em um grupo é igual ao 1:1 uma vez que você tem a chave de co Sempre criptografe com a versão de chave **mais recente** após uma rotação motivada por mudança de composição. +### Mudanças de chave de membros que saíram + +Os eventos de mudança de chave de um grupo são assinados por quem os executou — frequentemente o criador ou um admin. Se esse membro depois **sair do grupo** (ou desativar a conta), os endpoints de chaves públicas param de retornar as chaves dele, então o caminho de descriptografia verificada (`decrypt_events` com chaves de assinatura) falha nesses eventos de mudança de chave com `signature missing or no matching signing key`. Os eventos não estão corrompidos; o material de verificação simplesmente não é mais servido. + +Grupos de longa duração devem prever isso e recorrer a **`extract_conversation_keys`** para eventos de mudança de chave que não podem ser verificados. Esse caminho ignora a verificação de assinatura e recupera a chave de conversa descriptografando-a com sua chave de identidade. O modelo de segurança se mantém porque: + +- Apenas o material de chave que foi **criptografado para sua chave de identidade** pode ser recuperado — um terceiro não pode injetar uma chave que você consiga ler +- Cada **mensagem** ainda é verificada por assinatura contra seu próprio remetente, então a autoria das mensagens não é afetada + +Mantenha o caminho verificado em primeiro lugar: use `decrypt_events` (que também alimenta o cache de chaves do SDK quando `set_cache_keys(true)` está habilitado) e recorra a `extract_conversation_keys` apenas para os eventos de mudança de chave que ele rejeita. + --- ## Checklist diff --git a/pt/xchat/media.mdx b/pt/xchat/media.mdx index e7678a06a..bdcb410c5 100644 --- a/pt/xchat/media.mdx +++ b/pt/xchat/media.mdx @@ -122,23 +122,19 @@ Use os corpos de requisição nas páginas OpenAPI em **Referência da API → M ## Enviar com um anexo -Criptografe com um anexo de mídia e depois faça POST do corpo de envio de mensagem (mesmo mapeamento de campos do [Guia de introdução](/xchat/getting-started#5-send-a-message)). +Criptografe com um anexo de mídia e depois faça POST do corpo de envio de mensagem (mesmo mapeamento de campos do [Guia de introdução](/xchat/getting-started#5-send-a-message)). O SDK gera o `message_id` e o retorna no payload — envie esse valor e reutilize o mesmo payload em reenvios para que um ID nunca seja cunhado duas vezes. ```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, @@ -151,7 +147,7 @@ Criptografe com um anexo de mídia e depois faça POST do corpo de envio de mens 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, ), @@ -160,26 +156,23 @@ Criptografe com um anexo de mídia e depois faça POST do corpo de envio de mens ```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, }); @@ -187,10 +180,23 @@ Criptografe com um anexo de mídia e depois faça POST do corpo de envio de mens ```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, }); @@ -203,10 +209,12 @@ Criptografe com um anexo de mídia e depois faça POST do corpo de envio de mens ```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, @@ -216,42 +224,44 @@ Criptografe com um anexo de mídia e depois faça POST do corpo de envio de mens 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 ``` ```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 ``` ```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 ``` +O par de chave de conversa pode ser omitido completamente: com `set_cache_keys(true)` habilitado, `encrypt_message` resolve a chave e a versão a partir da última mudança de chave verificada da conversa (veja [Guia de introdução](/xchat/getting-started)). + --- ## Baixar e descriptografar diff --git a/pt/xchat/real-time-events.mdx b/pt/xchat/real-time-events.mdx index 9e9ab0ab0..e1673b4e8 100644 --- a/pt/xchat/real-time-events.mdx +++ b/pt/xchat/real-time-events.mdx @@ -10,7 +10,7 @@ O X entrega **`chat.received`**, **`chat.sent`** e atividades relacionadas do X |:-------|:------| | **X Activity API** | `GET /2/activity/stream`; `POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions` (veja segurança OpenAPI por operação) | | **Webhooks** | Rotas opcionais `POST` / `GET` `/2/webhooks` e `PUT` / `DELETE` `/2/webhooks/{webhook_id}` se você terminar em sua própria URL HTTPS | -| **Chat XDK** | `extract_conversation_keys`, `decrypt_event` / `decrypt_events` | +| **Chat XDK** | `decrypt_event` / `decrypt_events`, com os armazenamentos de sessão `set_signing_keys` / `set_cache_keys` | Tipos de eventos privados do X Chat requerem autorização para o usuário que você monitora. Anexos de arquivos criptografados do X Chat usam **`media_hash_key`** e o download de mídia do X Chat — não `expansions=attachments.media_keys` / `media.fields=variants` da Post API. @@ -86,118 +86,76 @@ Se você usa webhooks, responda aos Challenge-Response Checks (GET `crc_token`) ## 3. Descriptografar com o Chat XDK -Campos ao vivo: **`payload.encoded_event`**, opcional **`payload.conversation_key_change_event`**. Deduplique por **`event_uuid`**. +Campos ao vivo: **`payload.encoded_event`**, opcional **`payload.conversation_key_change_event`**. Deduplique entregas por **`event_uuid`**; deduplique mensagens pelo **`message_id`** carregado no evento descriptografado — ele faz parte do conteúdo assinado, enquanto os sequence ids são metadados atribuídos pelo backend e não assinados. + +Os snippets abaixo usam os dois armazenamentos de sessão **opcionais** para o handler mais curto: `set_signing_keys` mantém as chaves públicas dos participantes (buscadas uma vez do [endpoint de chaves públicas](/x-api/chat/get-user-public-keys)), e `set_cache_keys(true)` mantém a chave verificada de cada conversa, para que `decrypt_event` precise apenas do evento. Quando um payload carrega `conversation_key_change_event`, passe-o antes por `decrypt_events`: isso verifica a mudança de chave e, com o cache ativo, retém sua chave para a chamada de `decrypt_event`. Prefere não manter estado na instância? Passe as chaves por chamada — veja a nota no final desta seção. JavaScript usa tipos de evento em camelCase (`message`); outros bindings usam `"Message"` e campos snake_case. ```python - from chat_xdk import Chat - - chat = Chat(JUICEBOX_CONFIG_JSON) - chat.unlock("YOUR_PASSCODE") - chat.set_key_version(SIGNING_KEY_VERSION) - conversation_keys = {} - - def signing_keys(user_id: str): - resp = api_client.chat.get_user_public_keys( - user_id, - public_key_fields=[ - "public_key_version", "public_key", "signing_public_key", "identity_public_key_signature", - ], - ) - return [ - { - "user_id": user_id, - "public_key_version": r["public_key_version"], - "public_key": r["signing_public_key"], - "identity_public_key": r["public_key"], - "identity_public_key_signature": r["identity_public_key_signature"], - } - for r in resp.data - ] + # chat has keys loaded and set_identity called (see Getting Started) + chat.set_cache_keys(True) + chat.set_signing_keys(participant_signing_keys) # all participants, from the public-key routes data = body.get("data") or {} if data.get("event_type") in ("chat.received", "chat.sent"): p = data.get("payload") or {} - cid = p.get("conversation_id") if p.get("conversation_key_change_event"): - conversation_keys[cid] = chat.extract_conversation_keys( - [p["conversation_key_change_event"]] - )["keys"] - ev = chat.decrypt_event( - p["encoded_event"], - conversation_keys.get(cid, {}), - signing_keys(p["sender_id"]), - ) + # Verify the key change and retain its key in the cache + chat.decrypt_events([p["conversation_key_change_event"]]) + ev = chat.decrypt_event(p["encoded_event"]) + if ev["type"] == "Message": + print(ev["sender_id"], ev["content"]["text"]) ``` ```typescript - import { createChat } from '@xdevplatform/chat-xdk'; - - const chat = await createChat({ - juiceboxConfig: JUICEBOX_CONFIG_JSON, - getAuthToken: async (realmId) => getRealmToken(realmId), - }); - await chat.unlock('YOUR_PASSCODE'); - chat.setKeyVersion(SIGNING_KEY_VERSION); - const conversationKeys = new Map>(); - - async function signingKeys(userId: string) { - const resp = await apiClient.chat.getUserPublicKeys(userId, { - publicKeyFields: [ - 'public_key_version', 'public_key', 'signing_public_key', 'identity_public_key_signature', - ], - }); - return resp.data.map((r: any) => ({ - userId, - publicKeyVersion: r.public_key_version, - publicKey: r.signing_public_key, - identityPublicKey: r.public_key, - identityPublicKeySignature: r.identity_public_key_signature, - })); - } + // chat has keys loaded and setIdentity called (see Getting Started) + chat.setCacheKeys(true); + chat.setSigningKeys(participantSigningKeys); // all participants, from the public-key routes const data = body?.data ?? {}; if (data.event_type === 'chat.received' || data.event_type === 'chat.sent') { const p = data.payload ?? {}; - const cid = p.conversation_id as string; if (p.conversation_key_change_event) { - conversationKeys.set( - cid, - chat.extractConversationKeys([p.conversation_key_change_event]).keys, - ); + // Verify the key change and retain its key in the cache + chat.decryptEvents([p.conversation_key_change_event]); + } + const ev = chat.decryptEvent(p.encoded_event); + if (ev.type === 'message') { + console.log(ev.senderId, ev.content.text); } - const ev = chat.decryptEvent( - p.encoded_event, - conversationKeys.get(cid) ?? {}, - await signingKeys(p.sender_id), - ); } ``` ```rust - // chat: ChatCore or Chat, already unlocked / keys imported + // chat has keys loaded and set_identity called (see Getting Started) + chat.set_cache_keys(true); + chat.set_signing_keys(participant_signing_keys); // all participants + if let Some(kc) = key_change.as_deref() { - let extracted = chat.extract_conversation_keys(&[kc]); - conv_keys.extend(extracted.keys); + // Verify the key change and retain its key in the cache + let _ = chat.decrypt_events(&[kc], &[]); } - // sender_signing_keys from GET /2/users/{sender_id}/public_keys - let event = chat.decrypt_event(&encoded_event, &conv_keys, &sender_signing_keys)?; + // Decrypt with the cached conversation key; verify against the stored signing keys + let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?; ``` ```go + // chat has keys loaded and SetIdentity called (see Getting Started) + chat.SetCacheKeys(true) + _ = chat.SetSigningKeys(participantSigningKeys) // all participants + if keyChange != "" { - extracted, _ := chat.ExtractConversationKeys([]string{keyChange}) - for v, k := range extracted.Keys { - convKeys[v] = k - } + // Verify the key change and retain its key in the cache + _, _ = chat.DecryptEvents([]string{keyChange}, nil) } - event, err := chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys) + // Decrypt with the cached conversation key; verify against the stored signing keys + event, err := chat.DecryptEvent(encodedEvent, nil, nil) if err == nil && event.Type == "Message" { fmt.Println(event.AsMessage().Text()) } @@ -205,24 +163,33 @@ JavaScript usa tipos de evento em camelCase (`message`); outros bindings usam `" ```csharp + // chat has keys loaded and SetIdentity called (see Getting Started) + chat.SetCacheKeys(true); + chat.SetSigningKeys(participantSigningKeys); // all participants + if (!string.IsNullOrEmpty(keyChangeB64)) { - var extracted = chat.ExtractConversationKeys(new[] { keyChangeB64 }); - foreach (var kv in extracted.Keys) - convKeys[kv.Key] = kv.Value; + // Verify the key change and retain its key in the cache + chat.DecryptEvents(new[] { keyChangeB64 }); } - var evt = chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys); + // Decrypt with the cached conversation key; verify against the stored signing keys + var evt = chat.DecryptEvent(encodedEvent); if (evt.GetProperty("type").GetString() == "Message") Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString()); ``` ```java + // chat has keys loaded and setIdentity called (see Getting Started) + chat.setCacheKeys(true); + chat.setSigningKeys(participantSigningKeys); // all participants + if (keyChangeB64 != null && !keyChangeB64.isEmpty()) { - var extracted = chat.extractConversationKeys(List.of(keyChangeB64)); - convKeys.putAll(extracted.keys); + // Verify the key change and retain its key in the cache + chat.decryptEvents(List.of(keyChangeB64), null); } - JsonNode evt = chat.decryptEvent(encodedEvent, convKeys, senderSigningKeys); + // Decrypt with the cached conversation key; verify against the stored signing keys + JsonNode evt = chat.decryptEvent(encodedEvent, (Map) null, null); if ("Message".equals(evt.path("type").asText())) { System.out.println(evt.path("content").path("text").asText()); } @@ -230,6 +197,8 @@ JavaScript usa tipos de evento em camelCase (`message`); outros bindings usam `" +Para manter os mapas de chaves em suas próprias mãos, `extract_conversation_keys` descriptografa as chaves de `conversation_key_change_event` e `decrypt_event` aceita-as (e as chaves de assinatura do remetente) como argumentos explícitos — um argumento não vazio explícito sempre vence sobre os armazenamentos. + Histórico: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — veja [Guia de introdução](/xchat/getting-started#6-receive-and-decrypt). --- @@ -257,6 +226,6 @@ Histórico: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conve ## Práticas - Verifique assinaturas de webhook conforme os requisitos de cada plataforma -- Faça cache das chaves de conversa e das chaves públicas dos remetentes -- Aplique blobs de mudança de chave antes de descriptografar mensagens dependentes -- Deduplique por `event_uuid` +- Defina os armazenamentos de sessão uma vez: `set_signing_keys` para todos os participantes, `set_cache_keys(true)` para as chaves de conversa +- Aplique blobs de mudança de chave (via `decrypt_events`) antes de descriptografar mensagens dependentes +- Deduplique entregas por `event_uuid` e mensagens pelo `message_id` assinado diff --git a/pt/xchat/troubleshooting.mdx b/pt/xchat/troubleshooting.mdx index 91d4cb4b8..c9c851d44 100644 --- a/pt/xchat/troubleshooting.mdx +++ b/pt/xchat/troubleshooting.mdx @@ -1,7 +1,7 @@ --- title: Solução de problemas sidebarTitle: Solução de problemas -description: Diagnostique problemas comuns de criptografia no X Chat, como erros do Chat XDK, recuperação de backup seguro de chaves e falhas de descriptografia. +description: "Diagnostique problemas de criptografia no X Chat: erros do Chat XDK, recuperação de backup de chaves, falhas de descriptografia e payloads assinados." keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encryption"] --- @@ -62,55 +62,122 @@ Para webhooks, OAuth, códigos de status HTTP e limites de taxa, use a documenta -### Criptografar ou descriptografar falha porque as chaves não foram carregadas +### Criptografar ou descriptografar falha porque as chaves ou a identidade não foram definidas -Carregue as chaves privadas primeiro e depois defina a **versão** de chave pública do seu registro no X. +Carregue as chaves privadas primeiro e depois defina a **identidade da sessão** — seu ID de usuário mais o `public_key_version` do seu registro no X. Os métodos `encrypt_*` e `prepare_*` assinam com ela; chamá-los sem identidade da sessão (e sem uma sobrescrita explícita por chamada) é um erro. ```python chat.unlock(passcode) # or: chat.import_keys(blob) - chat.set_key_version(signing_key_version) + chat.set_identity(my_user_id, signing_key_version) ``` ```typescript await chat.unlock(passcode); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity(myUserId, signingKeyVersion); ``` ```rust chat.import_keys(&blob)?; - chat.set_key_version(&signing_key_version); + chat.set_identity(&my_user_id, &signing_key_version); ``` ```go blob, _ := chatxdk.Base64ToBytes(privateKeysB64) _ = chat.ImportKeys(blob) - chat.SetKeyVersion(signingKeyVersion) + _ = chat.SetIdentity(myUserID, signingKeyVersion) ``` ```csharp chat.ImportKeys(blobBytes); - chat.SetKeyVersion(signingKeyVersion); + chat.SetIdentity(myUserId, signingKeyVersion); ``` ```java chat.importKeys(blobBytes); - chat.setKeyVersion(signingKeyVersion); + chat.setIdentity(myUserId, signingKeyVersion); ``` +### Sua chave pública local nunca corresponde às chaves registradas da conta + +Os clientes frequentemente precisam responder *"a chave neste dispositivo é uma das chaves registradas para esta conta?"* — após uma restauração ou importação, para adotar o `public_key_version` correto, ou para decidir se o onboarding já aconteceu. Comparar a saída de `get_public_keys` do Chat XDK com o campo `public_key` da API **como strings sempre falha**, mesmo para a mesma chave, porque as duas usam codificações diferentes: + +- A **API** armazena e retorna a chave exatamente como o registro fez o upload: a codificação DER (SPKI) — a chave crua atrás de um prefixo fixo de identificador de algoritmo +- O `get_public_keys` do **Chat XDK** retorna apenas a chave crua, sem esse prefixo + +A mesma chave, duas grafias. Para comparar, decodifique ambas de base64 e verifique se os bytes da API **terminam com** os bytes do SDK (bytes idênticos também correspondem, caso ambos os lados algum dia mantenham a mesma codificação): + + + + ```python + import base64 + + def same_key(local_b64: str, server_b64: str) -> bool: + local = base64.b64decode(local_b64) # chat.get_public_keys()["identity"] + server = base64.b64decode(server_b64) # API row's "public_key" + return local == server or (len(server) > len(local) and server.endswith(local)) + ``` + + + ```typescript + const sameKey = (localB64: string, serverB64: string): boolean => { + const local = Buffer.from(localB64, 'base64'); // chat.getPublicKeys().identity + const server = Buffer.from(serverB64, 'base64'); // API row's public_key + return local.equals(server) || + (server.length > local.length && server.subarray(server.length - local.length).equals(local)); + }; + ``` + + + ```rust + fn same_key(local: &[u8], server: &[u8]) -> bool { + local == server || (server.len() > local.len() && server.ends_with(local)) + } + ``` + + + ```go + func sameKey(local, server []byte) bool { + return bytes.Equal(local, server) || + (len(server) > len(local) && bytes.HasSuffix(server, local)) + } + ``` + + + ```csharp + static bool SameKey(byte[] local, byte[] server) => + local.SequenceEqual(server) || + (server.Length > local.Length && + server.AsSpan(server.Length - local.Length).SequenceEqual(local)); + ``` + + + ```java + static boolean sameKey(byte[] local, byte[] server) { + if (Arrays.equals(local, server)) return true; + if (server.length <= local.length) return false; + byte[] tail = Arrays.copyOfRange(server, server.length - local.length, server.length); + return Arrays.equals(tail, local); + } + ``` + + + +Depois de haver correspondência, adote o `public_key_version` daquela linha para `set_identity`. Ao comparar versões (por exemplo, para escolher a chave mais nova), compare **numericamente** — as versões são timestamps em milissegundos com comprimento de string variável, então uma comparação lexicográfica escolhe a errada. + ### Chave de conversa ausente para uma mensagem -Você não tem a chave **em bruto** para o `conversation_key_version` daquela mensagem. +Um erro como `Message encrypted with key version '…' but no matching key found` significa que você não tem a chave **em bruto** para o `conversation_key_version` daquela mensagem. -1. Descriptografe o material de chave a partir de `conversation_key_change_event` (eventos ao vivo) ou `meta.conversation_key_events` (histórico) com `extract_conversation_keys`, **ou** inclua esses blobs em `decrypt_events` +1. Descriptografe o material de chave a partir de `conversation_key_change_event` (eventos ao vivo) ou `meta.conversation_key_events` (histórico) com `extract_conversation_keys`, **ou** inclua esses blobs em `decrypt_events` — com `set_cache_keys(true)` habilitado, `decrypt_events` também retém a chave verificada mais recente de cada conversa para que chamadas posteriores de `decrypt_event` e `encrypt_*` possam omiti-la 2. Confirme que as chaves de conversa foram adicionadas para essa versão e que você ainda é um participante (veja [Guia de introdução](/xchat/getting-started#4-set-up-conversation-keys)) ### O par não tem chaves públicas @@ -132,8 +199,10 @@ Pode ser que ele não tenha concluído o onboarding. Depois que ele se registrar A verificação é **fail-closed por padrão** (`reject_unverified = true`): o SDK já rejeita eventos assinados não verificados, então uma falha aqui significa que as entradas de verificação estão erradas, e não que você precisa ativar a verificação. Causas comuns: - Entrada de chave de assinatura ausente ou incompleta para o **remetente** (todos os campos exigidos pelo Chat XDK — veja a referência do [Chat XDK](/xchat/xchat-xdk)) +- Nenhuma chave de assinatura passada na chamada e nenhuma armazenada via `set_signing_keys` - O remetente rotacionou versões — busque as chaves públicas dele novamente - Uma versão de chave abaixo do piso aceito nunca é verificada +- Em um **evento de mudança de chave de grupo**, quem assinou já saiu do grupo, então suas chaves não são mais servidas — veja [Mudanças de chave de membros que saíram](/xchat/groups#mudanças-de-chave-de-membros-que-saíram) O setter `set_reject_unverified` existe para você **desabilitar** esse padrão (`false`, não recomendado). Se você o desativou antes, restaure o padrão fail-closed: @@ -170,6 +239,10 @@ O setter `set_reject_unverified` existe para você **desabilitar** esse padrão +### Uma resposta carrega `reply_preview_validation: "Invalid"` + +Respostas descriptografadas podem carregar `reply_preview_validation` (`"Valid"` / `"Invalid"`; JavaScript usa `'valid'` / `'invalid'`). `Invalid` significa que a pré-visualização citada dentro da mensagem não corresponde ao evento original assinado que ela embute — trate a citação como não confiável e renderize o conteúdo citado apenas a partir do original validado. A mensagem em si é verificada separadamente e continua autêntica; nada é lançado por uma pré-visualização inválida. + ### Eventos antigos falham permanentemente na verificação Erros como `signature missing or no matching signing key` ou uma incompatibilidade ECDSA em eventos **antigos** são permanentes. Assinaturas são imutáveis e verificadas reconstruindo o payload assinado a partir do próprio evento, então um evento que foi assinado sobre bytes diferentes (ou nunca foi assinado) falha em cada carregamento futuro — nenhuma nova tentativa, atualização de chave ou chamada de API pode curá-lo. Trate esses eventos como lápides, não como erros repetíveis. Rotacionar a chave de conversa inicia um histórico limpo e verificável a partir daquele ponto; novas mensagens não são afetadas. @@ -184,8 +257,8 @@ Estes erros são específicos da criptografia do X Chat (não são erros HTTP ge |:---------|:---------| | Bytes de chave errados | Passe os bytes da chave de conversa **em bruto** para o Chat XDK, não a string de chave criptografada da API | | Nomes de campos JSON errados | Mapeie `encrypted_content` → `encoded_message_create_event` e `encoded_event_signature` → `encoded_message_event_signature` | -| ID de mensagem ausente | Gere `message_id` você mesmo e envie o mesmo valor no corpo da requisição | -| Versão incompatível | Alinhe `conversation_key_version` com a chave usada; alinhe a versão da chave de assinatura com `set_key_version` / seu registro de chave pública | +| ID de mensagem errado | Envie o `message_id` do payload retornado — o SDK o gera e o embute no evento assinado, então qualquer outro valor falha. Em reenvios, reutilize o mesmo payload criptografado para que o ID nunca seja cunhado duas vezes | +| Versão incompatível | Alinhe `conversation_key_version` com a chave usada; alinhe a versão da chave de assinatura passada para `set_identity` com seu registro de chave pública | | Forma do ID no path | Paths de URL ainda precisam do ID de conversa com hífen (`:` → `-`), mas para assinar o SDK aceita qualquer forma: `A:B`, `A-B` (em qualquer ordem), ou apenas o ID de usuário do destinatário — todos são canonicalizados para os mesmos bytes assinados | ### A API retorna 400 para uma chamada que altera estado @@ -210,5 +283,5 @@ Ao investigar falhas de criptografia: - Registre apenas IDs de conversa, IDs de evento e **versões** de chave - **Não** registre texto simples, códigos de acesso, chaves privadas ou blobs de chave completos -- Confirme que `set_key_version` corresponde ao `public_key_version` no seu registro de chave pública +- Confirme que a versão de chave de assinatura passada para `set_identity` corresponde ao `public_key_version` no seu registro de chave pública - Para histórico incompleto, pagine **todas** as páginas de eventos para que os metadados de mudança de chave não sejam pulados antes de descriptografar diff --git a/pt/xchat/xchat-xdk.mdx b/pt/xchat/xchat-xdk.mdx index 247c28522..276bbd7b5 100644 --- a/pt/xchat/xchat-xdk.mdx +++ b/pt/xchat/xchat-xdk.mdx @@ -1,15 +1,15 @@ --- title: Referência do Chat XDK sidebarTitle: Chat XDK -description: Referência do Chat XDK, o SDK de criptografia que gerencia chaves, criptografia, descriptografia e assinaturas do X Chat nas linguagens suportadas. +description: Referência do Chat XDK, o SDK de criptografia que gerencia chaves, criptografia, descriptografia e assinatura para o X Chat nas linguagens suportadas. keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] --- -O **Chat XDK** cuida do gerenciamento de chaves, criptografia, descriptografia e assinatura para o X Chat. Ele **não** chama a X HTTP API — combine-o com o **XDK** [Python](/xdks/python/overview) ou [TypeScript](/xdks/typescript/overview), ou com HTTPS e um token de acesso do usuário. +O **Chat XDK** cuida da gestão de chaves, criptografia, descriptografia e assinatura para o X Chat. Ele **não** chama a X HTTP API — combine-o com o **XDK** [Python](/xdks/python/overview) ou [TypeScript](/xdks/typescript/overview), ou com HTTPS e um token de acesso do usuário. Passo a passo do app: [Guia de introdução](/xchat/getting-started). Bots de exemplo: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). -### Instalação +### Instalar @@ -32,7 +32,7 @@ Passo a passo do app: [Guia de introdução](/xchat/getting-started). Bots de ex [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 @@ Passo a passo do app: [Guia de introdução](/xchat/getting-started). Bots de ex com.x chatxdk - 0.2.1 + 0.4.0 ``` @@ -70,31 +70,37 @@ Passo a passo do app: [Guia de introdução](/xchat/getting-started). Bots de ex ## Início rápido -Descriptografe um backlog, faça cache das chaves, descriptografe um evento, criptografe uma resposta. Conecte o corpo de envio a [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) como em [Guia de introdução](/xchat/getting-started). +Carregue as chaves, defina sua identidade uma vez, descriptografe um backlog, descriptografe um evento ao vivo, criptografe uma mensagem. Conecte o corpo de envio a [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) como em [Guia de introdução](/xchat/getting-started). + +Os snippets usam os dois armazenamentos de sessão **opcionais** para as formas de chamada mais curtas: `set_signing_keys` mantém as chaves públicas dos outros participantes (buscadas do [endpoint de chaves públicas](/x-api/chat/get-user-public-keys)) para que as chamadas de decrypt possam verificar remetentes sem um argumento por chamada, e `set_cache_keys(true)` permite que o SDK memorize a chave verificada de cada conversa para que as chamadas de encrypt precisem apenas do ID da conversa e do texto. Pule qualquer um deles e passe os mesmos valores por chamada — os dois estilos verificam de forma idêntica; veja [Descriptografar](#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 @@ Descriptografe um backlog, faça cache das chaves, descriptografe um evento, cri 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 @@ Descriptografe um backlog, faça cache das chaves, descriptografe um evento, cri 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 @@ Descriptografe um backlog, faça cache das chaves, descriptografe um evento, cri ## Ciclo de vida e chaves -Construa o SDK, armazene chaves privadas (backup seguro de chaves protegido por código de acesso ou blob de chave local), registre as chaves **públicas** com a Chat API e defina sua **versão de chave pública** registrada após unlock ou import. O backup seguro de chaves é implementado com o **Juicebox**, e é por isso que os campos de configuração relacionados carregam esse nome. Chame `generate_keypairs` uma vez por identidade de dispositivo/app; faça POST do payload de registro para o endpoint de chaves públicas. Use `setup` / `unlock` (e helpers de código de acesso relacionados) para o backup seguro de chaves em todos os bindings. `export_keys` / `import_keys` (persistência de blob de chave em bruto para bots e servidores) estão disponíveis **apenas nos bindings nativos** — Python, Go, .NET, JVM e Rust. O binding JS/WASM não expõe exportação nem importação de chave em bruto: em um navegador, qualquer script que alcance a instância poderia exfiltrar a identidade, então o JS mantém as chaves dentro do backup seguro de chaves. Um servidor JS que queira evitar um round-trip ao realm de backup por requisição deve reutilizar uma instância `Chat` desbloqueada entre requisições, ou rodar um binding nativo em que blobs de chave são suportados. +Construa o SDK, armazene as chaves privadas (backup seguro de chaves protegido por código de acesso ou um blob de chave local), registre as chaves **públicas** com a Chat API e chame **`set_identity(user_id, signing_key_version)`** após unlock ou import — isso define o remetente e a versão de chave de assinatura que toda ação assinada usa por padrão, para que os métodos de encrypt e prepare funcionem sem argumentos de identidade por chamada. Chame `generate_keypairs` uma vez por identidade de dispositivo/app; poste o payload de registro para o endpoint de chaves públicas. Use `setup` / `unlock` (e helpers de código de acesso relacionados) para backup seguro de chaves em todos os bindings. `export_keys` / `import_keys` (persistência de blob de chave em bruto para bots e servidores) estão disponíveis **apenas nos bindings nativos** — Python, Go, .NET, JVM e Rust. O binding JS/WASM não expõe exportação ou importação de chaves em bruto: em um navegador, qualquer script que alcance a instância poderia exfiltrar a identidade, então JS mantém as chaves dentro do backup seguro de chaves. Um servidor JS que quer evitar um round-trip ao realm de backup por requisição deve reutilizar uma única instância `Chat` desbloqueada entre requisições, ou rodar um binding nativo onde blobs de chave são suportados. + +O SDK também precisa da versão que a X API reporta para sua chave pública registrada, para que entradas de mudança de chave voltadas a outras versões sejam ignoradas. `set_identity` a registra junto com o ID do usuário; `import_keys` a aceita diretamente como argumento opcional (Rust e Go usam `import_keys_with_version` / `ImportKeysWithVersion`). @@ -217,13 +272,13 @@ Construa o SDK, armazene chaves privadas (backup seguro de chaves protegido por 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 @@ Construa o SDK, armazene chaves privadas (backup seguro de chaves protegido por }); 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 @@ Construa o SDK, armazene chaves privadas (backup seguro de chaves protegido por ```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 @@ Construa o SDK, armazene chaves privadas (backup seguro de chaves protegido por // 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 @@ Construa o SDK, armazene chaves privadas (backup seguro de chaves protegido por ```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 @@ Construa o SDK, armazene chaves privadas (backup seguro de chaves protegido por ```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(); } @@ -295,15 +350,15 @@ Construa o SDK, armazene chaves privadas (backup seguro de chaves protegido por -A configuração do backup seguro de chaves aceita três formatos: o objeto `juicebox_config` da X API (recomendado — passado verbatim), um wrapper `sdk_config` completo, ou um `token_map` puro. +A configuração de backup seguro de chaves aceita três formatos: o objeto `juicebox_config` da X API (recomendado — passado literalmente), um wrapper `sdk_config` completo, ou um `token_map` puro. -Opcional: a verificação de assinatura está **ativa por padrão** (`reject_unverified = true`) — chame `set_reject_unverified(false)` para desativá-la (não recomendado); `update_config` se a configuração do realm de backup mudar; `is_unlocked` / `has_identity_key` para o estado da UI. As listas completas de campos ficam nos stubs do [repositório chat-xdk](https://github.com/xdevplatform/chat-xdk). +Opcional: a verificação de assinatura está **ativa por padrão** (`reject_unverified = true`) — chame `set_reject_unverified(false)` para desativá-la (não recomendado); `update_config` se a configuração do realm de backup mudar; `is_unlocked` / `has_identity_key` para estado de UI. Listas completas de campos vivem nos stubs do [repositório chat-xdk](https://github.com/xdevplatform/chat-xdk). --- ## Chaves de conversa -Três métodos **prepare** fazem cada um uma única chamada que faz tudo o que uma mudança de chave precisa: gerar uma nova chave de conversa, criptografá-la para cada participante (das chaves públicas que você passa) e assinar a mudança. Todos retornam o mesmo formato **`PreparedConversationChange`**, pronto para POST — renomeie o campo do SDK `encrypted_key` para **`encrypted_conversation_key`** em `conversation_participant_keys`, e mapeie as assinaturas de ação para o campo obrigatório **`action_signatures`** no corpo. +Três métodos **prepare** fazem, cada um, com uma chamada tudo o que uma mudança de chave precisa: gerar uma nova chave de conversa, criptografá-la para cada participante (a partir das chaves públicas que você passa) e assinar a mudança. A identidade do remetente e a versão de chave de assinatura vêm da sessão (`set_identity`); defina `sender_id` / `signing_key_version` nos params para sobrescrever. Todos retornam o mesmo formato **`PreparedConversationChange`**, pronto para POST — renomeie o campo do SDK `encrypted_key` para **`encrypted_conversation_key`** em `conversation_participant_keys` e mapeie as assinaturas de ação para o campo obrigatório **`action_signatures`** do corpo. | Cenário | Método | Assinaturas de ação retornadas | |:--------|:-------|:-------------------------------| @@ -314,10 +369,10 @@ Três métodos **prepare** fazem cada um uma única chamada que faz tudo o que u Mantenha os bytes da chave **em bruto** para `encrypt_message` e mídia; nunca passe o envelope criptografado da API para encrypt. -**Verifique as chaves obtidas antes de envelopar.** Os métodos prepare criptografam a nova chave de conversa para quaisquer chaves públicas que você passar. Antes de passá-las, chame `verify_key_binding(identity, signing, signature)` em cada registro obtido — seus campos `public_key`, `signing_public_key` e `identity_public_key_signature` da API de chaves públicas — para que uma chave de identidade substituída não possa receber a chave de conversa. +**Verifique as chaves buscadas antes de envelopar.** Os métodos prepare criptografam a nova chave de conversa para quaisquer chaves públicas que você passar. Antes de passá-las, chame `verify_key_binding(identity, signing, signature)` em cada registro obtido — seus campos `public_key`, `signing_public_key` e `identity_public_key_signature` da API de chaves públicas — para que uma chave de identidade substituída não possa receber a chave de conversa. -Use `extract_conversation_keys` em payloads de eventos de mudança de chave para reconstruir `{ keys, latest_version }`. `decrypt_conversation_key` desencapsula um único blob ECIES. +Use `extract_conversation_keys` em payloads de eventos de mudança de chave para reconstruir `{ keys, latest_version }`. `decrypt_conversation_key` desembrulha um único blob ECIES. @@ -327,7 +382,7 @@ Use `extract_conversation_keys` em payloads de eventos de mudança de chave para # {"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` em payloads de eventos de mudança de chave para ```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` em payloads de eventos de mudança de chave para ```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` em payloads de eventos de mudança de chave para ```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` em payloads de eventos de mudança de chave para ```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` em payloads de eventos de mudança de chave para ```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); @@ -404,15 +452,24 @@ Use `extract_conversation_keys` em payloads de eventos de mudança de chave para -Para criação de grupo e adições de membros, passe os parâmetros que cada método precisa (listas de IDs de membro/admin para `prepare_group_create`; novos mais a lista atual para `prepare_group_members_change`) — veja [Grupos](/xchat/groups#create-the-group-and-establish-keys) para exemplos. Ambos retornam **duas** assinaturas de ação; o POST deve incluir as duas. +Para criação de grupo e adições de membros, passe os params que cada método precisa (listas de IDs de membros/admins para `prepare_group_create`; nova mais lista atual para `prepare_group_members_change`) — veja [Grupos](/xchat/groups#create-the-group-and-establish-keys) para exemplos. Ambos retornam **duas** assinaturas de ação; o POST precisa incluir ambas. --- ## Descriptografar -**`decrypt_events`** é para histórico e backlog: extrai chaves de conversa do stream, retorna mensagens descriptografadas e **coleta** os erros por evento em vez de falhar o batch inteiro. **`decrypt_event`** é para um único evento ao vivo quando você já tem um cache de chaves; lança/throws em falha. +**`decrypt_events`** é para histórico e backlog: puxa chaves de conversa do stream, retorna mensagens descriptografadas e **coleta** erros por evento em vez de falhar o lote inteiro. **`decrypt_event`** é para um único evento ao vivo; lança erro em caso de falha. + +Passe as **chaves de assinatura** para que o SDK possa verificar os remetentes. Mapeie campos de chave pública da API para `SigningKeyEntry`: `public_key_version` → `public_key_version` (mesmo nome), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, mais `identity_public_key_signature` e `user_id`. + +Dois armazenamentos de sessão opt-in permitem omitir os argumentos de chave por chamada: -Passe as **chaves de assinatura** para que o SDK possa verificar remetentes. Mapeie os campos de chave pública da API para `SigningKeyEntry`: `public_key_version` → `public_key_version` (mesmo nome), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, além de `identity_public_key_signature` e `user_id`. A verificação é obrigatória por padrão: omitir ou passar uma lista de chaves de assinatura vazia **não** a pula — eventos assinados falham (coletados em `errors` para `decrypt_events`, lançados para `decrypt_event`). Para realmente pular a verificação, você deve primeiro chamar `set_reject_unverified(false)` (não recomendado em produção). +- **`set_signing_keys(entries)`** armazena as chaves de assinatura dos participantes; uma chamada de decrypt que omite (ou passa vazio) o argumento de signing-keys usa o armazenamento. A verificação em si não muda — as chaves entram no armazenamento apenas por essa chamada, nunca a partir dos eventos sendo descriptografados. Cada chamada substitui o conjunto anterior. +- **`set_cache_keys(true)`** habilita o cache de chaves de conversa (desligado por padrão). Enquanto habilitado, `decrypt_events` faz cache, por conversa, da chave mais recente cuja mudança de chave carregou uma assinatura válida; `decrypt_event` recorre a ela quando seu argumento de chaves de conversa é omitido, e os helpers de encrypt resolvem uma chave de conversa omitida a partir dele. Desabilitar limpa o cache. + +Um argumento explícito não vazio sempre vence sobre os armazenamentos. Argumentos explícitos por chamada continuam sendo de primeira classe — e são a escolha certa para deployments serverless ou multi-instância, onde uma requisição pode cair em uma instância nova cujos armazenamentos estão vazios. + +A verificação é obrigatória por padrão: omitir as chaves de assinatura nunca a pula. Sem nada passado e nada armazenado, eventos assinados falham (coletados em `errors` para `decrypt_events`, lançados para `decrypt_event`). Para efetivamente pular a verificação você precisa primeiro chamar `set_reject_unverified(false)` (não recomendado em produção). @@ -430,11 +487,11 @@ Passe as **chaves de assinatura** para que o SDK possa verificar remetentes. Map 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 @@ Passe as **chaves de assinatura** para que o SDK possa verificar remetentes. Map 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 @@ Passe as **chaves de assinatura** para que o SDK possa verificar remetentes. Map 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 @@ Passe as **chaves de assinatura** para que o SDK possa verificar remetentes. Map 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,48 +539,56 @@ Passe as **chaves de assinatura** para que o SDK possa verificar remetentes. Map 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); ``` --- -## Helpers de criptografar e enviar +## Helpers de criptografia e envio + +**`encrypt_message(conversation_id, text)`** constrói o texto cifrado assinado para uma mensagem de texto; opcionais `entities`, `attachments` (via `media_hash_key`), `should_notify` e `ttl_msec`. A identidade do remetente é resolvida a partir da sessão (`set_identity`) e a chave de conversa a partir do cache de chaves opt-in (`set_cache_keys`) — ou passe `sender_id` / `signing_key_version` e `conversation_key` + `conversation_key_version` explicitamente. O SDK gera o **`message_id`** (um UUID embutido no evento assinado) e o retorna no payload — nunca crie o seu; reutilize o mesmo payload em reenvios para que um ID nunca seja cunhado duas vezes. Mapeie o payload para o corpo de envio: `message_id` → **`message_id`**, `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**. -**`encrypt_message`** constrói o texto cifrado assinado para uma mensagem de texto (entidades opcionais, anexos via `media_hash_key`, TTL, flags de notificação). Mapeie o payload retornado para o corpo de envio de mensagem: `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**, mais seu **`message_id`**. +**Respostas são baseadas em eventos.** `encrypt_reply(conversation_id, text, reply_to_event)` recebe o evento em bruto base64 que está sendo respondido. O SDK deriva dela a pré-visualização citada (sequence id, remetente, texto, entidades, anexos) e embute o original assinado na mensagem de saída para que os destinatários possam validar a citação. Passe `reply_to_ckces` — os eventos brutos de mudança de chave — quando o original tiver sido criptografado sob uma versão de chave mais antiga do que a resposta. Quando o original foi **editado**, passe o evento bruto de edição como `reply_to_edit_event`: a pré-visualização então cita o que a mensagem diz agora (seu texto e entidades vêm da edição), e a edição viaja junto com o original para o destinatário conferir. Os campos explícitos `reply_to_*` continuam disponíveis como sobrescritas para chamadores que já não têm o evento em bruto. -Use **`encrypt_reply`**, **`encrypt_add_reaction`** e **`encrypt_remove_reaction`** para respostas e reações (`sequence_id` aponta para o pai). **`encrypt` / `decrypt`** são para metadados UTF-8 sob a chave de conversa (por exemplo, um nome de grupo criptografado) — não envelopes de mensagem. **`encrypt_stream` / `decrypt_stream`** criptografam bytes de anexo; veja [Mídia](/xchat/media). Os métodos de baixo nível **`sign` / `verify` / `verify_key_binding`** suportam fluxos avançados; mudanças de chave de conversa, criação de grupos e adições de membros são assinadas pelos [métodos prepare](#chaves-de-conversa). +**Reações também são baseadas em eventos.** `encrypt_add_reaction(target_event, emoji)` e `encrypt_remove_reaction(...)` derivam o ID da conversa e o sequence id alvo do evento em bruto que está sendo reagido; os mesmos params podem adicionar e depois remover uma reação. Defina `conversation_id` e `target_message_sequence_id` explicitamente somente quando você não tiver mais o evento em bruto. -O ID de conversa passado para `encrypt_message` / `encrypt_reply` pode ser em qualquer forma que você tenha — `A:B` de eventos, `A-B` de listagens ou paths de URL (em qualquer ordem), ou o ID de usuário do destinatário puro — o SDK o canonicaliza antes de assinar. IDs de grupo (prefixados com `g`) passam sem alteração. +Do lado do recebedor, uma mensagem descriptografada que cita uma resposta carrega **`reply_preview_validation`** (`"Valid"` / `"Invalid"`; o binding JS usa `'valid'` / `'invalid'`): o SDK verificou a assinatura do original embutido contra suas chaves de assinatura — nunca uma chave carregada no evento — o descriptografou e comparou o conteúdo citado e o autor contra ele. Quando a pré-visualização embute um evento de edição, o SDK verifica a edição da mesma forma (mesma conversa, mesmo autor do original) e confere o texto citado contra o conteúdo editado, e não contra o texto pré-edição. O campo está ausente quando a mensagem não carrega pré-visualização ou a pré-visualização não embute um original. Trate pré-visualizações `Invalid` como não confiáveis: a mensagem em si é autêntica, mas o material citado não é — renderize citações apenas a partir do original validado. + +**`encrypt` / `decrypt`** são para metadados UTF-8 sob a chave de conversa (por exemplo, um nome de grupo criptografado) — não envelopes de mensagem. **`encrypt_stream` / `decrypt_stream`** criptografam bytes de anexos; veja [Mídia](/xchat/media). **`sign` / `verify` / `verify_key_binding`** de baixo nível suportam fluxos avançados; mudanças de chave de conversa, criações de grupo e adições de membros são assinadas pelos [métodos prepare](#conversation-keys). + +O ID de conversa passado para `encrypt_message` / `encrypt_reply` pode ser qualquer forma que você tenha — `A:B` de eventos, `A-B` de listagens ou paths de URL (em qualquer ordem), ou apenas o ID de usuário do destinatário — o SDK o canonicaliza antes de assinar. IDs de grupo (prefixados com `g`) passam sem alteração. ```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 @@ O ID de conversa passado para `encrypt_message` / `encrypt_reply` pode ser em qu ```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 @@ O ID de conversa passado para `encrypt_message` / `encrypt_reply` pode ser em qu ```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); @@ -611,7 +726,7 @@ O ID de conversa passado para `encrypt_message` / `encrypt_reply` pode ser em qu ## Streams de mídia -Criptografe bytes de arquivo com a **mesma** chave de conversa usada para texto, envie via APIs de mídia do Chat e anexe **`media_hash_key`** em `encrypt_message`. Isto não é o modelo de mídia dos Posts (`expansions=attachments.media_keys`). Fluxo completo de upload/download: [Mídia](/xchat/media). +Criptografe bytes de arquivo com a **mesma** chave de conversa usada para texto, faça upload via APIs de mídia do Chat e anexe **`media_hash_key`** em `encrypt_message`. Este não é o modelo de mídia de Posts (`expansions=attachments.media_keys`). Fluxo completo de upload/download: [Mídia](/xchat/media). @@ -661,10 +776,10 @@ Criptografe bytes de arquivo com a **mesma** chave de conversa usada para texto, ### Streaming incremental para mídia grande -Para arquivos grandes, evite manter todo o payload em memória: `stream_encryptor()` / `stream_decryptor()` retornam um `StreamEncryptor` / `StreamDecryptor` que você alimenta em chunks (cerca de 1 MB cada) com `push(chunk)`, e depois chama `finish()` uma vez ao final. Na descriptografia, `finish()` detecta um stream truncado (falha se a entrada terminar antes do frame final), então não trate o texto simples enviado como completo até que ele tenha sucesso. +Para arquivos grandes, evite manter o payload inteiro em memória: `stream_encryptor()` / `stream_decryptor()` retornam um `StreamEncryptor` / `StreamDecryptor` que você alimenta em chunks (cerca de 1 MB cada) com `push(chunk)`, e depois chama `finish()` uma vez ao final. Na descriptografia, `finish()` detecta um stream truncado (falha se a entrada terminou antes do frame final), então não trate texto simples enviado com `push` como completo até que ele seja bem-sucedido. -**Apenas JS/WASM:** `finish()` consome e libera o objeto WASM subjacente — nunca chame `free()` após `finish()` (ele lança exceção). Chame `free()` apenas para abandonar um stream *antes* de finalizar (por exemplo, em um caminho de erro). +**Apenas JS/WASM:** `finish()` consome e libera o objeto WASM subjacente — nunca chame `free()` após `finish()` (ele lança). Chame `free()` apenas para abandonar um stream *antes* de finalizar (por exemplo, em uma rota de erro). @@ -701,7 +816,7 @@ Para arquivos grandes, evite manter todo o payload em memória: `stream_encrypto ## Utilitários -Helpers de Base64/hex, detecção de MIME e dimensões de imagem estão disponíveis como funções de nível de módulo (Python/JS/Rust/Go) ou `ChatXdkUtilities` (C#/Java) — úteis ao construir metadados de anexos sem trazer bibliotecas adicionais. +Helpers de base64/hex, detecção de MIME e dimensões de imagem estão disponíveis como funções de nível de módulo (Python/JS/Rust/Go) ou `ChatXdkUtilities` (C#/Java) — úteis ao construir metadados de anexos sem depender de bibliotecas extras. @@ -792,23 +907,23 @@ Helpers de Base64/hex, detecção de MIME e dimensões de imagem estão disponí ## Tipos importantes -Estes tipos conceituais aparecem em todas as linguagens (os nomes exatos dos campos diferem; JS frequentemente usa discriminadores de evento em camelCase como `message`): +Estes tipos conceituais aparecem entre linguagens (nomes exatos de campos diferem; JS usa frequentemente discriminadores de evento em camelCase como `message`): -- **SendPayload** — valor de retorno de `encrypt_message` e helpers de criptografia relacionados; mapeie para o corpo de envio da Chat API. -- **PublicKeyRegistrationPayload** — saída de `generate_keypairs` / getters de chave pública para a API de adicionar chave pública. -- **SigningKeyEntry** — material público do remetente passado para descriptografar, para verificação de assinatura. -- **PreparedConversationChange** — saída dos três métodos prepare: o `conversation_id` derivado ou passado, os bytes da `conversation_key` em bruto, `conversation_key_version`, `participant_keys` (`user_id`, `encrypted_key`, `public_key_version`) e `action_signatures` (`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, opcional `signature_payload` — omitido em assinaturas de mudança de chave porque esse payload embute a chave em texto simples). -- **DecryptEventsResult** — mensagens, erros opcionais e `conversation_keys` extraídas. +- **SendPayload** — valor de retorno de `encrypt_message` e dos outros helpers de encrypt: o **`message_id`** gerado pelo SDK (um UUID embutido no evento assinado — envie-o como `message_id` da mensagem e guarde-o para deduplicação), `encrypted_content`, `encoded_event_signature`, metadados de assinatura, `conversation_key_version` e `should_notify`. Mapeie para o corpo de envio da Chat API. +- **PublicKeyRegistrationPayload** — saída de `generate_keypairs` / getters de chave pública para a API add-public-key. +- **SigningKeyEntry** — material público do remetente passado para decrypt para verificação de assinatura, ou armazenado via `set_signing_keys`. +- **PreparedConversationChange** — saída dos três métodos prepare: o `conversation_id` derivado ou passado, os bytes brutos de `conversation_key`, `conversation_key_version`, `participant_keys` (`user_id`, `encrypted_key`, `public_key_version`) e `action_signatures` (`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, opcional `signature_payload` — omitido em assinaturas de mudança de chave porque esse payload embute a chave em texto simples). +- **DecryptEventsResult** — mensagens, erros opcionais e `conversation_keys` extraídas. Mensagens descriptografadas que citam uma resposta carregam `reply_preview_validation` (veja [Helpers de criptografia e envio](#encrypt-and-send-helpers)). -Para listas de campos completas, use stubs de linguagem no [repositório chat-xdk](https://github.com/xdevplatform/chat-xdk) (`docs/API.md`, `*.pyi`, `index.d.ts`). +Para listas completas de campos, use os stubs de linguagem do [repositório chat-xdk](https://github.com/xdevplatform/chat-xdk) (`docs/API.md`, `*.pyi`, `index.d.ts`). --- ## Erros -Python normalmente lança **`ValueError`** com uma mensagem descritiva (por exemplo, um código de acesso inválido). TypeScript/JavaScript lança **`Error`**. Go retorna `(value, error)`. Prefira **`decrypt_events`** para histórico para que um evento ruim não aborte o batch; inspecione a coleção de erros para falhas parciais. +Python normalmente lança **`ValueError`** com uma mensagem descritiva (por exemplo, um código de acesso inválido). TypeScript/JavaScript lança **`Error`**. Go retorna `(value, error)`. Prefira **`decrypt_events`** para histórico para que um evento ruim não aborte o lote; inspecione a coleção de erros para falhas parciais. -Alguns erros de verificação são **permanentes**. Assinaturas são imutáveis e verificadas reconstruindo o payload assinado a partir do próprio evento, então um evento antigo que falha com `signature missing or no matching signing key` ou uma incompatibilidade ECDSA falhará em cada carregamento futuro — nenhuma nova tentativa, atualização de chave ou chamada de API pode curá-lo. Trate esses como lápides, não como erros transitórios. Rotacionar a chave de conversa inicia um histórico limpo e verificável a partir daquele ponto. +Alguns erros de verificação são **permanentes**. Assinaturas são imutáveis e verificadas reconstruindo o payload assinado a partir do próprio evento, então um evento antigo que falha com `signature missing or no matching signing key` ou uma incompatibilidade ECDSA falhará em cada carregamento futuro — nenhuma nova tentativa, atualização de chave ou chamada de API pode curá-lo. Trate-os como lápides, não como erros transitórios. Rotacionar a chave de conversa inicia um histórico limpo e verificável a partir daquele ponto. --- @@ -819,10 +934,10 @@ Alguns erros de verificação são **permanentes**. Assinaturas são imutáveis Conecte o Chat XDK à Chat API - Criptografia de stream e REST de mídia + Criptografia de streams e REST de mídia - Entrega via webhooks e atividade + Webhooks e entrega por atividade Falhas comuns