From df5d5c6bd2b3849e5be6084b2e86717d723cc0f5 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:21:13 +0000 Subject: [PATCH] docs(xchat): translate v0.4.0 Chat XDK pages to ja/es/pt/ko --- es/xchat/cryptography-primer.mdx | 166 +++++----- es/xchat/getting-started.mdx | 446 ++++++++++++++------------- es/xchat/groups.mdx | 103 ++++--- es/xchat/media.mdx | 116 +++---- es/xchat/real-time-events.mdx | 193 +++++------- es/xchat/troubleshooting.mdx | 97 +++--- es/xchat/xchat-xdk.mdx | 495 ++++++++++++++++++------------ ja/xchat/cryptography-primer.mdx | 258 ++++++++-------- ja/xchat/getting-started.mdx | 450 ++++++++++++++------------- ja/xchat/groups.mdx | 103 ++++--- ja/xchat/media.mdx | 128 ++++---- ja/xchat/real-time-events.mdx | 191 +++++------- ja/xchat/troubleshooting.mdx | 99 +++--- ja/xchat/xchat-xdk.mdx | 499 ++++++++++++++++++------------ ko/xchat/cryptography-primer.mdx | 216 ++++++------- ko/xchat/getting-started.mdx | 442 +++++++++++++++------------ ko/xchat/groups.mdx | 99 +++--- ko/xchat/media.mdx | 126 ++++---- ko/xchat/real-time-events.mdx | 197 +++++------- ko/xchat/troubleshooting.mdx | 93 +++--- ko/xchat/xchat-xdk.mdx | 509 +++++++++++++++++++------------ pt/xchat/cryptography-primer.mdx | 168 +++++----- pt/xchat/getting-started.mdx | 452 ++++++++++++++------------- pt/xchat/groups.mdx | 93 +++--- pt/xchat/media.mdx | 124 ++++---- pt/xchat/real-time-events.mdx | 185 +++++------ pt/xchat/troubleshooting.mdx | 89 +++--- pt/xchat/xchat-xdk.mdx | 495 ++++++++++++++++++------------ 28 files changed, 3592 insertions(+), 3040 deletions(-) diff --git a/es/xchat/cryptography-primer.mdx b/es/xchat/cryptography-primer.mdx index 33d391594..cc2c53ef3 100644 --- a/es/xchat/cryptography-primer.mdx +++ b/es/xchat/cryptography-primer.mdx @@ -1,15 +1,15 @@ --- -title: Manual básico de criptografía -sidebarTitle: Manual básico de criptografía -description: Aprende los conceptos de ECDH, cifrado con clave pública y firmas digitales detrás del cifrado de extremo a extremo de X Chat, sin implementación. +title: Introducción a la criptografía +sidebarTitle: Introducción a la criptografía +description: Aprende los conceptos de ECDH, cifrado de clave pública y firmas digitales detrás del cifrado de extremo a extremo de X Chat sin detalles de implementación. keywords: ["X Chat cryptography", "E2EE primer", "encryption basics", "public key encryption", "ECDH", "digital signatures", "conversation keys"] --- import { Button } from '/snippets/button.mdx'; -Este manual explica las ideas criptográficas detrás de X Chat a nivel conceptual. No necesitas esta profundidad para construir—el [Chat XDK](/xchat/xchat-xdk) realiza el cifrado, descifrado, firma y almacenamiento de claves por ti—pero el modelo mental ayuda cuando diseñas tu app o depuras su comportamiento. +Esta introducción explica las ideas criptográficas detrás de X Chat a nivel conceptual. No necesitas esta profundidad para construir—el [Chat XDK](/es/xchat/xchat-xdk) realiza el cifrado, descifrado, firma y almacenamiento de claves por ti—pero el modelo mental ayuda cuando diseñas tu app o depuras su comportamiento. -Cuando estés listo para implementar, usa [Primeros pasos](/xchat/getting-started) para un recorrido completo y la [referencia de la API](/x-api/chat/get-chat-conversations) en la barra lateral para rutas individuales. +Cuando estés listo para implementarlo, usa [Primeros pasos](/es/xchat/getting-started) para un recorrido completo y la [referencia de la API](/x-api/chat/get-chat-conversations) en la barra lateral para las rutas individuales. **Tú no implementas esta criptografía por tu cuenta.** El Chat XDK se encarga. Esta página es para entender, no una lista de verificación de la API. @@ -22,10 +22,10 @@ Cuando estés listo para implementar, usa [Primeros pasos](/xchat/getting-starte X Chat usa un sistema de cifrado por capas donde: 1. Los **mensajes** se cifran con una **clave de conversación** (cifrado simétrico rápido) -2. Las **claves de conversación** se cifran para cada participante usando su **clave pública de identidad** (intercambio asimétrico de claves) +2. Las **claves de conversación** se cifran para cada participante usando su **clave pública de identidad** (intercambio de claves asimétrico) 3. Los **mensajes se firman** con la **clave de firma** para que los destinatarios puedan verificar quién los envió y que nada fue alterado -El cifrado simétrico es eficiente para mucho tráfico de mensajes; el cifrado asimétrico se usa principalmente para **distribuir** las claves de conversación de forma segura. +El cifrado simétrico es eficiente para grandes volúmenes de tráfico de mensajes; el cifrado asimétrico se usa principalmente para **distribuir** claves de conversación de manera segura. ```mermaid flowchart TB @@ -45,50 +45,50 @@ flowchart TB end ``` -En el flujo del producto, X transporta **texto cifrado y sobres de claves**—no contenido legible de mensajes ni la clave de conversación en bruto. Tu app usa el Chat XDK para la criptografía y la [Chat API](/xchat/introduction) (mediante el XDK en Python/TypeScript, o HTTPS) para registrar claves y enviar o recibir esas cargas cifradas. Consulta [Primeros pasos](/xchat/getting-started) para ver cómo encajan esas piezas. +En el flujo del producto, X transporta **texto cifrado y sobres de clave**—no contenido legible del mensaje ni la clave de conversación en bruto. Tu app usa el Chat XDK para la criptografía y la [Chat API](/es/xchat/introduction) (a través del XDK en Python/TypeScript, o HTTPS) para registrar claves y enviar o recibir esos payloads cifrados. Consulta [Primeros pasos](/es/xchat/getting-started) para ver cómo encajan estas piezas. --- ## Tipos de claves explicados -X Chat usa tres tipos de material de claves, cada uno con un propósito específico. +X Chat usa tres tipos de material de clave, cada uno con un propósito específico. ### 1. Par de claves de identidad -**Propósito:** Intercambiar de forma segura claves de conversación entre usuarios +**Propósito:** Intercambiar de manera segura claves de conversación entre usuarios | Componente | Descripción | -|:-----------|:------------| +|:----------|:------------| | **Clave pública de identidad** | Se comparte con otros; se usa para cifrar claves de conversación *dirigidas a* ti | -| **Clave privada de identidad** | Se mantiene en secreto; se usa para descifrar las claves de conversación enviadas *a* ti | +| **Clave privada de identidad** | Se mantiene en secreto; se usa para descifrar claves de conversación enviadas *a* ti | -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. +Cuando alguien te añade 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 se descubren a través de las APIs de **public-key** de la plataforma (consulta Claves de cifrado en la referencia de la API). Las mitades privadas permanecen en el Chat XDK (por ejemplo, mediante [copia de seguridad segura de claves](#secure-key-backup-distributed-key-storage) o un blob de claves cuidadosamente protegido). ### 2. Par de claves de firma -**Propósito:** Demostrar que fuiste tú quien creó un mensaje +**Propósito:** Demostrar que fuiste el autor de un mensaje | Componente | Descripción | -|:-----------|:------------| +|:----------|:------------| | **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 lo verifican usando tu clave pública de firma (también publicada a través de las APIs de public-key). 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 -**Propósito:** Cifrar y descifrar mensajes (y multimedia) dentro de una conversación específica +**Propósito:** Cifrar y descifrar mensajes (y [contenido multimedia](/es/xchat/media)) dentro de una conversación específica | Propiedad | Descripción | -|:----------|:------------| +|:---------|:------------| | **Simétrica** | La misma clave cifra y descifra | | **Por conversación** | Cada conversación tiene su propia clave | -| **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 | +| **Compartida entre participantes** | Todos los participantes que deban leer la conversación tienen una copia | +| **Versionada** | Las claves pueden rotarse; las apps deben rastrear 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, generada con su clave pública de identidad. Después de descifrar tu copia una vez, guardas la clave de conversación en **bruto** y la usas para el cifrado rápido de mensajes (y [contenido multimedia](/es/xchat/media)). La configuración de esas copias para una conversación se realiza mediante el Chat XDK junto con los endpoints de **key** de conversación—se recorre en [Primeros pasos](/es/xchat/getting-started#4-set-up-conversation-keys). --- @@ -101,16 +101,16 @@ Las claves de conversación se generan cuando se configura una conversación o c Escribes: "Hola, ¿cómo estás?" - Tu app usa la clave de conversación en bruto para este chat (obtenida de la configuración o de un evento previo de distribución de claves), para la versión correcta. + Tu app usa la clave de conversación en bruto para este chat (de la configuración o de un evento anterior de distribución de claves), para la versión de clave correcta. - El Chat XDK cifra tu mensaje con la clave de conversación. El resultado es texto cifrado inútil sin esa clave. + El Chat XDK cifra tu mensaje con la clave de conversación. El resultado es texto cifrado que es inútil sin esa clave. - El Chat XDK firma la carga cifrada con tu clave privada de firma, demostrando que tú creaste exactamente ese contenido. + El Chat XDK firma el payload cifrado con tu clave privada de firma, demostrando que fuiste el autor de este contenido exacto. - Tu app envía la carga cifrada y la firma a X a través del endpoint **send message** de la Chat API. X almacena y entrega bytes que no puede leer como texto plano. + Tu app envía el payload cifrado y la firma a X a través del endpoint **send message** de la Chat API. X almacena y entrega bytes que no puede leer como texto plano. @@ -118,73 +118,73 @@ Las claves de conversación se generan cuando se configura una conversación o c - Tu app recibe texto cifrado desde X—vía [webhooks o un activity stream](/xchat/real-time-events), o leyendo los **events** de la conversación para el historial. + Tu app recibe texto cifrado de X—a través de [webhooks o un activity stream](/es/xchat/real-time-events), o al leer **events** de conversación para el historial. - Usa tu clave en bruto en caché, u obtenla descifrando tu copia desde un evento de distribución de claves (cambio de clave) si es nueva o rotada. + Usa tu clave en bruto en caché, u obténla descifrando tu copia desde un evento de distribución de claves (cambio de clave) si es nueva o rotada. - El Chat XDK comprueba la firma usando la clave pública de firma del remitente (y el vínculo de identidad correspondiente), de modo que sepas quién lo envió y que no fue modificado. + El Chat XDK comprueba la firma usando la clave pública de firma del remitente (y el enlace de identidad asociado), para que sepas quién lo envió y que no fue modificado. El Chat XDK descifra con la clave de conversación. Ahora puedes leer: "Hola, ¿cómo estás?" -La implementación de cifrar, enviar, recibir y descifrar está en [Primeros pasos](/xchat/getting-started) y en la referencia del [Chat XDK](/xchat/xchat-xdk). +La implementación de cifrar, enviar, recibir y descifrar está en [Primeros pasos](/es/xchat/getting-started) y en la referencia del [Chat XDK](/es/xchat/xchat-xdk). --- ## Distribución de claves explicada -Un desafío central del cifrado de extremo a extremo es la **distribución de claves**: cómo los participantes obtienen la clave de conversación **sin** que X (u otro observador) vea esa clave en claro. +Un desafío central en el cifrado de extremo a extremo es la **distribución de claves**: cómo los participantes obtienen la clave de conversación **sin** que X (o un observador) vea esa clave en claro. -### Configuración inicial de claves +### Configuración inicial de la clave 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 para **la clave pública de identidad de cada participante** +3. Tu app publica esas copias cifradas a través de las Chat APIs 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. +X solo maneja las copias **envueltas**, nunca la clave de conversación en bruto. ### Eventos de cambio de clave -Cuando la clave de conversación se rota (por ejemplo, al cambiar la membresía), los participantes reciben un evento de **cambio de clave** con nuevas copias cifradas para cada miembro. +Cuando la clave de conversación rota (por ejemplo cuando cambia la membresía), los participantes reciben un evento de **cambio de clave** con nuevas copias cifradas para cada miembro. -Tu app debería: +Tu app debe: -1. Detectar material de cambio de clave en los eventos en vivo o en el historial de la conversación -2. Descifrar y almacenar la nueva clave de conversación (y su versión) -3. Usar la versión más reciente para envíos posteriores +1. Detectar material de cambio de clave en eventos en vivo o en el historial de la conversación +2. Descifrar y almacenar la nueva clave de conversación (y versión) +3. Usar la versión más reciente para los envíos posteriores -[Primeros pasos](/xchat/getting-started#6-receive-and-decrypt) y [Eventos en tiempo real](/xchat/real-time-events) describen dónde aparecen esos eventos en la práctica. +[Primeros pasos](/es/xchat/getting-started#6-receive-and-decrypt) y [Eventos en tiempo real](/es/xchat/real-time-events) describen dónde aparecen esos eventos en la práctica. --- ## 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 de 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 en distintos dispositivos sin darle a un solo servidor el secreto completo. ### El problema con el almacenamiento tradicional de claves | Enfoque | Problema | -|:--------|:---------| +|:---------|:--------| | Almacenar solo en el dispositivo | Perder el dispositivo = perder las claves = perder acceso al historial de mensajes | -| Almacenar en un respaldo ordinario en la nube | El proveedor podría acceder al material de la clave | +| Almacenar en una copia de seguridad en la nube común | El proveedor podría acceder al material de la clave | | Recordar una clave larga | Las personas no pueden memorizar de forma confiable claves de alta entropía | ### Cómo lo resuelve la copia de seguridad segura de claves -La copia de seguridad segura de claves combina **secret sharing** con **protección por código de acceso**: +La copia de seguridad segura de claves combina **compartición de secretos** con **protección por código de acceso**: -1. Las claves privadas se **dividen en partes (shares)** -2. Los shares están en poder de **realms independientes** (servidores separados) -3. **Ningún realm individual** tiene suficiente información para reconstruir las claves por sí solo +1. Las claves privadas se **dividen en shares** +2. Los shares los guardan **realms independientes** (servidores separados) +3. **Ningún realm** tiene por sí solo información suficiente para reconstruir las claves 4. La recuperación requiere tu **código de acceso** y la cooperación de **suficientes realms** -5. Los códigos de acceso incorrectos están **limitados por tasa** para ralentizar los intentos +5. Los códigos de acceso incorrectos están **limitados por tasa** para ralentizar las conjeturas ```mermaid flowchart LR @@ -198,41 +198,43 @@ flowchart LR end ``` -Obtienes recuperabilidad (nuevo dispositivo + código de acceso) sin que una sola parte tenga el secreto completo. +Obtienes capacidad de recuperación (nuevo dispositivo + código de acceso) sin que una sola parte guarde todo el secreto. -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 manualmente para el flujo normal. El Chat XDK incluye el cliente de copia de seguridad; la configuración de los realms viene de la X API como el campo **`juicebox_config`** en tu registro de public-key. El almacenamiento inicial del código de acceso y el desbloqueo posterior son llamadas del Chat XDK—consulta [inicializar con claves existentes](/es/xchat/getting-started#2-initialize-the-chat-xdk-with-existing-keys) y [crear y registrar claves](/es/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 si fuera una contraseña. --- ## Firmas explicadas -Cada mensaje de X Chat incluye una **firma digital** que respalda: +Cada mensaje de X Chat incluye una **firma digital** que aporta: -1. **Autenticidad** — fue producido con la clave privada de firma del remitente -2. **Integridad** — el contenido cifrado no fue modificado después de firmarse +1. **Autenticidad** — se produjo con la clave privada de firma del remitente +2. **Integridad** — el contenido cifrado no se modificó después de firmarse ### Cómo funcionan las firmas (conceptualmente) | Acción | Clave utilizada | Resultado | -|:-------|:----------------|:----------| -| **Firmar** | Clave privada de firma del remitente | Una firma vinculada exactamente a este mensaje cifrado | +|:-------|:---------|:-------| +| **Firmar** | Clave privada de firma del remitente | Una firma vinculada a este mensaje cifrado exacto | | **Verificar** | Clave pública de firma del remitente | Confirma que la firma coincide con el mensaje y la clave | -Si algo cambia en el material firmado, la verificación falla. Solo alguien con la clave privada de firma puede producir una firma válida para esa clave. +Si algo en el material firmado cambia, la verificación falla. Solo alguien con la clave privada de firma puede producir una firma válida para esa clave. ### 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 los entrantes contra el material de clave pública del remitente (obtenido de las APIs de public-key). La verificación es **obligatoria por defecto**: el SDK rechaza 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](/es/xchat/xchat-xdk). + +Las firmas también cubren el contenido citado. Una respuesta incrusta el mensaje original **firmado** en bruto que cita; cuando el Chat XDK descifra la respuesta, verifica ese original incrustado y compara la cita contra él, reportando el resultado como `reply_preview_validation` (`Valid` / `Invalid`). Un resultado `Invalid` significa que la cita no coincide con el original firmado—trata el material citado como no confiable, aunque la respuesta en sí se verifique por separado—de modo que ningún participante pueda atribuir palabras inventadas a otro. ### Cambios de estado firmados (firmas de acción) -Los mensajes no son el único material firmado. Cada llamada que cambia el estado de una conversación—agregar o rotar claves de conversación, crear un grupo, agregar miembros—debe llevar una o más **firmas de acción**: el remitente firma una carga que describe exactamente lo que hace el cambio (para un cambio de clave, esa carga incluye la nueva clave de conversación en sí), y la API rechaza la solicitud si las firmas están ausentes o mal formadas. +Los mensajes no son el único material firmado. Cada llamada que cambia el estado de una conversación—añadir o rotar claves de conversación, crear un grupo, añadir miembros—debe llevar una o más **firmas de acción**: el remitente firma un payload que describe exactamente lo que hace el cambio (para un cambio de clave, ese payload incluye la nueva clave de conversación en sí), y la API rechaza la solicitud si las firmas faltan o están mal formadas. -Como el servidor nunca posee la clave de conversación en texto plano, no puede comprobar criptográficamente la firma de un cambio de clave; valida que la descripción firmada y codificada del cambio coincida con la solicitud que recibió. La comprobación **criptográfica** ocurre en los extremos: el Chat XDK de cada destinatario verifica la firma contra la clave pública de firma del remitente cuando descifra el evento de cambio de clave. Los métodos `prepare` del Chat XDK producen estas firmas por ti—los group creates y los member adds devuelven **dos** (el cambio de clave más la acción de grupo), y ambos deben enviarse. +Como el servidor nunca posee la clave de conversación en texto plano, no puede comprobar criptográficamente la firma de un cambio de clave; valida que la descripción firmada y codificada del cambio coincida con la solicitud que recibió. La comprobación **criptográfica** ocurre en los extremos: el Chat XDK de cada destinatario verifica la firma contra la clave pública de firma del remitente cuando descifra el evento de cambio de clave. Los métodos `prepare` del Chat XDK producen estas firmas por ti—las creaciones de grupo y las adiciones de miembros devuelven **dos** (el cambio de clave más la acción del grupo), y ambas deben enviarse. -Las firmas están vinculadas al contenido del evento y son inmutables: un evento cuya firma no verifica nunca podrá volverse válido más adelante. Consulta [Solución de problemas](/xchat/troubleshooting) para saber cómo tratarlos. +Las firmas están vinculadas al contenido del evento y son inmutables: un evento cuya firma no se verifica nunca podrá volverse válido más tarde. Consulta [Solución de problemas](/es/xchat/troubleshooting) para saber cómo tratarlos. --- @@ -241,55 +243,55 @@ Las firmas están vinculadas al contenido del evento y son inmutables: un evento ### Contra qué protege X Chat | Amenaza | Protección | -|:--------|:-----------| -| **Que X lea el cuerpo de los mensajes** | El contenido se cifra antes de enviarse a X | -| **Espionaje de red** | Seguridad de transporte más contenido cifrado de extremo a extremo | +|:-------|:-----------| +| **X lee los cuerpos de los mensajes** | El contenido se cifra antes de enviarse a X | +| **Espías de la red** | Seguridad del transporte más contenido cifrado de extremo a extremo | | **Manipulación de mensajes** | Las firmas detectan la modificación | | **Suplantación trivial del remitente** | Las firmas válidas requieren la clave privada de firma del remitente | -| **Robo de claves en un único servidor (con copia de seguridad segura de claves)** | Los shares se dividen entre realms y se protegen con código de acceso | +| **Robo de claves en un solo servidor (con copia de seguridad segura de claves)** | Los shares se dividen entre realms y están protegidos por código de acceso | ### Contra qué **no** protege X Chat | Amenaza | Por qué no | -|:--------|:-----------| +|:-------|:--------| | **Dispositivo comprometido** | El texto plano y las claves pueden quedar expuestos en un cliente desbloqueado | -| **Metadatos** | X puede saber quién envió un mensaje a quién y cuándo—no el texto del mensaje | -| **Forward secrecy** | El compromiso de las claves de identidad puede exponer las claves de conversación envueltas hacia esas claves | -| **Post-compromise security** | Rotar las claves no reescribe el historial | +| **Metadatos** | X puede saber quién envió mensajes a quién y cuándo—no el texto del mensaje | +| **Confidencialidad hacia adelante** | El compromiso de las claves de identidad puede exponer las claves de conversación que se envolvieron con esas claves | +| **Seguridad post-compromiso** | Rotar las claves no reescribe el historial | --- ## Glosario | 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) | -| **Clave pública** | Es seguro compartirla; se usa para cifrar *hacia* alguien o verificar sus firmas | +|:-----|:-----------| +| **Cifrado simétrico** | La misma clave cifra y descifra (usado para mensajes y streams de multimedia) | +| **Cifrado asimétrico** | Claves diferentes para cifrar y descifrar (usado para intercambiar claves de conversación) | +| **Clave pública** | Se puede compartir con seguridad; se usa para cifrar *a* 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) | -| **Secret sharing** | División de un secreto para que se necesiten varias piezas para reconstruirlo | -| **Realm** | Un servidor independiente de copia de seguridad segura de claves que mantiene un share de tu material de claves | +| **Clave de conversación** | Clave simétrica compartida por los participantes de una conversación (versionada en el tiempo) | +| **Compartición de secretos** | Dividir un secreto de forma que se necesiten varias piezas para reconstruirlo | +| **Realm** | Un servidor independiente de copia de seguridad segura de claves que guarda un share de tu material de clave | --- ## Próximos pasos - + Implementa claves, envío y recepción paso a paso - + Métodos y tipos del SDK de cifrado - + Descripción general del producto y arquitectura - + Cómo se entregan los eventos cifrados diff --git a/es/xchat/getting-started.mdx b/es/xchat/getting-started.mdx index 5fea0a354..0518773dd 100644 --- a/es/xchat/getting-started.mdx +++ b/es/xchat/getting-started.mdx @@ -1,18 +1,18 @@ --- title: Primeros pasos con la Chat API sidebarTitle: Primeros pasos -description: Tutorial paso a paso para crear mensajería X Chat cifrada de extremo a extremo con el Chat XDK en Python, TypeScript, Go, Rust, C# o Java. +description: Tutorial paso a paso para crear mensajería X Chat cifrada de extremo a extremo usando el Chat XDK en Python, TypeScript, Go, Rust, C# o Java. keywords: ["X Chat tutorial", "X Chat quickstart", "Chat XDK", "encrypted DM", "Python", "TypeScript", "Go", "Rust", "C#", "Java"] --- Envía y recibe mensajes directos cifrados de extremo a extremo en X: configura claves, inicializa una conversación, envía un mensaje y descifra el tráfico entrante. -Las apps de X Chat usan dos piezas juntas: +Las apps de X Chat usan dos piezas en conjunto: -| Componente | Función | -|:-----------|:--------| -| **[Chat XDK](/xchat/xchat-xdk)** | Cifrado, descifrado, firma y almacenamiento de claves privadas (copia de seguridad segura de claves o un blob de claves) | -| **X API** | Claves públicas, claves de conversación, mensajes y eventos—vía el XDK de [Python](/xdks/python/overview) o [TypeScript](/xdks/typescript/overview), o HTTPS con un token de acceso de usuario | +| Componente | Rol | +|:----------|:-----| +| **[Chat XDK](/es/xchat/xchat-xdk)** | Cifrado, descifrado, firma y almacenamiento de claves privadas (copia de seguridad segura de claves o un blob de claves) | +| **X API** | Claves públicas, claves de conversación, mensajes y eventos—a través del XDK de [Python](/xdks/python/overview) o [TypeScript](/xdks/typescript/overview), o HTTPS con un token de acceso de usuario | **Requisitos previos** @@ -23,7 +23,7 @@ Las apps de X Chat usan dos piezas juntas: --- -## 1. Instala las dependencias +## 1. Instalar dependencias @@ -39,17 +39,16 @@ Las apps de X Chat usan dos piezas juntas: npm install juicebox-sdk # optional peer dependency — required for setup()/unlock() secure key backup ``` - El motor WASM compilado viene dentro de `@xdevplatform/chat-xdk`: no hay paso de build. Requiere Node.js 18+. + El motor WASM compilado se distribuye dentro de `@xdevplatform/chat-xdk`—sin paso de build. Requiere Node.js 18+. ```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] @@ -61,29 +60,29 @@ Las apps de X Chat usan dos piezas juntas: go get github.com/xdevplatform/chat-xdk/go/chatxdk ``` - Se incluyen bibliotecas estáticas precompiladas (macOS arm64/amd64, Linux amd64 glibc/musl): necesitas un compilador de C, pero no Rust. Requiere Go 1.21+. + Se incluyen bibliotecas estáticas precompiladas (macOS arm64/amd64, Linux amd64 glibc/musl)—necesitas un compilador de C, pero no Rust. Requiere Go 1.21+. ```bash dotnet add package XDevPlatform.ChatXdk ``` - El paquete es autónomo: incluye las bibliotecas nativas para macOS (arm64, x64), Linux (x64) y Windows (x64). Requiere .NET 8+. + El paquete es autónomo: las bibliotecas nativas para macOS (arm64, x64), Linux (x64) y Windows (x64) se distribuyen dentro. Requiere .NET 8+. ```xml com.x chatxdk - 0.2.1 + 0.4.0 ``` - Disponible en Maven Central. El jar incluye la biblioteca nativa para macOS (arm64, x64), Linux (x64) y Windows (x64): no necesitas configurar `jna.library.path`. Importa desde `com.x.chatxdk`. Requiere JDK 17+. + Disponible en Maven Central. El jar incluye la biblioteca nativa para macOS (arm64, x64), Linux (x64) y Windows (x64)—no se necesita configurar `jna.library.path`. Importa desde `com.x.chatxdk`. Requiere JDK 17+. -Crea un cliente de API con tu token de acceso OAuth 2.0 de **usuario**: +Crea un cliente de la API con tu token de acceso OAuth 2.0 de **usuario**: @@ -131,16 +130,16 @@ Crea un cliente de API con tu token de acceso OAuth 2.0 de **usuario**: --- -## 2. Inicializa el Chat XDK con claves existentes +## 2. Inicializar 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 haya completado 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`. +- **Copia de seguridad segura de claves:** construye el SDK con el `juicebox_config` de tu registro de public-key, luego `unlock` con tu código de acceso para recuperar las claves privadas (por ejemplo, en un nuevo dispositivo). +- **Blob de claves:** `import_keys` con un blob que exportaste previamente mediante `export_keys`, pasando junto a él la versión de clave registrada (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). +Luego 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 almacena la identidad de la sesión: cada llamada posterior de 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. +**¿Configurando por primera vez?** Construye el SDK de la misma forma pero omite `unlock`/`import_keys`, y continúa al [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,28 +237,34 @@ 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); } ``` -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. +Los ejemplos de servidor y bot suelen usar un **blob de claves** (`export_keys` / `import_keys`). Las apps de cliente suelen usar **copia de seguridad segura de claves** (`setup` / `unlock` con un código de acceso). Consulta la referencia del [Chat XDK](/es/xchat/xchat-xdk) para ambas rutas. -**¿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 producido por `export_keys` del Chat XDK—es una serialización privada y versionada del estado completo de la clave, no claves P-256 en bruto o codificadas en PEM. No puedes construir este blob por tu cuenta: genera claves mediante `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 artesanales o modificados fallan al importar. --- -## 3. Crea y registra las claves (configuración inicial) +## 3. Crear y registrar 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). En caso contrario, la configuración única para una nueva identidad 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. **Almacenar 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** — POST al payload de registro 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 de registro único listos para ejecutar para cada binding están en [`chat-xdk/examples`](https://github.com/xdevplatform/chat-xdk/tree/main/examples) (Python, TypeScript, Go, Rust, C# y Java). Úsalos en lugar de armar el flujo a mano cuando solo necesites 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,25 +384,25 @@ 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"); ``` -Usa un código de acceso fuerte para la copia de seguridad segura de claves. Perder el código de acceso o un blob de claves desprotegido puede impedir el descifrado de mensajes anteriores. +Usa un código de acceso robusto para la copia de seguridad segura de claves. Perder el código de acceso o un blob de claves desprotegido puede impedir descifrar mensajes pasados. --- -## 4. Configura las claves de conversación +## 4. Configurar 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 configuraste en el paso 2. Una 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). Guarda 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. +La respuesta devuelve el ID canónico de la conversación (`data.conversation_id`—el par unido por guion 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 del lado del cliente. La misma llamada también **rota** claves más tarde: 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 fue expuesta—la rotación protege solo los mensajes **futuros**; los mensajes cifrados con versiones anteriores de la clave siguen siendo legibles para cualquiera que tenga esas versiones. -**Verifica las claves recuperadas antes de envolverlas.** `prepare_conversation_key_change` cifra la nueva clave de conversación hacia cualesquiera claves públicas que le pases. Comprueba primero cada registro obtenido con `verify_key_binding(identity, signing, signature)`—pasando los campos `public_key`, `signing_public_key` y `identity_public_key_signature` del registro obtenidos 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 obtenidas antes de envolverlas.** `prepare_conversation_key_change` cifra la nueva clave de conversación con cualquier clave pública que pases. Verifica primero cada registro obtenido con `verify_key_binding(identity, signing, signature)`—pasando los campos `public_key`, `signing_public_key` e `identity_public_key_signature` del registro desde la API de public-keys—para que una clave de identidad sustituida no pueda recibir la clave de conversación. @@ -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) { @@ -671,38 +676,34 @@ La respuesta devuelve el id canónico de la conversación (`data.conversation_id --- -## 5. Envía un mensaje +## 5. Enviar 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 lo generas 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. +Usa un ID de conversación con **guiones** en la ruta de la URL cuando la API lo requiera (`:` → `-`). El SDK en sí 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 simplemente el user id del destinatario—y lo canonicaliza antes de firmar. Los IDs de grupo (con prefijo `g`) se 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,22 +822,26 @@ 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)`—el SDK completa con la última clave verificada. Los reintentos deben reenviar el **mismo** payload cifrado, para que nunca se genere un ID dos veces. + + --- -## 6. Recibe y descifra +## 6. Recibir y descifrar -Usa [webhooks o el activity stream](/xchat/real-time-events) para el tráfico en vivo, o pagina los **events** de la conversación para el historial. +Usa [webhooks o el activity stream](/es/xchat/real-time-events) para el tráfico en vivo, o pagina los **events** de conversación para el historial. -- 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)) -- JavaScript usa tipos de evento en camelCase (`message`); los demás lenguajes usan `"Message"` y campos snake_case en JSON +- Campos del payload en vivo: `encoded_event`, opcional `conversation_key_change_event` +- Historial: `GET /2/chat/conversations/{id}/events` — prefiere **`decrypt_events`** en todos los eventos más `meta.conversation_key_events` +- 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 public-keys que usaste en el paso 4 y mapea los campos a `SigningKeyEntry` (los snippets a continuación incluyen el mapeo) +- Puedes pasar las claves de firma (y, para `decrypt_event`, las claves de conversación) en cada llamada, **o** configurar dos almacenes de sesión opcionales una vez y usar las formas de llamada breves. Los snippets a continuación usan los almacenes: `set_signing_keys(entries)` guarda las claves de los participantes, y `set_cache_keys(true)` (desactivado por defecto) mantiene 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`); otros lenguajes usan `"Message"` y campos en 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. Donde 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 anulaciones `conversation_key`/`conversation_key_version` en los métodos de cifrado. Persiste tú mismo las `conversation_keys` devueltas por `decrypt_events` y vuelve a pasarlas. + + +Bots completos de poll-and-reply 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 el almacén de claves de firma actualizado: vuelve a llamar a `set_signing_keys` con el conjunto completo de participantes cuando un remitente registre una nueva versión de clave, y refresca 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..f9c9aabea 100644 --- a/es/xchat/groups.mdx +++ b/es/xchat/groups.mdx @@ -1,45 +1,46 @@ --- 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 multiparticipante 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"] --- -Los chats de grupo usan el **mismo modelo de cifrado** que un X Chat 1:1: una **clave de conversación** compartida por los miembros, envuelta hacia la **clave pública de identidad** de cada miembro, con mensajes cifrados y firmados por el Chat XDK. Lo que cambia es la **membresía**, **cómo creas la conversación** y, a menudo, los campos de **título/avatar cifrados** en la conversación. +Los chats de grupo usan el **mismo modelo de cifrado** que X Chat 1:1: una **clave de conversación** compartida por los miembros, envuelta con la **clave pública de identidad** de cada miembro, con mensajes cifrados y firmados por el Chat XDK. Lo que cambia es la **membresía**, **cómo creas la conversación** y, a menudo, los campos **cifrados de título/avatar** de la conversación. -Los flujos 1:1 están en [Primeros pasos](/xchat/getting-started). Los detalles de los endpoints están bajo **API reference → Conversations and messages**. +Los flujos 1:1 están en [Primeros pasos](/es/xchat/getting-started). Los detalles de los endpoints están en **API reference → Conversations and messages**. --- -## En qué se diferencian los grupos de los 1:1 +## Cómo difieren los grupos de 1:1 | Tema | 1:1 | Grupo | -|:-----|:----|:------| -| Identidad | Frecuentemente direccionado por el id de usuario del par en las rutas | El id de conversación normalmente comienza con `g` | -| Crear | Claves + mensajería a un usuario | APIs de crear / inicializar grupo, luego claves | +|:------|:----|:------| +| Identidad | A menudo direccionado por user id del par en las rutas | El ID de conversación normalmente comienza con `g` | +| Crear | Claves + mensajería a un usuario | APIs de crear/inicializar grupo, luego claves | | Participantes | Tú + un par | Muchos usuarios; la membresía puede cambiar | -| Metadatos | Mínimos | Nombre, avatar, etc. pueden ser **texto cifrado** (descifra con la clave de conversación) | -| Rotación de claves | Menos frecuente | Común cuando entra o sale gente | +| Metadatos | Mínimos | Nombre, avatar, etc. pueden ser **texto cifrado** (descifrar con la clave de conversación) | +| Rotación de claves | Menos frecuente | Común cuando entran o salen personas | -La criptografía sigue siendo: **Chat XDK** para claves y cargas útiles; **X API** para crear el grupo, publicar los envoltorios de clave para los participantes, enviar mensajes y cargar eventos. +La criptografía sigue siendo: **Chat XDK** para claves y payloads; **X API** para crear el grupo, publicar los envoltorios de clave de los participantes, enviar mensajes y cargar eventos. --- -## Crea el grupo y establece las claves +## Crear el grupo y establecer claves -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). -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. +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 siguiente. +2. Carga la clave pública de identidad y el `public_key_version` de cada miembro (rutas `GET` de public-key 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](/es/xchat/getting-started#4-set-up-conversation-keys)). +3. Ejecuta **`prepare_group_create`** una vez, con **todos** los miembros (incluido tú), el ID con prefijo `g`, y las listas de IDs de miembros/administradores. Una 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 humanos, 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. Guarda 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 `avatar_url` que pasas y los incrusta literalmente en el evento group-create. El servidor los verifica contra tu solicitud, así que los valores `group_name` / `group_avatar_url` en el cuerpo del POST deben ser **idénticos byte a byte** 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 @@ -107,19 +106,19 @@ El `title` y el `avatar_url` que pasas a `prepare_group_create` se firman y se i -El mapeo del cuerpo para las claves de participantes y las firmas de acción (`message_id`, `encoded_message_event_detail`, `message_event_signature` anidado) es el mismo que el POST de claves en [Primeros pasos — claves de conversación](/xchat/getting-started#4-set-up-conversation-keys). +El mapeo del cuerpo para las claves de los participantes y las firmas de acción (`message_id`, `encoded_message_event_detail`, `message_event_signature` anidado) es el mismo que el POST de claves en [Primeros pasos — claves de conversación](/es/xchat/getting-started#4-set-up-conversation-keys). -Cuando cambia la membresía, llama a **`prepare_group_members_change`** con los nuevos ids de miembros más el roster actual (miembros, administradores, miembros pendientes y el título/avatar/TTL actual si está configurado). Rota la clave de conversación y, al igual que la creación de grupo, devuelve **dos** firmas de acción — envía todo con POST a **add members** (`POST /2/chat/conversations/{id}/members`). Después espera tráfico de **cambio de clave**: trátalo como la [rotación de claves en Primeros pasos](/xchat/getting-started#6-receive-and-decrypt) (`extract_conversation_keys` / `decrypt_events`, luego cifra con la versión más reciente). +Cuando cambia la membresía, llama a **`prepare_group_members_change`** con los nuevos IDs de miembros más la lista actual (miembros, administradores, miembros pendientes y el título/avatar/TTL actuales si están definidos). Rota la clave de conversación y, como group create, devuelve **dos** firmas de acción — envía todo con POST a **add members** (`POST /2/chat/conversations/{id}/members`). Luego espera tráfico de **key-change**: trátalo como [rotación de clave en Primeros pasos](/es/xchat/getting-started#6-receive-and-decrypt) (`extract_conversation_keys` / `decrypt_events`, luego cifra con la última versión). -Como `prepare_group_members_change` genera una clave de conversación **nueva** envuelta solo para el roster que pasas, los nuevos miembros reciben la nueva versión de la clave y no pueden descifrar los mensajes enviados bajo versiones anteriores. Lo inverso no es cierto: la rotación nunca revoca el acceso a **versiones anteriores** — cualquiera que ya tenga una clave vieja puede seguir leyendo los mensajes cifrados con ella. Si sospechas que una clave de conversación fue expuesta, rota con `prepare_conversation_key_change`; esto protege únicamente los mensajes futuros. +Como `prepare_group_members_change` genera una clave de conversación **nueva** envuelta solo para la lista que pasas, los nuevos miembros reciben la nueva versión de clave y no pueden descifrar mensajes enviados con versiones anteriores. Lo contrario no es cierto: la rotación nunca revoca el acceso a versiones **anteriores** — cualquiera que ya tenga una clave vieja aún puede leer los mensajes cifrados con ella. Si sospechas que una clave de conversación fue expuesta, rota con `prepare_conversation_key_change`; esto protege solo los mensajes futuros. --- -## Metadatos de grupo cifrados +## Metadatos cifrados del grupo -Algunos campos de conversación (por ejemplo el **nombre** para mostrar o la **URL del avatar**) pueden llegar **cifrados** con la clave de conversación. Eso **no** es `encrypt_message`; es el par genérico **`encrypt` / `decrypt`** del Chat XDK (cadena UTF-8 de entrada, texto cifrado base64 de salida, con la clave de conversación **en bruto**). +Algunos campos de la conversación (por ejemplo el **name** o **avatar URL** de visualización) pueden llegar **cifrados** con la clave de conversación. Eso **no** es `encrypt_message`; es el par genérico **`encrypt` / `decrypt`** del Chat XDK (string UTF-8 dentro, texto cifrado en base64 fuera, con la clave de conversación en **bruto**). -Si un campo dado se almacena cifrado lo decide el cliente que lo escribe: `prepare_group_create` firma y envía el título exactamente como se lo proporcionas (la clave de conversación no existe hasta que esa llamada la genera, así que un título en el momento de la creación no puede cifrarse con ella). Cuando lees una conversación cuyos campos son texto cifrado, descífralos con `decrypt` y la versión de clave que estaba activa cuando el campo se escribió. +Si un campo dado se almacena cifrado lo decide el cliente que lo escribe: `prepare_group_create` firma y envía el título exactamente como lo proporcionas (la clave de conversación no existe hasta que esa llamada la genera, por lo que un título en el momento de creación no se puede cifrar con ella). Cuando lees una conversación cuyos campos son texto cifrado, descífralos con `decrypt` y la versión de clave que estaba activa cuando se escribió el campo. @@ -166,27 +165,27 @@ Si un campo dado se almacena cifrado lo decide el cliente que lo escribe: `prepa -Usa la versión **actual** de la clave de conversación que aplica a esos metadatos. Si las claves rotaron, descifra con la versión que estaba activa cuando el campo se escribió (o sigue las reglas del producto si los metadatos siempre se reescriben en la rotación). +Usa la versión **actual** de la clave de conversación que corresponde a esos metadatos. Si las claves rotaron, descifra con la versión que estaba activa cuando se escribió el campo (o sigue las reglas del producto si los metadatos siempre se reescriben en la rotación). --- ## Mensajes y eventos -Enviar y recibir en un grupo es lo mismo que en un 1:1 una vez que tienes la clave de conversación en bruto: +Enviar y recibir en un grupo es lo mismo que 1:1 una vez que tienes la clave de conversación en bruto: -- **Enviar:** `encrypt_message` → API send-message ([Primeros pasos](/xchat/getting-started#5-send-a-message)) -- **Recibir:** API de events o [entrega en tiempo real](/xchat/real-time-events) → `decrypt_event` / `decrypt_events` -- **Multimedia:** [Multimedia](/xchat/media) con el id de conversación del grupo +- **Enviar:** `encrypt_message` → API send-message ([Primeros pasos](/es/xchat/getting-started#5-send-a-message)) +- **Recibir:** API de eventos o [entrega en tiempo real](/es/xchat/real-time-events) → `decrypt_event` / `decrypt_events` +- **Contenido multimedia:** [Multimedia](/es/xchat/media) con el ID de conversación de grupo -Siempre cifra con la versión **más reciente** de la clave después de una rotación impulsada por cambios de membresía. +Siempre cifra con la **última** versión de clave tras una rotación por cambio de membresía. --- ## 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 **todos** los miembros; POST los envoltorios de clave de los participantes y **ambas** firmas de acción a `POST /2/chat/conversations/group` +3. Guarda en caché 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 1:1 diff --git a/es/xchat/media.mdx b/es/xchat/media.mdx index 409dbaa74..115e1d8f6 100644 --- a/es/xchat/media.mdx +++ b/es/xchat/media.mdx @@ -1,15 +1,15 @@ --- title: Multimedia y adjuntos sidebarTitle: Multimedia -description: Cifra, sube, envía, descarga y descifra imágenes y archivos adjuntos en X Chat con el Chat XDK, cifrado de streams y endpoints de subida. +description: Cifra, sube, envía, descarga y descifra imágenes y archivos adjuntos en X Chat usando el cifrado de streams del Chat XDK y los endpoints de subida de multimedia. keywords: ["X Chat media", "encrypted images", "attachments", "encrypt_stream", "media upload"] --- Las imágenes y otros archivos usan la **misma clave de conversación** que el texto. Cifra los bytes con el Chat XDK (`encrypt_stream` / `decrypt_stream`), sube mediante las rutas **`/2/chat/media/upload`** (barra lateral **API reference → Media**) y luego adjunta **`media_hash_key`** en `encrypt_message`. -Incluye **`media.write`** con tus scopes de DM al subir. Usa ids de conversación con guión en las rutas (`:` → `-`). Prefiere el MIME/dimensiones desde los bytes **descifrados**. +Incluye **`media.write`** con tus alcances de DM al subir. Usa IDs de conversación con guiones en las rutas (`:` → `-`). Prefiere el MIME/dimensiones a partir de los bytes **descifrados**. -Este camino **no** es el modelo de multimedia de Posts (`expansions=attachments.media_keys`, `media.fields=variants`, etc.). Esos parámetros aplican a **Posts**; los blobs E2EE de X Chat se direccionan por **`media_hash_key`** y por la descarga de multimedia de X Chat. +Esta ruta **no** es el modelo de multimedia de Posts (`expansions=attachments.media_keys`, `media.fields=variants`, etc.). Esos parámetros aplican a **Posts**; los blobs de X Chat E2EE se direccionan por **`media_hash_key`** y la descarga de multimedia de X Chat. ```mermaid flowchart LR @@ -104,7 +104,7 @@ flowchart LR -`encrypt_stream` / `decrypt_stream` procesan la carga completa en memoria. Para archivos grandes, `stream_encryptor()` / `stream_decryptor()` devuelven objetos incrementales (`StreamEncryptor` / `StreamDecryptor`): aliméntalos por fragmentos con `push` y luego llama a `finish` una vez—`finish` arroja error si el stream se truncó. +`encrypt_stream` / `decrypt_stream` procesan todo el payload en memoria. Para archivos grandes, `stream_encryptor()` / `stream_decryptor()` devuelven objetos incrementales (`StreamEncryptor` / `StreamDecryptor`): alimenta trozos con `push`, luego llama a `finish` una vez—`finish` falla si el stream se truncó. --- @@ -116,29 +116,25 @@ flowchart LR | Añadir | `POST` | `/2/chat/media/upload/{id}/append` | | Finalizar | `POST` | `/2/chat/media/upload/{id}/finalize` | -Usa los cuerpos de solicitud en las páginas de OpenAPI bajo **API reference → Media**. Prefiere el tamaño del blob **cifrado** donde se requiera el tamaño. Finalizar produce **`media_hash_key`** para adjuntos y descarga. Reintenta `5xx` transitorios con backoff. Python/TypeScript pueden usar el XDK cuando existan helpers de multimedia; en caso contrario, haz POST con un Bearer token en cualquier lenguaje. +Usa los cuerpos de solicitud en las páginas OpenAPI bajo **API reference → Media**. Prefiere el tamaño del blob **cifrado** donde se requiera el tamaño. Finalize entrega **`media_hash_key`** para los adjuntos y la descarga. Reintenta los `5xx` transitorios con backoff. Python/TypeScript pueden usar el XDK cuando existan helpers de multimedia; de lo contrario, haz POST con un token Bearer en cualquier lenguaje. --- ## 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 multimedia, luego haz POST del cuerpo send-message (mismo mapeo de campos que [Primeros pasos](/es/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 para que nunca se genere 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 se puede omitir por completo: con `set_cache_keys(true)` habilitado, `encrypt_message` resuelve la clave y la versión desde el último cambio de clave verificado de la conversación (consulta [Primeros pasos](/es/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` de 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 por la versión de clave del evento.** Cada evento de mensaje descifrado lleva el `keyVersion` (JS; `key_version` en los otros bindings) con el 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 la incorporación de un miembro), la última clave no puede descifrar multimedia adjunta 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 clave de conversación** con la que se cifró el contenido multimedia +- No registres en logs contenido 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; mantén 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..dfc09fd6c 100644 --- a/es/xchat/real-time-events.mdx +++ b/es/xchat/real-time-events.mdx @@ -1,45 +1,45 @@ --- title: Eventos de X Chat en tiempo real sidebarTitle: Eventos en tiempo real -description: Recibe eventos chat.received, chat.sent y otras actividades cifradas de X Chat mediante webhooks o activity stream y descifra con el Chat XDK. +description: Recibe actividad cifrada de X Chat como chat.received, chat.sent y otros eventos mediante webhooks o el activity stream, y luego descifra los payloads con el Chat XDK. --- -X entrega **`chat.received`**, **`chat.sent`** y actividad relacionada de X Chat con **texto cifrado** en el payload. Descifra con el [Chat XDK](/xchat/xchat-xdk). +X entrega **`chat.received`**, **`chat.sent`** y actividad de X Chat relacionada con **texto cifrado** en el payload. Descifra con el [Chat XDK](/es/xchat/xchat-xdk). -| Capa | Función | -|:-----|:--------| -| **X Activity API** | `GET /2/activity/stream`; `POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions` (consulta la seguridad de OpenAPI por operación) | +| Capa | Rol | +|:------|:-----| +| **X Activity API** | `GET /2/activity/stream`; `POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions` (consulta la seguridad 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. +Los tipos de eventos privados de X Chat necesitan autorización para el usuario que monitorizas. Los adjuntos 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 API de Posts. --- ## Tipos de evento | Evento | Cuándo | -|:-------|:-------| +|:------|:-----| | `chat.received` | El usuario suscrito recibe un DM cifrado | | `chat.sent` | El usuario suscrito envía un DM cifrado | -| `chat.conversation_join` | El usuario suscrito se une a un grupo (cuando se le ofrece) | +| `chat.conversation_join` | El usuario suscrito se une a un grupo (cuando se ofrece) | --- -## 1. Elige el modo de entrega +## 1. Elige la entrega -**Activity stream (a menudo lo más simple para bots):** `GET /2/activity/stream` con un Bearer token de app (opcional `backfill_minutes`, `start_time`, `end_time` según OpenAPI). Filtra en el cliente por `chat.received` / `chat.sent`. +**Activity stream (a menudo lo más simple para bots):** `GET /2/activity/stream` con un token Bearer de app (opcionales `backfill_minutes`, `start_time`, `end_time` según OpenAPI). Filtra del lado del cliente para `chat.received` / `chat.sent`. -**Suscripciones de Activity:** gestiona suscripciones duraderas con: +**Suscripciones de Activity:** administra 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. +Los cuerpos de solicitud y los alcances requeridos están definidos en la operación OpenAPI de cada ruta. Crear una suscripción de la X Activity API (XAA) requiere **autorización en contexto de usuario** (contexto de usuario OAuth 2.0 con los alcances relevantes, como `dm.read` para eventos de chat) del usuario cuya actividad monitorizas. -**Webhooks:** si terminas los eventos en tu endpoint HTTPS, registra un webhook con `POST /2/webhooks`, pasa los challenges CRC y luego crea tus suscripciones de actividad con `POST /2/activity/subscriptions`, haciendo referencia a tu `webhook_id` (consulta las operaciones Webhooks y Activity en OpenAPI). El XDK de Python/TypeScript puede exponer helpers para webhooks y actividad cuando tu versión del SDK los incluya. +**Webhooks:** si terminas los eventos en tu endpoint HTTPS, registra un webhook con `POST /2/webhooks`, pasa los desafíos CRC, luego crea tus suscripciones de Activity con `POST /2/activity/subscriptions`, referenciando tu `webhook_id` (consulta las operaciones Webhooks y Activity en OpenAPI). El XDK de Python/TypeScript puede exponer helpers para webhooks y activity cuando la versión de tu SDK los incluya. @@ -74,130 +74,88 @@ Los cuerpos de solicitud y los scopes requeridos se definen en la operación Ope -Suscríbete también a `chat.sent` si necesitas copias salientes. Otros lenguajes: llama directamente a las mismas rutas HTTPS `/2/activity/*` (token de contexto de usuario para crear suscripciones, Bearer token de app para el stream). +Suscríbete también a `chat.sent` si necesitas copias salientes. Otros lenguajes: llama directamente a las mismas rutas HTTPS `/2/activity/*` (token en contexto de usuario para crear suscripciones, token Bearer de app para el stream). --- ## 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 tu consumer secret, en la forma JSON que espera tu producto de webhooks (típicamente `sha256=`). --- -## 3. Descifra con el Chat XDK +## 3. Descifrar 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 con **`event_uuid`**; deduplica los mensajes con el **`message_id`** que lleva el evento descifrado—forma parte del contenido firmado, mientras que los sequence ids son metadatos no firmados 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 a continuación usan los dos almacenes de sesión **opcionales** para el handler más corto: `set_signing_keys` guarda las claves públicas de los participantes (obtenidas una vez del [endpoint public-keys](/x-api/chat/get-user-public-keys)), y `set_cache_keys(true)` mantiene la clave verificada de cada conversación, así que `decrypt_event` solo necesita el evento. Cuando un payload lleva `conversation_key_change_event`, pásalo primero por `decrypt_events`: eso verifica el cambio de clave y, con la caché activada, retiene su clave para la llamada a `decrypt_event`. ¿Prefieres no tener estado de 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`); otros 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,7 +197,9 @@ JavaScript usa tipos de evento en camelCase (`message`); los demás bindings usa -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). +Para mantener los mapas de claves bajo tu control 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](/es/xchat/getting-started#6-receive-and-decrypt). --- @@ -256,7 +225,7 @@ 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` +- Verifica las firmas de webhooks según los requisitos de la plataforma +- 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 key-change (vía `decrypt_events`) antes de descifrar los mensajes dependientes +- Deduplica las entregas con `event_uuid` y los mensajes con el `message_id` firmado diff --git a/es/xchat/troubleshooting.mdx b/es/xchat/troubleshooting.mdx index 48dd5408b..a3a507b71 100644 --- a/es/xchat/troubleshooting.mdx +++ b/es/xchat/troubleshooting.mdx @@ -1,22 +1,22 @@ --- 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 comunes de cifrado en X Chat, incluidos errores del Chat XDK, recuperación de copia de seguridad segura de claves, fallos de descifrado y construcción de payloads de envío firmados. keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encryption"] --- -Esta página cubre problemas **específicos del cifrado de X Chat y del Chat XDK**—claves, copia de seguridad segura de claves, descifrar/verificar y construcción de payloads cifrados de envío. +Esta página cubre problemas que son **específicos del cifrado de X Chat y del Chat XDK**—claves, copia de seguridad segura de claves, descifrar/verificar, y construcción de payloads de envío cifrados. -Para webhooks, OAuth, códigos de estado HTTP y límites de tasa, usa la documentación general de la [X API](/x-api/introduction) y de [autenticación](/fundamentals/authentication/overview). +Para webhooks, OAuth, códigos de estado HTTP y límites de tasa, usa la documentación general de la [X API](/es/x-api/introduction) y de [autenticación](/es/fundamentals/authentication/overview). --- ## Claves y copia de seguridad segura de claves -### El desbloqueo falla (código de acceso inválido) +### Falla el unlock (código de acceso inválido) -- Confirma que el código de acceso coincide con el usado en `setup` -- Espera entre intentos; los realms limitan por tasa los intentos incorrectos y pueden bloquear la recuperación tras demasiados fallos +- Confirma que el código de acceso coincide con el usado con `setup` +- Espera entre intentos; los realms limitan por tasa los intentos erróneos y pueden bloquear la recuperación tras demasiados fallos @@ -62,80 +62,81 @@ 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 +### Cifrar o descifrar 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, luego configura la **identidad de sesión**—tu user id 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 anulación 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); ``` ### 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` -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)) +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 última clave verificada de cada conversación para que las llamadas posteriores `decrypt_event` y `encrypt_*` puedan omitirla +2. Confirma que se añadieron claves de conversación para esa versión y que sigues siendo participante (consulta [Primeros pasos](/es/xchat/getting-started#4-set-up-conversation-keys)) ### El par no tiene claves públicas -Quizás no ha completado la incorporación. Después de que se registren, carga `public_key`, `signing_public_key`, `identity_public_key_signature` y `public_key_version` desde **API reference → Encryption keys**. +Es posible que no haya terminado el onboarding. Después de que se registre, carga `public_key`, `signing_public_key`, `identity_public_key_signature` y `public_key_version` desde **API reference → Encryption keys**. --- ## Descifrado y firmas -### El descifrado falla +### Falla el descifrado -- Clave de conversación **en bruto** desactualizada o incorrecta, o versión de clave incorrecta +- Clave de conversación en **bruto** obsoleta o incorrecta, o versión de clave incorrecta - Cadena `encoded_event` incompleta - El tipo de evento no es un mensaje cifrado que puedas tratar como contenido descifrable -### La firma no verifica +### La firma no se verifica -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: +La verificación es **fail-closed por defecto** (`reject_unverified = true`): el SDK ya rechaza eventos firmados no verificados, así que un fallo aquí significa que las entradas de verificación son incorrectas, no que necesites 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)) +- Entrada de clave de firma faltante o incompleta para el **remitente** (todos los campos requeridos por el Chat XDK—consulta la referencia del [Chat XDK](/es/xchat/xchat-xdk)) +- No se pasaron claves de firma en la llamada y no hay ninguna 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 +- Una versión de clave por debajo del mínimo aceptado nunca se verifica -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: +El setter `set_reject_unverified` existe para **desactivar** este comportamiento predeterminado (`false`, no recomendado). Si lo desactivaste antes, restaura el predeterminado fail-closed: @@ -170,45 +171,49 @@ El setter `set_reject_unverified` existe para **optar por salir** de este predet -### Los eventos antiguos fallan la verificación de forma permanente +### Una respuesta lleva `reply_preview_validation: "Invalid"` -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. +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; nada se lanza por una vista previa inválida. + +### Los eventos antiguos fallan permanentemente la verificación + +Errores como `signature missing or no matching signing key` o una discrepancia ECDSA en eventos **antiguos** son permanentes. Las firmas son inmutables y se verifican reconstruyendo el payload firmado a partir del propio evento, así que un evento que fue firmado sobre bytes diferentes (o nunca firmado) fallará en cada carga futura—ningún reintento, refresco de claves ni 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 a partir de ese punto hacia adelante; los nuevos mensajes no se ven afectados. --- -## Construcción del payload de envío +## Construyendo el payload de envío -Estos errores son específicos del cifrado de X Chat (no errores HTTP generales): +Estos errores son específicos del cifrado de X Chat (no son errores HTTP generales): | Problema | Solución | -|:---------|:---------| -| 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 | +|:------|:----| +| Bytes de clave incorrectos | Pasa los bytes de la clave de conversación en **bruto** al Chat XDK, no la cadena de clave cifrada de 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 | -| 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 | +| 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 genere dos veces | +| Discrepancia de versión | Alinea `conversation_key_version` con la clave que uses; alinea la versión de la clave de firma pasada a `set_identity` con tu registro de public-key | +| Forma del ID en la ruta | Las rutas URL todavía necesitan el ID de conversación con guiones (`:` → `-`), pero para firmar el SDK acepta cualquier forma: `A:B`, `A-B` (en cualquier orden), o el user id del destinatario a secas—todo se canonicaliza a los mismos bytes firmados | -### La API devuelve 400 en una llamada que cambia estado +### La API devuelve 400 para una llamada que cambia el estado -Cada llamada de chat que cambia estado—agregar o rotar claves de conversación, crear un grupo, agregar miembros—requiere **`action_signatures`** en el cuerpo de la solicitud, validado en el límite de la API. Una entrada faltante o mal formada (cada una necesita `message_id`, `encoded_message_event_detail` y un `message_event_signature` con `signature`, `public_key_version` y `signature_version`) devuelve inmediatamente una respuesta HTTP 400 problem-details. Usa los métodos prepare del SDK (`prepare_conversation_key_change`, `prepare_group_create`, `prepare_group_members_change`) y envía **todas** las firmas devueltas—crear un grupo y añadir miembros devuelven dos. +Cada llamada de chat que cambia el estado—añadir o rotar claves de conversación, crear un grupo, añadir miembros—requiere **`action_signatures`** en el cuerpo de la solicitud, validadas en el límite de la API. Una entrada faltante o mal formada (cada una necesita `message_id`, `encoded_message_event_detail` y una `message_event_signature` con `signature`, `public_key_version` y `signature_version`) devuelve una respuesta problem-details HTTP 400 de inmediato. Usa los métodos prepare del SDK (`prepare_conversation_key_change`, `prepare_group_create`, `prepare_group_members_change`) y envía **todas** las firmas devueltas—group create y member adds devuelven dos. --- -## Cifrar y descifrar multimedia +## Cifrado y descifrado de multimedia -- Usa la **misma** clave de conversación (y versión) que el mensaje que referencia el adjunto -- Trata las respuestas de descarga como **texto cifrado** hasta que ejecutes `decrypt_stream` -- Infiere el tipo MIME **después** de descifrar; el `Content-Type` de la descarga a menudo no es el tipo real de imagen +- Usa la **misma** clave de conversación (y versión) que el mensaje que hace referencia al adjunto +- Trata las respuestas de descarga como **texto cifrado** hasta ejecutar `decrypt_stream` +- Infere el tipo MIME **después** de descifrar; el `Content-Type` de descarga a menudo no es el tipo real de la imagen -Detalles: [Multimedia](/xchat/media). +Detalles: [Multimedia](/es/xchat/media). --- ## Depuración segura -Al investigar fallos criptográficos: +Al investigar fallos de criptografía: -- 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 -- Para historial incompleto, pagina **todas** las páginas de eventos para no saltarte metadatos de cambio de clave antes de descifrar +- Registra en logs los IDs de conversación, los IDs de evento y las **versiones** de claves únicamente +- **No** registres en logs texto plano, códigos de acceso, claves privadas ni blobs de clave completos +- Confirma que la versión de la clave de firma pasada a `set_identity` coincide con el `public_key_version` de tu registro de public-key +- Para historial incompleto, pagina **todas** las páginas de eventos para no saltar los metadatos de key-change antes de descifrar diff --git a/es/xchat/xchat-xdk.mdx b/es/xchat/xchat-xdk.mdx index 31d15145f..11c9b229f 100644 --- a/es/xchat/xchat-xdk.mdx +++ b/es/xchat/xchat-xdk.mdx @@ -1,13 +1,13 @@ --- title: Referencia del Chat XDK sidebarTitle: Chat XDK -description: Referencia del Chat XDK, el SDK de cifrado que gestiona claves, cifrado, descifrado y firmas para X Chat en los lenguajes compatibles. +description: Referencia del Chat XDK, el SDK de cifrado que gestiona la administración de claves, cifrado, descifrado y firma para X Chat en los lenguajes compatibles. keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] --- -El **Chat XDK** se encarga de la gestión de claves, cifrado, descifrado y firma para X Chat. **No** llama a la HTTP API de X—combínalo con el **XDK** de [Python](/xdks/python/overview) o [TypeScript](/xdks/typescript/overview), o con HTTPS y un token de acceso de usuario. +El **Chat XDK** gestiona la administración de claves, cifrado, descifrado y firma para X Chat. **No** llama a la API HTTP de X—combínalo con el **XDK** de [Python](/xdks/python/overview) o [TypeScript](/xdks/typescript/overview), o con HTTPS y un token de acceso de usuario. -Recorrido de la app: [Primeros pasos](/xchat/getting-started). Bots de ejemplo: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). +Recorrido de la app: [Primeros pasos](/es/xchat/getting-started). Bots de ejemplo: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). ### Instalar @@ -25,14 +25,14 @@ Recorrido de la app: [Primeros pasos](/xchat/getting-started). Bots de ejemplo: npm install juicebox-sdk # optional peer dependency — required for setup()/unlock() secure key backup ``` - El motor WASM compilado viene dentro del paquete: no hay paso de build. Requiere Node.js 18+. + El motor WASM compilado se distribuye dentro del paquete—sin paso de build. Requiere Node.js 18+. ```toml [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] @@ -44,25 +44,25 @@ Recorrido de la app: [Primeros pasos](/xchat/getting-started). Bots de ejemplo: go get github.com/xdevplatform/chat-xdk/go/chatxdk ``` - Se incluyen bibliotecas estáticas precompiladas (macOS arm64/amd64, Linux amd64 glibc/musl): necesitas un compilador de C, pero no Rust. Requiere Go 1.21+. + Se incluyen bibliotecas estáticas precompiladas (macOS arm64/amd64, Linux amd64 glibc/musl)—necesitas un compilador de C pero no Rust. Requiere Go 1.21+. ```bash dotnet add package XDevPlatform.ChatXdk ``` - El paquete es autónomo: incluye las bibliotecas nativas para macOS (arm64, x64), Linux (x64) y Windows (x64). Requiere .NET 8+. + El paquete es autónomo: las bibliotecas nativas para macOS (arm64, x64), Linux (x64) y Windows (x64) se distribuyen dentro. Requiere .NET 8+. ```xml com.x chatxdk - 0.2.1 + 0.4.0 ``` - Disponible en Maven Central. El jar incluye la biblioteca nativa para macOS (arm64, x64), Linux (x64) y Windows (x64): no necesitas configurar `jna.library.path`. Importa desde `com.x.chatxdk`. Requiere JDK 17+. + Disponible en Maven Central. El jar incluye la biblioteca nativa para macOS (arm64, x64), Linux (x64) y Windows (x64)—no se necesita configurar `jna.library.path`. Importa desde `com.x.chatxdk`. Requiere JDK 17+. @@ -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 las claves, configura tu identidad una vez, descifra un backlog, descifra un evento en vivo y cifra un mensaje. Conecta el cuerpo del envío a [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) como en [Primeros pasos](/es/xchat/getting-started). + +Los snippets usan los dos almacenes de sesión **opcionales** para las formas de llamada más cortas: `set_signing_keys` guarda las claves públicas de los demás participantes (obtenidas del [endpoint public-keys](/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)` permite al SDK recordar 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 y pasa los mismos valores por llamada en su lugar—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, almacena 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 la clave de firma que cada acción firmada usa por defecto, así los métodos encrypt y prepare funcionan sin argumentos de identidad por llamada. Llama a `generate_keypairs` una vez por identidad de dispositivo/app; publica el payload de registro en el endpoint public-keys. 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 blobs 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 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 a un realm de backup por solicitud debería reutilizar una única instancia `Chat` desbloqueada entre solicitudes, o ejecutar un binding nativo donde se admitan blobs de claves. + +El SDK también necesita la versión que la X API reporta para tu clave pública registrada, así que las entradas de key-change dirigidas a otras versiones se omiten. `set_identity` la registra junto con el user id; `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,26 +350,26 @@ 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—se pasa literalmente), 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á **activada 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 backup; `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). --- ## 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`**. +Los tres métodos **prepare** hacen que una llamada haga todo lo que necesita un cambio de clave: generar una nueva clave de conversación, 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 anularlas. 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 requerido **`action_signatures`** del cuerpo. | 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 | -| Agregar miembros a un grupo | `prepare_group_members_change` | 2—envía ambas | +|:---------|:-------|:---------------------------| +| 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 generado por `POST /2/chat/conversations/group/initialize`) | `prepare_group_create` | 2—envía ambas | +| Añadir 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 de la clave en **bruto** para `encrypt_message` y multimedia; nunca pases el sobre cifrado de la API al cifrar. -**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 obtenidas antes de envolverlas.** Los métodos prepare cifran la nueva clave de conversación con cualquier clave pública 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 public-keys—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. @@ -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/administradores para `prepare_group_create`; nuevos más la lista actual para `prepare_group_members_change`)—consulta [Grupos](/es/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 mensajes descifrados y **recopila** errores por evento en lugar de fallar todo el batch. **`decrypt_event`** es para un solo evento en vivo; lanza/tira en caso de fallo. + +Pasa las **claves de firma** para que el SDK pueda verificar a los remitentes. Mapea los campos de public-key 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 opt-in 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)`** almacena las claves de firma de los participantes; una llamada de descifrado que omita (o pase un argumento vacío de) las claves de firma usa el almacén en su lugar. La verificación en sí no cambia—las claves entran al almacén solo a través de 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á activada, `decrypt_events` guarda en caché, por conversación, la última clave cuyo cambio de clave llevaba 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 recién creada cuyos almacenes están vacíos. + +La verificación es obligatoria por defecto: omitir las claves de firma nunca la salta. Sin pasar nada y sin nada almacenado, los eventos firmados fallan (recopilados en `errors` para `decrypt_events`, lanzados para `decrypt_event`). Para saltarte la verificación realmente, primero debes llamar 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,14 +539,14 @@ 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); ``` @@ -498,32 +555,40 @@ Pasa **claves de firma** para que el SDK pueda verificar a los remitentes. Mapea ## Helpers de cifrado y envío -**`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`**. +**`encrypt_message(conversation_id, text)`** construye el texto cifrado firmado para un mensaje de texto; opcionales `entities`, `attachments` (vía `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 opt-in (`set_cache_keys`)—o pasa `sender_id` / `signing_key_version` y `conversation_key` + `conversation_key_version` explícitamente. El SDK genera el **`message_id`** (un UUID incrustado en el evento firmado) y lo devuelve en el payload—nunca generes el tuyo; reutiliza el mismo payload en los reintentos para que el ID nunca se genere 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`**. + +**Las respuestas se basan en eventos.** `encrypt_reply(conversation_id, text, reply_to_event)` toma el evento en bruto codificado en base64 al que se responde. El SDK deriva la vista previa citada (sequence id, remitente, texto, entidades, adjuntos) 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ó con 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 entonces cita lo que dice el mensaje ahora (su texto y entidades vienen de la edición), y la edición viaja junto al original para que el receptor la compruebe. Los campos explícitos `reply_to_*` permanecen como anulaciones para los 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 se basan en eventos.** `encrypt_add_reaction(target_event, emoji)` y `encrypt_remove_reaction(...)` derivan el ID de conversación y el sequence id del objetivo a partir del evento en bruto al que se reacciona; los mismos params pueden añadir y más tarde eliminar una reacción. Establece `conversation_id` y `target_message_sequence_id` explícitamente 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 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 llevada 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 el contenido editado 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 mensajes. **`encrypt_stream` / `decrypt_stream`** cifran bytes de adjuntos; consulta [Multimedia](/es/xchat/media). Los **`sign` / `verify` / `verify_key_binding`** de bajo nivel soportan flujos avanzados; los cambios de clave de conversación, creaciones de grupo y adiciones de miembros son firmados por los [métodos prepare](#conversation-keys). + +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 el user id del destinatario a secas—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 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](/es/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 todo el payload en memoria: `stream_encryptor()` / `stream_decryptor()` devuelven un `StreamEncryptor` / `StreamDecryptor` al que le pasas trozos (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 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 finalizar (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 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 bibliotecas adicionales. @@ -792,13 +907,13 @@ 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 en todos los lenguajes (los nombres exactos de los campos varían; JS suele usar discriminadores de eventos 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 de 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 public-key para la API add-public-key. +- **SigningKeyEntry** — material público del remitente pasado al descifrar para verificación de firma, 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`, opcional `signature_payload`—omitido en firmas de key-change porque ese payload incrusta la clave en texto plano). +- **DecryptEventsResult** — mensajes, errores opcionales y `conversation_keys` extraídas. Los mensajes descifrados que citan una respuesta llevan `reply_preview_validation` (consulta [Helpers de cifrado y 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`). @@ -806,25 +921,25 @@ Para listas completas de campos, usa los stubs de lenguaje en el [repo chat-xdk] ## 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 para que un evento defectuoso no aborte el batch; inspecciona la colección de errores para ver 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 a partir del propio evento, así que un evento antiguo que falla con `signature missing or no matching signing key` o una discrepancia ECDSA fallará en cada carga futura—ningún reintento, refresco de claves ni 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 a partir de ese punto hacia adelante. --- ## Próximos pasos - - Conecta el Chat XDK a la Chat API + + Conecta el Chat XDK con la Chat API - - Cifrado por stream y REST de multimedia + + Cifrado de streams y REST de multimedia - + Webhooks y entrega de actividad - + Fallos comunes diff --git a/ja/xchat/cryptography-primer.mdx b/ja/xchat/cryptography-primer.mdx index 8c7a435a8..f8df4115a 100644 --- a/ja/xchat/cryptography-primer.mdx +++ b/ja/xchat/cryptography-primer.mdx @@ -1,31 +1,31 @@ --- -title: 暗号化入門 -sidebarTitle: 暗号化入門 -description: X Chat のエンドツーエンド暗号化を支える ECDH、公開鍵暗号、デジタル署名の概念を、実装の詳細に立ち入らずに学びます。 +title: 暗号技術入門 +sidebarTitle: 暗号技術入門 +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](/ja/xchat/xchat-xdk) が暗号化、復号、署名、鍵の保管を代わりに行います——が、この考え方はアプリを設計したり動作をデバッグしたりする際に役立ちます。 -実装の準備ができたら、完全なウォークスルーとして [Getting Started](/xchat/getting-started) を、個別のルートについてはサイドバーの [API リファレンス](/x-api/chat/get-chat-conversations) を使用してください。 +実装の準備ができたら、フル解説の[はじめに](/ja/xchat/getting-started)と、サイドバーの各ルートに関する [API リファレンス](/x-api/chat/get-chat-conversations)を参照してください。 -**この暗号化を自分で実装する必要はありません。** Chat XDK が処理します。このページは理解のためのものであり、API チェックリストではありません。 +**この暗号処理を自分で実装する必要はありません。** Chat XDK が処理します。このページは理解のためのものであり、API のチェックリストではありません。 --- ## 全体像 -X Chat は次のような階層化された暗号化システムを使用します: +X Chat は階層的な暗号化システムを採用しており、次のように動作します。 -1. **メッセージ**は**会話鍵**で暗号化されます(高速な対称暗号化) -2. **会話鍵**は各参加者の **identity 公開鍵**を使って暗号化されます(非対称鍵交換) -3. **メッセージは**、**署名鍵**で**署名される**ので、受信者は誰が送信したのか、そして何も変更されていないことを検証できます +1. **メッセージ**は**会話鍵**で暗号化されます(高速な対称鍵暗号) +2. **会話鍵**は各参加者の**アイデンティティ公開鍵**を使って暗号化されます(非対称鍵交換) +3. **メッセージは署名鍵で署名**されるため、受信者は送信者と改ざんがないことを検証できます -対称暗号化は大量のメッセージトラフィックに対して効率的です。非対称暗号化は、主に会話鍵を安全に**配布**するために使用されます。 +対称鍵暗号は大量のメッセージ通信に効率的です。非対称鍵暗号は主に会話鍵を安全に**配布**するために使われます。 ```mermaid flowchart TB @@ -45,146 +45,146 @@ flowchart TB end ``` -製品フローにおいて、X は**暗号文と鍵エンベロープ**を転送します — 読み取り可能なメッセージ内容や生の会話鍵ではありません。あなたのアプリは暗号処理に Chat XDK を使い、鍵の登録や暗号化されたペイロードの送受信には [Chat API](/xchat/introduction)(Python/TypeScript の XDK 経由、または HTTPS)を使用します。これらのピースがどのように組み合わさるかは [Getting Started](/xchat/getting-started) を参照してください。 +プロダクトのフローでは、X が伝送するのは**暗号文と鍵のエンベロープ**であり、読み取り可能なメッセージ内容や生の会話鍵ではありません。あなたのアプリは暗号処理に Chat XDK を用い、鍵の登録や暗号化ペイロードの送受信に [Chat API](/ja/xchat/introduction)(Python/TypeScript の XDK、または HTTPS 経由)を使います。これらがどのように組み合わさるかは[はじめに](/ja/xchat/getting-started)を参照してください。 --- -## 鍵の種類の説明 +## 鍵の種類の解説 -X Chat は 3 種類の鍵素材を使用しており、それぞれ特定の目的があります。 +X Chat は 3 種類の鍵素材を用い、それぞれに特定の目的があります。 -### 1. Identity キーペア +### 1. アイデンティティ鍵ペア -**目的:** ユーザー間で会話鍵を安全に交換すること +**目的:** ユーザー間で会話鍵を安全にやり取りするため -| 構成要素 | 説明 | +| コンポーネント | 説明 | |:----------|:------------| -| **Identity 公開鍵** | 他のユーザーと共有;会話鍵を*あなた宛て*に暗号化するために使用 | -| **Identity 秘密鍵** | 秘密に保つ;*あなた宛て*に送られた会話鍵を復号するために使用 | +| **アイデンティティ公開鍵** | 他者と共有し、会話鍵をあなた宛に暗号化するために使われる | +| **アイデンティティ秘密鍵** | 秘密に保持し、あなた宛に送られた会話鍵を復号するために使われる | -誰かが会話にあなたを追加するとき、彼らはあなたの identity 公開鍵を使って会話鍵を暗号化します。あなたの identity 秘密鍵だけがそれを復号できます。 +誰かがあなたを会話に追加すると、その相手はあなたのアイデンティティ公開鍵を用いて会話鍵を暗号化します。それを復号できるのはあなたのアイデンティティ秘密鍵だけです。 -公開鍵の半分はプラットフォームの**公開鍵**API を通じて登録および検出されます(API リファレンスの Encryption keys を参照)。秘密鍵の半分は Chat XDK 内に留まります(たとえば [安全な鍵バックアップ](#安全な鍵バックアップ-分散鍵ストレージ) や慎重に保護された鍵 blob 経由)。 +公開部分はプラットフォームの **public-key** API を通じて登録・検索されます(API リファレンスの「Encryption keys」を参照)。秘密部分は Chat XDK 内に保持されます(たとえば[セキュアキーバックアップ](#secure-key-backup-distributed-key-storage)や慎重に保護された鍵ブロブとして)。 -### 2. 署名キーペア +### 2. 署名鍵ペア -**目的:** メッセージを作成したのがあなたであることを証明すること +**目的:** メッセージの作者があなたであることを証明するため -| 構成要素 | 説明 | +| コンポーネント | 説明 | |:----------|:------------| -| **署名公開鍵** | 他のユーザーと共有;あなたの署名を検証するために使用 | -| **署名秘密鍵** | 秘密に保つ;あなたのメッセージに署名するために使用 | +| **署名公開鍵** | 他者と共有し、あなたの署名を検証するために使われる | +| **署名秘密鍵** | 秘密に保持し、メッセージへの署名に使われる | -メッセージを送信するとき、あなたの署名秘密鍵で署名されます。受信者はあなたの署名公開鍵(同じく公開鍵 API を通じて公開されます)を使って検証します。Chat XDK はメッセージの暗号化の一部として署名を行い、送信者の公開鍵素材を渡した場合は復号時に検証することもできます。 +メッセージを送信すると、あなたの署名秘密鍵で署名されます。受信者はあなたの署名公開鍵(これも public-key API を通じて公開されます)を用いて検証します。Chat XDK はメッセージを暗号化するのと同時に署名し、送信者の公開鍵素材を渡せば復号時に検証も行えます。 ### 3. 会話鍵 -**目的:** 特定の会話内でメッセージ(およびメディア)を暗号化・復号すること +**目的:** 特定の会話内でメッセージ(および[メディア](/ja/xchat/media))を暗号化・復号するため | プロパティ | 説明 | |:---------|:------------| -| **対称** | 同じ鍵で暗号化と復号を行う | -| **会話ごと** | 各会話に独自の鍵がある | -| **参加者間で共有** | 会話を読むべき参加者全員がコピーを保持 | -| **バージョン付き** | 鍵はローテーション可能;アプリはバージョンを時間の経過とともに追跡すべき | +| **対称鍵** | 同じ鍵で暗号化と復号を行う | +| **会話ごと** | 各会話は独自の鍵を持つ | +| **参加者間で共有** | 会話を読めるべきすべての参加者がコピーを持つ | +| **バージョン管理** | 鍵はローテーション可能。アプリは時系列でバージョンを追跡すべき | -会話鍵は会話がセットアップされたときや、鍵がローテーションされたときに生成されます。各参加者は identity 公開鍵で暗号化された、鍵の**暗号化コピー**を受け取ります。自分のコピーを一度復号したら、**未加工の**会話鍵を保持し、高速なメッセージ(および[メディア](/xchat/media))暗号化に使用します。会話のためにこれらのコピーをセットアップすることは、Chat XDK と会話**鍵**エンドポイントを組み合わせて行われます — [Getting Started](/xchat/getting-started#4-set-up-conversation-keys) で解説されています。 +会話鍵は会話がセットアップされる時や鍵がローテーションされる時に生成されます。各参加者は自分のアイデンティティ公開鍵で作られた鍵の**暗号化されたコピー**を受け取ります。自分のコピーを一度復号したあと、**生の**会話鍵を保持し、高速なメッセージ(および[メディア](/ja/xchat/media))暗号化に使用します。会話のためにこれらのコピーをセットアップする処理は Chat XDK と会話の **key** エンドポイントの組み合わせで行います——手順は[はじめに](/ja/xchat/getting-started#4-set-up-conversation-keys)で解説しています。 --- -## 暗号化の仕組み(概念的に) +## 暗号化の仕組み(概念) ### メッセージの送信 - - あなたが入力: "Hello, how are you?" + + あなたは「Hello, how are you?」と入力します。 - - アプリは、正しい鍵バージョンについて、このチャットの未加工の会話鍵(セットアップまたは以前の鍵配布イベントから)を使用します。 + + アプリはこのチャット用の生の会話鍵(セットアップ時、または以前の鍵配布イベントから取得したもの)を、正しい鍵バージョンで使用します。 - - Chat XDK は会話鍵でメッセージを暗号化します。結果はその鍵がなければ役に立たない暗号文です。 + + Chat XDK が会話鍵でメッセージを暗号化します。結果はその鍵なしでは無意味な暗号文となります。 - - Chat XDK は暗号化されたペイロードにあなたの署名秘密鍵で署名し、あなたがまさにこの内容を作成したことを証明します。 + + Chat XDK が暗号化されたペイロードにあなたの署名秘密鍵で署名し、まさにこの内容の作者があなたであることを証明します。 - - アプリは、Chat API の**メッセージ送信**エンドポイントを介して暗号化されたペイロードと署名を X に送信します。X は平文として読み取れないバイトを保存し配信します。 + + アプリは Chat API の **send message** エンドポイントを介して、暗号化ペイロードと署名を X に送信します。X は平文として読めないバイト列を保存・配信します。 ### メッセージの受信 - - アプリは X から暗号文を受信します — [Webhook またはアクティビティストリーム](/xchat/real-time-events)経由、または履歴のために会話**イベント**を読み取ることで。 + + アプリは X から暗号文を受信します——[Webhook またはアクティビティストリーム](/ja/xchat/real-time-events)経由、または履歴用に会話の **events** を読み取ることで受信します。 - - キャッシュされた未加工の鍵を使うか、これが新規または鍵ローテーション後の場合は鍵配布(鍵変更)イベントからあなたのコピーを復号して取得します。 + + キャッシュした生の鍵を使うか、新規または新しくローテーションされた会話の場合は鍵配布(key change)イベントから自分のコピーを復号して取得します。 - - Chat XDK は送信者の署名公開鍵(および関連する identity バインディング)を使って署名をチェックするので、誰が送ったか、そして変更されていないことを知ることができます。 + + Chat XDK が送信者の署名公開鍵(および関連するアイデンティティ束縛)を使って署名を検証するので、誰が送ったか、そして改ざんされていないかがわかります。 - - Chat XDK は会話鍵で復号します。これで "Hello, how are you?" を読むことができます。 + + Chat XDK が会話鍵で復号します。これで「Hello, how are you?」と読めるようになります。 -暗号化、送信、受信、復号の実装は [Getting Started](/xchat/getting-started) と [Chat XDK](/xchat/xchat-xdk) リファレンスにあります。 +暗号化、送信、受信、復号の実装は[はじめに](/ja/xchat/getting-started)と [Chat XDK](/ja/xchat/xchat-xdk) リファレンスで確認できます。 --- -## 鍵配布の説明 +## 鍵配布の解説 -エンドツーエンド暗号化における中心的な課題は**鍵配布**です: X(または観察者)が鍵を平文で見ることなく、参加者が会話鍵をどうやって受け取るか。 +エンドツーエンド暗号化の中心的な課題は**鍵配布**です。X(または観察者)に会話鍵を平文で見せることなく、いかにして参加者に配るかという問題です。 -### 初期鍵セットアップ +### 初回の鍵セットアップ -会話がメッセージング用に準備される際: +会話でメッセージのやり取りができるように準備される時: -1. ランダムな会話鍵が生成されます(Chat XDK 内) -2. **各参加者**について、その鍵は彼らの **identity 公開鍵**で暗号化されます -3. それらの暗号化コピーは X の Chat API 経由で保存および配信されます -4. 各参加者は**自分の**コピーを identity 秘密鍵で復号します(Chat XDK 内) +1. Chat XDK がランダムな会話鍵を生成します +2. Chat XDK が**各参加者のアイデンティティ公開鍵**でその鍵を暗号化します +3. あなたのアプリは X の Chat API を通じてこれらの暗号化コピーを公開します +4. 各参加者は自分のアイデンティティ秘密鍵で**自分の**コピーを復号します(Chat XDK 内で) -X が扱うのは**ラップされた**コピーだけで、生の会話鍵ではありません。 +X が扱うのは**ラップされた**コピーのみで、生の会話鍵ではありません。 ### 鍵変更イベント -会話鍵がローテーションされる(たとえばメンバーシップが変更された)とき、参加者は各メンバー用の新しい暗号化コピーを含む**鍵変更**イベントを受け取ります。 +会話鍵がローテーションされると(たとえばメンバーが変更された時)、参加者は各メンバー向けの新しい暗号化コピーを含む **key change** イベントを受け取ります。 -あなたのアプリは次のようにすべきです: +アプリは次の処理を行うべきです。 -1. ライブイベントや会話履歴で鍵変更素材に気づく -2. 新しい会話鍵(およびバージョン)を復号して保存する -3. 以降の送信には最新バージョンを使用する +1. ライブイベントや会話履歴で鍵変更素材に気付く +2. 新しい会話鍵(とバージョン)を復号して保存する +3. 以降の送信で最新のバージョンを使用する -これらのイベントが実際にどこに現れるかは [Getting Started](/xchat/getting-started#6-receive-and-decrypt) と [リアルタイムイベント](/xchat/real-time-events) が説明しています。 +[はじめに](/ja/xchat/getting-started#6-receive-and-decrypt)と[リアルタイムイベント](/ja/xchat/real-time-events)で、実際にこれらのイベントがどこに現れるかを説明しています。 --- -## 安全な鍵バックアップ: 分散鍵ストレージ +## セキュアキーバックアップ:分散型鍵ストレージ -あなたの **identity と署名の秘密鍵**は慎重に保存されなければなりません。X Chat には**安全な鍵バックアップ**システム(Juicebox で実装)が含まれており、単一のサーバーに完全な秘密を渡すことなく、パスコードを使ってデバイス間で鍵を復元できます。 +**秘密の**アイデンティティ鍵と署名鍵は慎重に保管する必要があります。X Chat には**セキュアキーバックアップ**の仕組みが含まれており、単一のサーバーに完全な秘密を渡すことなく、パスコードを使って複数デバイス間で鍵を復元できます。 -### 従来の鍵ストレージの問題点 +### 従来の鍵ストレージの問題 -| アプローチ | 問題 | +| 手法 | 問題 | |:---------|:--------| -| デバイスにのみ保存 | デバイスを紛失 = 鍵を失う = メッセージ履歴へのアクセスを失う | -| 通常のクラウドバックアップに保存 | プロバイダーが鍵素材にアクセスする可能性がある | -| 長い鍵を覚える | 人間は高エントロピーの鍵を確実に暗記できない | +| デバイスにのみ保存 | デバイスを失う = 鍵を失う = メッセージ履歴へのアクセスを失う | +| 通常のクラウドバックアップに保存 | プロバイダーが鍵素材にアクセスできる可能性がある | +| 長い鍵を暗記 | 高エントロピーな鍵を確実に暗記することは難しい | -### 安全な鍵バックアップがどう解決するか +### セキュアキーバックアップによる解決 -安全な鍵バックアップは**秘密分散**と**パスコード保護**を組み合わせます: +セキュアキーバックアップは**秘密分散**と**パスコード保護**を組み合わせます。 -1. 秘密鍵は**シェアに分割**されます -2. シェアは**独立した realm**(別々のサーバー)が保持します -3. **単一の realm** だけでは鍵を再構築するのに十分な情報がありません -4. 復元には**パスコード**と**十分な数の realm** の協力が必要です -5. 間違ったパスコードは**レート制限**されて推測を遅らせます +1. 秘密鍵を**シェアに分割**します +2. シェアは**独立したレルム**(別々のサーバー)に保管されます +3. **単一のレルム**だけでは鍵を再構成するのに十分な情報を持ちません +4. 復元にはあなたの**パスコード**と**十分な数のレルム**の協力が必要です +5. 誤ったパスコード試行は**レート制限**され、総当たり攻撃を遅らせます ```mermaid flowchart LR @@ -198,64 +198,66 @@ flowchart LR end ``` -単一の当事者が秘密全体を保持することなく、復元可能性(新しいデバイス + パスコード)を得ることができます。 +これにより、単一の当事者が完全な秘密を保持することなく、復元可能性(新しいデバイス + パスコード)を実現できます。 -通常のパスでは鍵バックアップサーバーを手動で設定する必要はありません。Chat XDK にはバックアップクライアントが含まれており、realm 設定は X API から公開鍵レコード上の **`juicebox_config`** として提供されます(このフィールド名は、基盤となる実装である Juicebox に由来します)。初回のパスコード保存と後のアンロックは Chat XDK 呼び出しです — Getting Started の [既存の鍵で初期化する](/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 にはバックアップクライアントが含まれており、レルム構成はあなたの public-key レコードの **`juicebox_config`** フィールドとして X API から取得します。初回のパスコード保存と以降のアンロックは Chat XDK の呼び出しで行います——はじめにの [既存の鍵で初期化する](/ja/xchat/getting-started#2-initialize-the-chat-xdk-with-existing-keys) と [鍵を作成して登録する](/ja/xchat/getting-started#3-create-and-register-keys-first-time-setup) を参照してください。一部のアプリ(特にサーバーやボット)はセキュアキーバックアップではなくエクスポートした鍵ブロブを使用します。その素材はパスワードのように保護してください。 --- -## 署名の説明 +## 署名の解説 -すべての X Chat メッセージには、以下をサポートする**デジタル署名**が含まれます: +すべての X Chat メッセージには**デジタル署名**が含まれ、次の 2 つを支えます。 -1. **真正性** — 送信者の署名秘密鍵で生成されたこと -2. **完全性** — 暗号化されたコンテンツが署名後に変更されていないこと +1. **真正性** — 送信者の署名秘密鍵で作成されたこと +2. **完全性** — 暗号化された内容が署名後に改変されていないこと -### 署名の仕組み(概念的に) +### 署名の仕組み(概念) -| アクション | 使用する鍵 | 結果 | +| 操作 | 使用する鍵 | 結果 | |:-------|:---------|:-------| -| **署名** | 送信者の署名秘密鍵 | この暗号化メッセージに正確にバインドされた署名 | -| **検証** | 送信者の署名公開鍵 | 署名がメッセージと鍵に一致することを確認 | +| **署名** | 送信者の署名秘密鍵 | この暗号化メッセージにひもづく署名 | +| **検証** | 送信者の署名公開鍵 | 署名がメッセージおよび鍵と一致することを確認 | -署名対象の何かが変更されると、検証は失敗します。その鍵の有効な署名を生成できるのは、署名秘密鍵を持つ人だけです。 +署名された素材の一部でも変更されると検証は失敗します。その鍵に対する有効な署名を作成できるのは、署名秘密鍵を持つ者だけです。 -### アプリでの利用 +### アプリでの動作 -Chat XDK は送信メッセージを暗号化するときに署名し、受信メッセージを復号するときに(公開鍵 API からの)送信者の公開鍵素材に対して検証します。検証は**デフォルトで必須**です: SDK は明示的にチェックを無効にしない限り、検証されていない署名付きイベントを拒否します(推奨されません)。詳細は [Chat XDK](/xchat/xchat-xdk) リファレンスにあります。 +Chat XDK は送信メッセージを暗号化する際に署名し、受信メッセージを復号する際に送信者の公開鍵素材(public-key API から取得)に対して検証を行います。検証は**デフォルトで必須**です:SDK は明示的にチェックを無効化しない限り、検証されていない署名付きイベントを拒否します(推奨されません)。詳細は [Chat XDK](/ja/xchat/xchat-xdk) リファレンスにあります。 -### 署名付き状態変更(アクション署名) +署名は引用された内容もカバーします。返信は引用している生の**署名済み**元メッセージを埋め込みます。Chat XDK が返信を復号する際、埋め込まれた元メッセージを検証し、引用と比較して、結果を `reply_preview_validation`(`Valid` / `Invalid`)として報告します。`Invalid` の結果は、引用が署名済みの元メッセージと一致しないことを意味します——返信自体は別途検証されていますが、引用素材は信頼できないものとして扱ってください——これにより、いかなる参加者も他者に捏造した発言を帰属させることはできません。 -メッセージだけが署名対象ではありません。会話の状態を変更するすべての呼び出し — 会話鍵の追加やローテーション、グループの作成、メンバーの追加 — は、1 つ以上の**アクション署名**を伴う必要があります: 送信者は変更が正確に何をするかを記述したペイロードに署名し(鍵変更の場合、そのペイロードには新しい会話鍵自体が含まれます)、署名が欠落または不正な形式の場合、API はリクエストを拒否します。 +### 署名付きの状態変更(アクション署名) -サーバーは平文の会話鍵を保持しないため、鍵変更の署名を暗号学的にチェックできません。受信したリクエストと、署名され、エンコードされた変更の説明が一致することを検証します。**暗号学的な**チェックはエッジで行われます: 各受信者の Chat XDK は、鍵変更イベントを復号する際に、送信者の署名公開鍵に対して署名を検証します。Chat XDK の `prepare` メソッドはこれらの署名を生成してくれます — グループの作成とメンバー追加は**2 つ**(鍵変更とグループアクション)を返し、両方を送信する必要があります。 +署名される素材はメッセージだけではありません。会話の状態を変える呼び出し(会話鍵の追加やローテーション、グループの作成、メンバーの追加)はすべて、1 つ以上の**アクション署名**を伴う必要があります。送信者は変更内容を厳密に記述するペイロードに署名し(鍵変更の場合、そのペイロードには新しい会話鍵そのものが含まれます)、署名がない、または不正な形式の場合、API はリクエストを拒否します。 -署名はイベントの内容にバインドされ、不変です: 署名が検証できないイベントは、将来的に有効になることはありません。それらの扱い方については [トラブルシューティング](/xchat/troubleshooting) を参照してください。 +サーバーは平文の会話鍵を保持しないため、鍵変更の署名を暗号学的に検証することはできません。サーバーは変更の署名済みエンコード記述が受信したリクエストと一致することを検証します。**暗号学的**な検証はエッジで行われます:各受信者の Chat XDK が鍵変更イベントを復号する際に、送信者の署名公開鍵に対して署名を検証します。Chat XDK の `prepare` メソッドはこれらの署名を代わりに生成します——グループ作成とメンバー追加は**2 つ**の署名(鍵変更とグループアクション)を返し、両方を送信する必要があります。 + +署名はイベントの内容と結び付いており、不変です:署名が検証されないイベントが後から有効になることはありません。それらの扱いは[トラブルシューティング](/ja/xchat/troubleshooting)を参照してください。 --- -## セキュリティプロパティ +## セキュリティ特性 -### X Chat が保護する対象 +### X Chat が保護するもの | 脅威 | 保護 | |:-------|:-----------| -| **X がメッセージ本文を読む** | コンテンツは X に送信される前に暗号化される | -| **ネットワーク盗聴者** | 転送セキュリティに加えてエンドツーエンド暗号化されたコンテンツ | -| **メッセージの改ざん** | 署名が変更を検出する | +| **X がメッセージ本文を読むこと** | コンテンツは X に送信される前に暗号化される | +| **ネットワーク盗聴者** | トランスポートセキュリティに加えてエンドツーエンド暗号化されたコンテンツ | +| **メッセージの改ざん** | 署名により改変を検出 | | **単純な送信者なりすまし** | 有効な署名には送信者の署名秘密鍵が必要 | -| **単一サーバー鍵盗難(安全な鍵バックアップ使用時)** | シェアは realm 間で分散され、パスコードによってゲートされる | +| **単一サーバーからの鍵の盗難(セキュアキーバックアップ使用時)** | シェアは複数のレルムに分散されパスコードで保護される | -### X Chat が**保護しない**対象 +### X Chat が保護**しない**もの -| 脅威 | なぜか | +| 脅威 | 理由 | |:-------|:--------| -| **侵害されたデバイス** | 平文と鍵がアンロックされたクライアントで露出する可能性がある | -| **メタデータ** | X は誰がいつ誰にメッセージを送ったかを知ることができる — メッセージ本文は知らない | -| **前方秘匿性** | identity 鍵の侵害により、それらの鍵にラップされた会話鍵が露出する可能性がある | -| **侵害後セキュリティ** | 鍵をローテーションしても履歴は書き換えられない | +| **侵害されたデバイス** | アンロックされたクライアント上では平文や鍵が露出する可能性がある | +| **メタデータ** | X は誰と誰がいつやり取りしたかを知ることができる——ただしメッセージのテキストは知らない | +| **前方秘匿性** | アイデンティティ鍵の侵害により、それらの鍵にラップされた会話鍵が露出する可能性がある | +| **侵害後のセキュリティ** | 鍵をローテーションしても履歴は書き換えられない | --- @@ -263,33 +265,33 @@ Chat XDK は送信メッセージを暗号化するときに署名し、受信 | 用語 | 定義 | |:-----|:-----------| -| **対称暗号化** | 同じ鍵で暗号化と復号を行う(メッセージとメディアストリームで使用) | -| **非対称暗号化** | 暗号化と復号で異なる鍵(会話鍵をラップするために使用) | -| **公開鍵** | 共有しても安全;誰かに*向けて*暗号化するか、彼らの署名を検証するために使用 | -| **秘密鍵** | 秘密のままにしなければならない;復号または署名に使用 | -| **キーペア** | リンクされた公開鍵と秘密鍵 | -| **ECDH / ECIES** | 会話鍵を identity 鍵にラップするときに使用するアルゴリズム | -| **ECDSA** | メッセージ作成者に使用される署名アルゴリズム | -| **P-256** | X Chat で使用される楕円曲線(secp256r1) | -| **会話鍵** | 1 つの会話の参加者間で共有される対称鍵(時間経過でバージョン化される) | -| **秘密分散** | 秘密を分割して、再構築するために複数のピースが必要になるようにすること | -| **Realm** | 鍵素材の 1 シェアを保持する独立した安全な鍵バックアップサーバー | +| **対称鍵暗号** | 同じ鍵で暗号化と復号を行う(メッセージやメディアストリームに使用) | +| **非対称鍵暗号** | 暗号化と復号で異なる鍵を使う(会話鍵の交換に使用) | +| **公開鍵** | 共有して安全。誰かに暗号化して送るとき、または署名の検証に使う | +| **秘密鍵** | 秘密に保つ必要がある。復号や署名に使用 | +| **鍵ペア** | 関連付けられた公開鍵と秘密鍵 | +| **ECDH / ECIES** | アイデンティティ鍵を介して会話鍵をやり取りする際に使われるアルゴリズム | +| **ECDSA** | メッセージの作者性に使われる署名アルゴリズム | +| **P-256** | X Chat で使われる楕円曲線(secp256r1) | +| **会話鍵** | 1 つの会話の参加者が共有する対称鍵(時系列でバージョン管理される) | +| **秘密分散** | 秘密を分割し、再構成に複数のピースを必要とすること | +| **レルム** | 鍵素材のシェアを 1 つ保持する独立したセキュアキーバックアップサーバー | --- ## 次のステップ - + 鍵、送信、受信をステップバイステップで実装 - + 暗号化 SDK のメソッドと型 - - 製品概要とアーキテクチャ + + プロダクトの概要とアーキテクチャ - - 暗号化イベントがどのように配信されるか + + 暗号化イベントの配信方法 diff --git a/ja/xchat/getting-started.mdx b/ja/xchat/getting-started.mdx index f1a555f1d..52b6374d0 100644 --- a/ja/xchat/getting-started.mdx +++ b/ja/xchat/getting-started.mdx @@ -1,29 +1,29 @@ --- -title: Chat API を始める -sidebarTitle: Getting Started -description: Python、TypeScript、Go、Rust、C#、Java の Chat XDK を使って、エンドツーエンド暗号化された X Chat メッセージングを構築するチュートリアル。 +title: Chat API のはじめに +sidebarTitle: はじめに +description: Chat XDK を Python、TypeScript、Go、Rust、C#、Java のいずれかで使い、エンドツーエンド暗号化された X Chat メッセージングを構築するステップバイステップのチュートリアル。 keywords: ["X Chat tutorial", "X Chat quickstart", "Chat XDK", "encrypted DM", "Python", "TypeScript", "Go", "Rust", "C#", "Java"] --- -X 上でエンドツーエンド暗号化されたダイレクトメッセージを送受信します: 鍵をセットアップし、会話を初期化し、メッセージを送信し、受信トラフィックを復号します。 +X 上でエンドツーエンド暗号化されたダイレクトメッセージの送受信を行います:鍵をセットアップし、会話を初期化し、メッセージを送信し、受信トラフィックを復号します。 -X Chat アプリは、2 つの要素を組み合わせて使用します: +X Chat アプリは 2 つの要素を組み合わせて使います。 | コンポーネント | 役割 | |:----------|:-----| -| **[Chat XDK](/xchat/xchat-xdk)** | 暗号化、復号、署名、および秘密鍵の保存(安全な鍵バックアップまたは鍵 blob) | -| **X API** | 公開鍵、会話鍵、メッセージ、イベント — [Python](/xdks/python/overview) または [TypeScript](/xdks/typescript/overview) XDK 経由、または HTTPS とユーザーアクセストークン経由 | +| **[Chat XDK](/ja/xchat/xchat-xdk)** | 暗号化、復号、署名、および秘密鍵の保管(セキュアキーバックアップまたは鍵ブロブ) | +| **X API** | 公開鍵、会話鍵、メッセージ、イベント——[Python](/xdks/python/overview) や [TypeScript](/xdks/typescript/overview) の XDK 経由、あるいは HTTPS とユーザーアクセストークンで呼び出し | **前提条件** -- [開発者アカウント](https://developer.x.com/en/portal/petition/essential/basic-info) と OAuth 2.0 用に構成されたアプリ -- `dm.read`、`dm.write`、`tweet.read`、`users.read` を持つユーザーアクセストークン +- [開発者アカウント](https://developer.x.com/en/portal/petition/essential/basic-info) と OAuth 2.0 に対応するように構成された App +- `dm.read`、`dm.write`、`tweet.read`、`users.read` スコープを持つユーザーアクセストークン --- -## 1. 依存関係をインストールする +## 1. 依存関係のインストール @@ -31,7 +31,7 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: pip install chatxdk xdk ``` - PyPI パッケージは `chatxdk` です。`chat_xdk` としてインポートします。Python 3.10+ が必要です。 + PyPI のパッケージ名は `chatxdk` で、`chat_xdk` としてインポートします。Python 3.10 以上が必要です。 ```bash @@ -39,17 +39,16 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: npm install juicebox-sdk # optional peer dependency — required for setup()/unlock() secure key backup ``` - コンパイル済みの WASM エンジンは `@xdevplatform/chat-xdk` に同梱されています。ビルド手順は不要です。Node.js 18+ が必要です。 + コンパイル済みの WASM エンジンは `@xdevplatform/chat-xdk` に同梱されています——ビルドステップは不要です。Node.js 18 以上が必要です。 ```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] @@ -61,29 +60,29 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: go get github.com/xdevplatform/chat-xdk/go/chatxdk ``` - プリコンパイル済みの静的ライブラリが含まれています(macOS arm64/amd64、Linux amd64 glibc/musl)。C コンパイラは必要ですが、Rust は不要です。Go 1.21+ が必要です。 + プリコンパイル済みの静的ライブラリが同梱されています(macOS arm64/amd64、Linux amd64 glibc/musl)——C コンパイラは必要ですが Rust は不要です。Go 1.21 以上が必要です。 ```bash 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 ``` - Maven Central で入手できます。jar には macOS(arm64、x64)、Linux(x64)、Windows(x64)用のネイティブライブラリが同梱されており、`jna.library.path` の設定は不要です。`com.x.chatxdk` からインポートします。JDK 17+ が必要です。 + Maven Central で提供されています。jar には macOS(arm64, x64)、Linux(x64)、Windows(x64)向けのネイティブライブラリが同梱されており、`jna.library.path` のセットアップは不要です。`com.x.chatxdk` からインポートします。JDK 17 以上が必要です。 -**ユーザー** OAuth 2.0 アクセストークンで API クライアントを作成します: +**ユーザー**の OAuth 2.0 アクセストークンで API クライアントを作成します。 @@ -133,14 +132,14 @@ X Chat アプリは、2 つの要素を組み合わせて使用します: ## 2. 既存の鍵で Chat XDK を初期化する -このステップでは、**すでに持っている鍵をロード**します — この identity が過去に初回セットアップを完了している場合に使用してください: +このステップは**既に所有している鍵をロード**します——このアイデンティティで初回セットアップを完了済みの場合に使用します。 -- **安全な鍵バックアップ:** 公開鍵レコードの `juicebox_config` で SDK を構築し、パスコードで `unlock` して秘密鍵を復元します(たとえば新しいデバイスで)。 -- **鍵 blob:** 以前に `export_keys` でエクスポートした blob を `import_keys` に渡します。 +- **セキュアキーバックアップ:** public-key レコードから取得した `juicebox_config` で SDK を構築し、`unlock` にパスコードを渡して秘密鍵を復元します(たとえば新しいデバイスで)。 +- **鍵ブロブ:** 以前 `export_keys` でエクスポートしたブロブを `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-鍵を作成して登録する初回セットアップ) に進んで鍵の作成、バックアップ、登録を行ってください。 +**初回セットアップの場合は?** 同じように 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,28 +237,34 @@ 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); } ``` -サーバーおよびボットのサンプルでは、通常**鍵 blob**(`export_keys` / `import_keys`)を使用します。クライアントアプリでは通常**安全な鍵バックアップ**(パスコードを使った `setup` / `unlock`)を使用します。両方のパスについては [Chat XDK](/xchat/xchat-xdk) リファレンスを参照してください。 +サーバーやボットのサンプルでは**鍵ブロブ**(`export_keys` / `import_keys`)がよく使われます。クライアントアプリでは**セキュアキーバックアップ**(パスコードを使う `setup` / `unlock`)がよく使われます。両方のパスについては [Chat XDK](/ja/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` が生成した不透明なブロブのみを受け付けます——これは完全な鍵状態のバージョン管理された内部シリアライズであり、生の鍵や PEM エンコードされた P-256 鍵ではありません。このブロブを自分で構築することはできません:[ステップ 3](#3-create-and-register-keys-first-time-setup) の `generate_keypairs` を通じて鍵を生成し、一度ブロブをエクスポートして、base64 エンコードして保存してください。手作りしたり改変したブロブはインポートに失敗します。 --- -## 3. 鍵を作成して登録する(初回セットアップ) +## 3. 鍵を作成して登録する(初回セットアップ) + +[ステップ 2](#2-initialize-the-chat-xdk-with-existing-keys) で既存の鍵をロードした場合は、このステップをスキップしてください。それ以外の場合、新しいアイデンティティの一度きりのセットアップでは**次の 3 つ**を行います。 + +1. **鍵ペアを作成する** — `generate_keypairs` がアイデンティティ鍵ペアと署名鍵ペアを生成します。 +2. **秘密鍵を保管する** — パスコードを使う `setup` はセキュアキーバックアップに書き込みます(クライアント)。または `export_keys` が安全に保管するための鍵ブロブを返します(サーバーやボット)。 +3. **公開鍵を登録する** — 他者があなたに暗号化したり、あなたの署名を検証したりできるように、登録ペイロードを add-public-key エンドポイントに POST します。 -[ステップ 2](#2-既存の鍵で-chat-xdk-を初期化する) で既存の鍵をロードした場合は、このステップをスキップしてください。それ以外の場合、新しい identity の 1 回限りのセットアップでは **3 つのこと**を行います: +最後に、登録の鍵バージョンで `set_identity` を呼び出し、このセッションが新しいアイデンティティとして署名するようにします。 -1. **キーペアを作成する** — `generate_keypairs` が identity と署名のキーペアを生成します。 -2. **公開鍵を登録する** — 他のユーザーがあなた宛てに暗号化し、あなたの署名を検証できるよう、登録ペイロードを add-public-key エンドポイントに POST します。 -3. **秘密鍵を保存する** — パスコードで `setup` すると安全な鍵バックアップに書き込まれます(クライアント)。または `export_keys` が返す鍵 blob を安全に保存します(サーバーとボット)。 + +すぐに実行できる、各バインディング用の一度きりの登録スクリプトが [`chat-xdk/examples`](https://github.com/xdevplatform/chat-xdk/tree/main/examples) にあります(Python、TypeScript、Go、Rust、C#、Java)。新しいアイデンティティをオンボードするだけであれば、下記のフローを手作業で実装するのではなく、これらを使ってください。 + @@ -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,25 +384,25 @@ 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"); ``` -安全な鍵バックアップには強力なパスコードを使用してください。パスコードを紛失したり、保護されていない鍵 blob を失うと、過去のメッセージの復号ができなくなる可能性があります。 +セキュアキーバックアップには強力なパスコードを使用してください。パスコードを失うか、保護されていない鍵ブロブを失うと、過去のメッセージを復号できなくなる可能性があります。 --- ## 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`** を呼び出します。送信者のアイデンティティはステップ 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 します。会話鍵が漏洩したと疑われる場合はローテーションしてください — ローテーションは**将来の**メッセージのみを保護します;以前の鍵バージョンで暗号化されたメッセージは、そのバージョンを保持している人にとっては引き続き読み取り可能です。 +レスポンスは正規の会話 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)` でチェックし(public-keys API のレコードの `public_key`、`signing_public_key`、`identity_public_key_signature` フィールドを渡します)、置き換えられた identity 鍵が会話鍵を受け取れないようにしてください。 +**ラップする前に取得した鍵を検証してください。** `prepare_conversation_key_change` は渡されたどんな公開鍵に対しても新しい会話鍵を暗号化します。取得した各レコードをまず `verify_key_binding(identity, signing, signature)` でチェックし——public-keys API から得たレコードの `public_key`、`signing_public_key`、`identity_public_key_signature` の各フィールドを渡してください——差し替えられたアイデンティティ鍵が会話鍵を受け取れないようにします。 @@ -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` プレフィックス付き)はそのまま渡されます。 +API がハイフン付きの会話 ID を必要とする場合(`:` → `-`)は URL パスでそれを使用します。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,22 +822,26 @@ API で要求される場合、URL パスでは**ハイフン付きの**会話 I + +このフローではステップ 4 で作成したばかりなので、スニペットでは会話鍵を明示的に渡しています。鍵キャッシュがオンで、`decrypt_events` パスが会話の鍵を検証した後([ステップ 6](#6-receive-and-decrypt))は、`encrypt_message(conversation_id, text)` だけで十分です——SDK が最新の検証済み鍵を自動で埋めます。リトライでは**同じ**暗号化ペイロードを再送信すべきなので、ID が二度発行されることはありません。 + + --- -## 6. 受信して復号する +## 6. 受信と復号 -ライブトラフィックには [Webhook またはアクティビティストリーム](/xchat/real-time-events) を使用します。履歴には会話**イベント**をページングします。 +ライブトラフィックには [Webhook かアクティビティストリーム](/ja/xchat/real-time-events)を使い、履歴には会話 **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) を参照) -- JavaScript は camelCase のイベントタイプ(`message`)を使用します;他の言語は JSON で `"Message"` と snake_case フィールドを使用します +- ライブペイロードフィールド:`encoded_event`、オプションで `conversation_key_change_event` +- 履歴:`GET /2/chat/conversations/{id}/events` — すべてのイベントに `meta.conversation_key_events` を加えて **`decrypt_events`** を使うのが推奨 +- 復号には、SDK が各メッセージを誰が書いたかを検証できるように、送信者の**署名鍵**が必要です。これらは他の参加者の *公開* 鍵です——ステップ 4 で使ったのと同じ public-keys エンドポイントから取得し、フィールドを `SigningKeyEntry` にマップします(下のスニペットにマッピングが含まれています) +- 呼び出しごとに署名鍵(および `decrypt_event` の場合は会話鍵)を渡すか、**あるいは** 2 つのオプションのセッションストアを一度セットして短い呼び出し形式を使用できます。下のスニペットはストアを使用しています:`set_signing_keys(entries)` は参加者の鍵を保持し、`set_cache_keys(true)`(既定でオフ)は各会話の最新の**署名検証済み**鍵を保持するので、後の呼び出しは鍵引数を省略できます。どちらのスタイルでも検証は同じです +- JavaScript は camelCase のイベントタイプ(`message`)を使いますが、他の言語は `"Message"` と 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 @@ 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 インスタンスのメモリ内に存在します。それが機能しない状況——ある呼び出しが復号し、別の呼び出しが送信する——では、代わりに鍵を明示的に渡してください:`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)。 --- ## ベストプラクティス -- 未加工の会話鍵と送信者の公開鍵をキャッシュし、署名検証が失敗した場合は更新する -- `event_uuid` でライブ配信の重複を排除する -- ページネーションが完了するまでイベント履歴をページングし、鍵変更のメタデータを見逃さないようにする -- 本番環境でパスコード、秘密鍵、メッセージ平文をログに残さない -- Web アプリでは、OAuth トークン(および鍵バックアップ realm トークン発行)をサーバー側で保持し、秘密鍵はクライアントの Chat XDK 内でのみ保持することが望ましい - ---- - -## 次のステップ - - - - すべての言語バインディングのメソッドと型 - - - 暗号化された画像とファイル添付 - - - 複数参加者の会話とメタデータ - - - Webhook とアクティビティ配信 - - +- 署名鍵ストアを最新に保つ:送信者が新しい鍵バージョンを登録した場合は完全な参加者セットで `set_signing_keys` を呼び直し、署名検証失敗時にも更新してください +- ライブ配信を `event_uuid` で重複排除してください diff --git a/ja/xchat/groups.mdx b/ja/xchat/groups.mdx index b2f0143f9..abf13c1ba 100644 --- a/ja/xchat/groups.mdx +++ b/ja/xchat/groups.mdx @@ -1,45 +1,46 @@ --- title: グループ会話 -sidebarTitle: Groups -description: 共有された会話鍵、暗号化されたタイトル、メンバー管理、署名付きメッセージを備えた複数参加者の X Chat グループ会話を作成します。 +sidebarTitle: グループ +description: 共有の会話鍵、暗号化されたタイトル、メンバー管理、署名付きメッセージを備えた、複数参加者の X Chat グループ会話を作成します。 keywords: ["X Chat groups", "group DM", "conversation keys", "group name encryption"] --- -グループチャットは、1:1 の X Chat と**同じ暗号化モデル**を使用します: メンバーで共有する 1 つの**会話鍵**を、各メンバーの **identity 公開鍵**にラップし、メッセージは Chat XDK によって暗号化・署名されます。変わるのは、**メンバーシップ**、**会話の作成方法**、そしてしばしば会話上の**暗号化されたタイトル/アバター**フィールドです。 +グループチャットは 1:1 の X Chat と**同じ暗号化モデル**を使用します:1 つの**会話鍵**をメンバーで共有し、各メンバーの**アイデンティティ公開鍵**でラップし、Chat XDK によってメッセージを暗号化・署名します。変わるのは**メンバーシップ**、**会話の作成方法**、そしてしばしば会話上の**暗号化されたタイトル/アバター**フィールドです。 -1:1 のフローは [Getting Started](/xchat/getting-started) にあります。エンドポイントの詳細は **API リファレンス → Conversations and messages** の下にあります。 +1:1 のフローは[はじめに](/ja/xchat/getting-started)にあります。エンドポイントの詳細は **API リファレンス → Conversations and messages** にあります。 --- -## グループと 1:1 の違い +## グループが 1:1 と異なる点 | トピック | 1:1 | グループ | |:------|:----|:------| -| 識別 | 多くの場合、パス内でピアユーザー ID によって指定される | 会話 ID は通常 `g` で始まる | -| 作成 | 鍵 + ユーザーへのメッセージ送信 | グループ作成/初期化 API、その後鍵 | -| 参加者 | あなた + 1 人のピア | 多数のユーザー;メンバーシップは変更可能 | -| メタデータ | 最小限 | 名前、アバターなどが**暗号文**の可能性あり(会話鍵で復号) | -| 鍵ローテーション | 頻度は少ない | メンバーの参加や離脱時によく発生 | +| アイデンティティ | パスでピアのユーザー ID がよく使われる | 会話 ID は通常 `g` で始まる | +| 作成 | ユーザーへの鍵 + メッセージング | グループの作成 / 初期化 API、次に鍵 | +| 参加者 | あなた + 1 人のピア | 多数のユーザー。メンバーシップは変わり得る | +| メタデータ | 最小限 | 名前、アバターなどが**暗号文**であり得る(会話鍵で復号) | +| 鍵ローテーション | 頻度は低い | 参加や退出時によく起こる | -暗号処理は依然として: 鍵とペイロードには **Chat XDK**、グループの作成、参加者鍵ラップの公開、メッセージの送信、イベントのロードには **X API** です。 +暗号処理は変わりません:鍵とペイロードには **Chat XDK**、グループの作成、参加者向け鍵ラップの公開、メッセージの送信、イベントのロードには **X API**。 --- -## グループの作成と鍵の確立 +## グループを作成して鍵を確立する -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 つ**のアクション署名(会話鍵の変更とグループ作成)を返します。 -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. 暗号化/復号のために、**未加工の**会話鍵と**バージョン**を保持します。 +1. `POST /2/chat/conversations/group/initialize` でグループ ID を発行します——レスポンスの `data.conversation_id` が、以降どこでも使う g プレフィックス付きの ID です。 +2. 各メンバーのアイデンティティ公開鍵と `public_key_version` をロードします(**Encryption keys** の下の公開鍵 `GET` ルート。[`GET /2/users/public_keys`](/x-api/users/get-public-keys-for-multiple-users) は複数のユーザーを 1 回のリクエストで取得します)。使用前に各レコードを `verify_key_binding` で検証してください([はじめに](/ja/xchat/getting-started#4-set-up-conversation-keys) の警告を参照)。 +3. **`prepare_group_create`** を、**すべての**メンバー(自分自身を含む)、g プレフィックス付きの ID、メンバー/管理者の ID リストとともに一度実行します。この 1 回の呼び出しで会話鍵を生成し、すべてのメンバー向けにラップし、`set_identity` からのセッションアイデンティティで作成に署名します——**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. 暗号化/復号のために**生の**会話鍵と**バージョン**を保持します。 -`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 @@ -107,19 +106,19 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt -参加者鍵とアクション署名のボディマッピング(`message_id`、`encoded_message_event_detail`、ネストされた `message_event_signature`)は、[Getting Started — conversation keys](/xchat/getting-started#4-set-up-conversation-keys) の鍵 POST と同じです。 +参加者鍵とアクション署名(`message_id`、`encoded_message_event_detail`、ネストされた `message_event_signature`)のボディマッピングは、[はじめに — 会話鍵](/ja/xchat/getting-started#4-set-up-conversation-keys) の keys POST と同じです。 -メンバーシップが変更されたときは、**`prepare_group_members_change`** を新しいメンバー ID と現在のロスター(メンバー、管理者、保留中のメンバー、および設定されている場合は現在のタイトル/アバター/TTL)とともに呼び出します。会話鍵をローテーションし、グループ作成と同様に**2 つ**のアクション署名を返します — すべてを **add members**(`POST /2/chat/conversations/{id}/members`)に POST します。その後、**鍵変更**トラフィックが期待されます: [Getting Started の鍵ローテーション](/xchat/getting-started#6-receive-and-decrypt) のように扱います(`extract_conversation_keys` / `decrypt_events`、その後最新バージョンで暗号化)。 +メンバーシップが変わるときは、新しいメンバー ID と現在の名簿(メンバー、管理者、保留中のメンバー、および現在のタイトル/アバター/TTL が設定されていればそれら)とともに **`prepare_group_members_change`** を呼び出します。会話鍵をローテーションし、グループ作成と同様に**2 つ**のアクション署名を返します——すべてを **add members**(`POST /2/chat/conversations/{id}/members`)に POST してください。その後、**鍵変更**トラフィックが来ることを想定してください:[はじめにの鍵ローテーション](/ja/xchat/getting-started#6-receive-and-decrypt)と同じように扱ってください(`extract_conversation_keys` / `decrypt_events`、その後に最新バージョンで暗号化)。 -`prepare_group_members_change` は、渡したロスターにのみラップされた**新しい**会話鍵を生成するため、新しいメンバーは新しい鍵バージョンを受け取り、以前のバージョンで送信されたメッセージを復号できません。その逆は真ではありません: ローテーションは**以前の**バージョンへのアクセスを取り消しません — 古い鍵をすでに持っている人は、その下で暗号化されたメッセージを読み続けることができます。会話鍵が漏洩したと疑われる場合は、`prepare_conversation_key_change` でローテーションしてください;これは将来のメッセージのみを保護します。 +`prepare_group_members_change` は渡した名簿にのみラップされた**新しい**会話鍵を生成するため、新規メンバーは新しい鍵バージョンを受け取り、以前のバージョンで送られたメッセージを復号することはできません。逆は成り立ちません:ローテーションは**以前の**バージョンへのアクセスを取り消しません——古い鍵を既に持っている人は誰でも、その鍵で暗号化されたメッセージを引き続き読めます。会話鍵が漏えいした疑いがある場合は `prepare_conversation_key_change` でローテーションしてください。これは将来のメッセージのみを保護します。 --- ## 暗号化されたグループメタデータ -一部の会話フィールド(たとえば表示**名**や**アバター 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,27 +165,27 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt -そのメタデータに適用される**現在の**会話鍵バージョンを使用してください。鍵がローテーションされている場合は、フィールドが書き込まれたときにアクティブだったバージョンで復号してください(または、メタデータがローテーション時に常に書き換えられる場合は製品のルールに従ってください)。 +そのメタデータに該当する**現在の**会話鍵バージョンを使用してください。鍵がローテーションされている場合は、フィールドが書き込まれた時点で有効だった鍵バージョンで復号してください(あるいはメタデータがローテーション時に常に書き直されるならプロダクトのルールに従ってください)。 --- ## メッセージとイベント -グループでの送受信は、未加工の会話鍵を持てば 1:1 と同じです: +生の会話鍵を持っていれば、グループでの送受信は 1:1 と同じです。 -- **送信:** `encrypt_message` → メッセージ送信 API([Getting Started](/xchat/getting-started#5-send-a-message)) -- **受信:** イベント API または[リアルタイム配信](/xchat/real-time-events) → `decrypt_event` / `decrypt_events` -- **メディア:** グループの会話 ID を使った [Media](/xchat/media) +- **送信:** `encrypt_message` → send-message API([はじめに](/ja/xchat/getting-started#5-send-a-message)) +- **受信:** events API または[リアルタイム配信](/ja/xchat/real-time-events) → `decrypt_event` / `decrypt_events` +- **メディア:** グループの会話 ID を使った[メディア](/ja/xchat/media) -メンバーシップに起因するローテーション後は、常に**最新の**鍵バージョンで暗号化してください。 +メンバーシップ由来のローテーション後は、常に**最新の**鍵バージョンで暗号化してください。 --- ## チェックリスト -1. `POST /2/chat/conversations/group/initialize` で g プレフィックス付き ID を発行する -2. **すべての**メンバーで `prepare_group_create`;参加者鍵ラップと**両方**のアクション署名を `POST /2/chat/conversations/group` に POST する -3. 未加工の鍵 + バージョンをキャッシュ;鍵変更イベントで更新する -4. メンバーシップ変更時は `prepare_group_members_change`(2 つの署名)→ `POST /2/chat/conversations/{id}/members` -5. フィールドが暗号文の場合は `decrypt` でグループメタデータを復号する -6. 1:1 と同じパターンで送受信する +1. `POST /2/chat/conversations/group/initialize` で g プレフィックス付きの ID を発行する +2. **すべての**メンバーで `prepare_group_create`。参加者鍵ラップと**両方**のアクション署名を `POST /2/chat/conversations/group` に POST する +3. 生の鍵 + バージョンをキャッシュし、鍵変更イベントで更新する +4. メンバーシップ変更時は `prepare_group_members_change`(2 つの署名)→ `POST /2/chat/conversations/{id}/members` +5. フィールドが暗号文の場合はグループメタデータを `decrypt` で復号する +6. 送受信は 1:1 と同じパターンで行う diff --git a/ja/xchat/media.mdx b/ja/xchat/media.mdx index 8565ec972..8fd533efd 100644 --- a/ja/xchat/media.mdx +++ b/ja/xchat/media.mdx @@ -1,15 +1,15 @@ --- title: メディアと添付ファイル -sidebarTitle: Media -description: Chat XDK のストリーム暗号化とメディアアップロードエンドポイントを使い、X Chat で画像やファイル添付を暗号化・アップロード・送信・ダウンロード・復号します。 +sidebarTitle: メディア +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 リファレンス → Media**)経由でアップロードし、`encrypt_message` で **`media_hash_key`** を添付します。 +画像やその他のファイルは、テキストと**同じ会話鍵**を使用します。Chat XDK でバイト列を暗号化し(`encrypt_stream` / `decrypt_stream`)、**`/2/chat/media/upload`** ルート(サイドバー **API リファレンス → Media**)経由でアップロードし、`encrypt_message` に **`media_hash_key`** を添付します。 -アップロード時は、DM スコープに **`media.write`** を含めてください。パスにはハイフン付きの会話 ID を使います(`:` → `-`)。MIME や寸法は**復号済みの**バイトから取得することが望ましいです。 +アップロード時は 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 ブロブは **`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` を 1 回呼び出します — ストリームが切り詰められた場合、`finish` はエラーを返します。 +`encrypt_stream` / `decrypt_stream` はペイロード全体をメモリ内で処理します。大きなファイルの場合は、`stream_encryptor()` / `stream_decryptor()` が増分オブジェクト(`StreamEncryptor` / `StreamDecryptor`)を返します:`push` でチャンクを送り込み、最後に一度 `finish` を呼びます——ストリームが切り捨てられていた場合、`finish` はエラーになります。 --- @@ -114,31 +114,27 @@ flowchart LR |:-----|:-------|:-----| | 初期化 | `POST` | `/2/chat/media/upload/initialize` | | 追加 | `POST` | `/2/chat/media/upload/{id}/append` | -| 確定 | `POST` | `/2/chat/media/upload/{id}/finalize` | +| 完了 | `POST` | `/2/chat/media/upload/{id}/finalize` | -**API リファレンス → Media** の下にある OpenAPI ページのリクエストボディを使用してください。サイズが必要な場合は、**暗号化済みの** blob サイズを優先してください。Finalize は添付とダウンロード用の **`media_hash_key`** を返します。一時的な `5xx` はバックオフで再試行してください。Python/TypeScript ではメディアヘルパーが存在する場合は XDK を使用できます;それ以外は任意の言語で Bearer トークンを使って POST します。 +**API リファレンス → Media** の OpenAPI ページのリクエストボディを使用してください。サイズが必要な場合は**暗号化された**ブロブサイズを優先してください。Finalize は添付やダウンロードのための **`media_hash_key`** を返します。一時的な `5xx` はバックオフしてリトライしてください。Python/TypeScript ではメディアヘルパーが存在すれば XDK を使えます。そうでない場合はどの言語でも Bearer トークンで POST してください。 --- -## 添付ファイル付きで送信する +## 添付付きで送信する -メディア添付付きで暗号化し、その後メッセージ送信ボディを POST します(フィールドマッピングは [Getting Started](/xchat/getting-started#5-send-a-message) と同じ)。 +メディア添付付きで暗号化し、send-message ボディを POST します([はじめに](/ja/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` は会話の最新の検証済み鍵変更から鍵とバージョンを解決します([はじめに](/ja/xchat/getting-started)を参照)。 + --- -## ダウンロードして復号する +## ダウンロードと復号 -パス: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/download-chat-media)。レスポンスボディは暗号文です。受信メッセージでは、復号された添付ファイル / `media_hashes` から `media_hash_key` を読み取ります。 +パス:[`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]` を使います。鍵ローテーション後(たとえばメンバー追加の後)、最新の鍵では古いメッセージに添付されたメディアを復号できません。 @@ -358,9 +368,9 @@ flowchart LR ## ヒント -- メディアが暗号化されたときと同じ**会話鍵バージョン**を使用する -- 平文メディアや未加工の鍵をログに出力しない -- MIME は復号**後**に検出する -- Web クライアント: 可能な場合はクライアント側で暗号化・復号する;OAuth トークンはサーバー側に保持する +- メディアが暗号化されたときと**同じ**会話鍵(およびバージョン)を使用してください +- 平文メディアや生の鍵をログに出力しないでください +- MIME は**復号後に**検出してください +- Web クライアント:可能な限りクライアントで暗号化/復号し、OAuth トークンはサーバー側に保持してください -各メディアルート(アップロード初期化、チャンク追加、アップロード確定、メディアダウンロード)の完全なリクエストとレスポンスのスキーマは、サイドバーの **API リファレンス → Media** の下にあります。 +各メディアルート(アップロード初期化、チャンク追加、アップロード完了、メディアダウンロード)の完全なリクエスト/レスポンススキーマは、サイドバーの **API リファレンス → Media** にあります。 diff --git a/ja/xchat/real-time-events.mdx b/ja/xchat/real-time-events.mdx index 7edc39d75..5739d1359 100644 --- a/ja/xchat/real-time-events.mdx +++ b/ja/xchat/real-time-events.mdx @@ -1,18 +1,18 @@ --- -title: リアルタイム X Chat イベント +title: X Chat のリアルタイムイベント sidebarTitle: リアルタイムイベント -description: Webhook またはアクティビティストリーム経由で chat.received、chat.sent などの暗号化された X Chat アクティビティを受信し、Chat XDK でペイロードを復号します。 +description: chat.received、chat.sent、その他の暗号化された X Chat のアクティビティを Webhook またはアクティビティストリームで受信し、Chat XDK でペイロードを復号します。 --- -X は **`chat.received`**、**`chat.sent`**、および関連する X Chat アクティビティを、ペイロード内の**暗号文**とともに配信します。[Chat XDK](/xchat/xchat-xdk) で復号します。 +X は **`chat.received`**、**`chat.sent`**、および関連する X Chat のアクティビティを、ペイロードに**暗号文**を含めて配信します。[Chat XDK](/ja/xchat/xchat-xdk) で復号してください。 | レイヤー | 役割 | |:------|:-----| -| **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` | +| **X Activity API** | `GET /2/activity/stream`; `POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions`(操作ごとに OpenAPI のセキュリティを参照) | +| **Webhook** | 独自の HTTPS URL で終端する場合はオプションの `POST` / `GET` `/2/webhooks` および `PUT` / `DELETE` `/2/webhooks/{webhook_id}` ルート | +| **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 メディアダウンロードを使用します。 +プライベート X Chat のイベントタイプでは、監視するユーザーに対する認可が必要です。暗号化された X Chat のファイル添付は **`media_hash_key`** と X Chat メディアダウンロードを使い、Post API の `expansions=attachments.media_keys` / `media.fields=variants` ではありません。 --- @@ -20,26 +20,26 @@ X は **`chat.received`**、**`chat.sent`**、および関連する X Chat ア | イベント | いつ | |:------|:-----| -| `chat.received` | サブスクライブされたユーザーが暗号化された DM を受信 | -| `chat.sent` | サブスクライブされたユーザーが暗号化された DM を送信 | -| `chat.conversation_join` | サブスクライブされたユーザーがグループに参加(提供時) | +| `chat.received` | 購読ユーザーが暗号化された DM を受信したとき | +| `chat.sent` | 購読ユーザーが暗号化された DM を送信したとき | +| `chat.conversation_join` | 購読ユーザーがグループに参加したとき(提供時) | --- -## 1. 配信方法を選択する +## 1. 配信方式を選ぶ -**アクティビティストリーム(ボットには多くの場合最もシンプル):** アプリの Bearer トークンで `GET /2/activity/stream`(オプションの `backfill_minutes`、`start_time`、`end_time` は OpenAPI ごと)。クライアント側で `chat.received` / `chat.sent` をフィルタリングします。 +**アクティビティストリーム(多くのボットで最もシンプル):** App の Bearer トークンで `GET /2/activity/stream`(OpenAPI に従いオプションで `backfill_minutes`、`start_time`、`end_time`)。クライアント側で `chat.received` / `chat.sent` をフィルタします。 -**アクティビティサブスクリプション:** 以下で永続的なサブスクリプションを管理します: +**アクティビティサブスクリプション:** 永続的なサブスクリプションを次で管理します。 - `POST /2/activity/subscriptions` — 作成 -- `GET /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)サブスクリプションの作成には、監視対象ユーザーに対する**ユーザーコンテキスト認可**(`dm.read` などチャットイベントに関連するスコープを持つ OAuth 2.0 ユーザーコンテキスト)が必要です。 +リクエストボディと必要なスコープは各ルートの OpenAPI 操作で定義されています。X Activity API(XAA)サブスクリプションの作成には、監視するユーザーに対する**ユーザーコンテキスト認可**(OAuth 2.0 のユーザーコンテキストと関連スコープ。チャットイベントには `dm.read` など)が必要です。 -**Webhook:** HTTPS エンドポイントでイベントを終端する場合、`POST /2/webhooks` で Webhook を登録し、CRC チャレンジをパスし、その後 `POST /2/activity/subscriptions` で `webhook_id` を参照してアクティビティサブスクリプションを作成します(OpenAPI の Webhooks および Activity オペレーションを参照)。Python/TypeScript XDK は、SDK のバージョンに含まれる場合、Webhook とアクティビティのヘルパーを公開することがあります。 +**Webhook:** HTTPS エンドポイントでイベントを終端する場合は、`POST /2/webhooks` で Webhook を登録し、CRC チャレンジに応答し、`POST /2/activity/subscriptions` で `webhook_id` を参照するアクティビティサブスクリプションを作成します(OpenAPI の Webhooks と Activity 操作を参照)。Python/TypeScript XDK では、SDK のバージョンに含まれていれば Webhook とアクティビティ用のヘルパーを公開している場合があります。 @@ -74,130 +74,88 @@ X は **`chat.received`**、**`chat.sent`**、および関連する X Chat ア -送信コピーが必要な場合は `chat.sent` にもサブスクライブしてください。他の言語: 同じ `/2/activity/*` HTTPS ルートを直接呼び出します(サブスクリプションの作成にはユーザーコンテキストトークン、ストリームにはアプリの Bearer トークン)。 +送信コピーも必要な場合は `chat.sent` も購読してください。他の言語では同じ `/2/activity/*` HTTPS ルートを直接呼び出します(サブスクリプション作成にはユーザーコンテキストトークン、ストリームには App の Bearer トークン)。 --- -## 2. CRC(Webhook のみ) +## 2. CRC(Webhook のみ) -Webhook を使用する場合、コンシューマシークレットを使ったトークンの HMAC-SHA256 を、Webhook 製品が期待する JSON 形式(通常は `sha256=`)で Challenge-Response Checks(GET `crc_token`)に応答してください。 +Webhook を使う場合は、Challenge-Response Check(GET `crc_token`)に対して、消費者シークレットを使ってトークンを HMAC-SHA256 したものを、Webhook プロダクトが期待する JSON 形式(通常は `sha256=`)で返してください。 --- ## 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 はバックエンドで割り当てられる署名なしのメタデータです。 -JavaScript は camelCase のイベントタイプ(`message`)を使用します;他のバインディングは `"Message"` と snake_case フィールドを使用します。 +以下のスニペットは、最も短いハンドラーになるように 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"` とスネークケースのフィールドを使います。 ```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,11 +197,13 @@ JavaScript は camelCase のイベントタイプ(`message`)を使用しま -履歴: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — [Getting Started](/xchat/getting-started#6-receive-and-decrypt) を参照。 +代わりに鍵マップを自分の手で保持したい場合、`extract_conversation_keys` は `conversation_key_change_event` から鍵を復号し、`decrypt_event` はそれら(および送信者の署名鍵)を明示的な引数として受け取ります——明示的で空でない引数は常にストアより優先されます。 + +履歴:[`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — [はじめに](/ja/xchat/getting-started#6-receive-and-decrypt) を参照してください。 --- -## ペイロードの形(ライブ) +## ペイロード形状(ライブ) ```json { @@ -256,7 +225,7 @@ JavaScript は camelCase のイベントタイプ(`message`)を使用しま ## プラクティス -- プラットフォームの要件に従って Webhook 署名を検証する -- 会話鍵と送信者の公開鍵をキャッシュする -- 依存メッセージを復号する前に鍵変更 blob を適用する -- `event_uuid` で重複を排除する +- プラットフォーム要件に沿って Webhook の署名を検証してください +- セッションストアは一度セットしてください:参加者全員に対して `set_signing_keys`、会話鍵には `set_cache_keys(true)` +- 依存メッセージを復号する前に鍵変更ブロブを適用してください(`decrypt_events` 経由) +- 配信は `event_uuid` で、メッセージは署名済みの `message_id` で重複排除してください diff --git a/ja/xchat/troubleshooting.mdx b/ja/xchat/troubleshooting.mdx index 783bba7ff..67e253550 100644 --- a/ja/xchat/troubleshooting.mdx +++ b/ja/xchat/troubleshooting.mdx @@ -1,22 +1,22 @@ --- 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 に固有の**問題(鍵、セキュアキーバックアップ、復号/検証、および暗号化された送信ペイロードの構築)を扱います。 -Webhook、OAuth、HTTP ステータスコード、レート制限については、一般的な [X API](/x-api/introduction) および [認証](/fundamentals/authentication/overview) のドキュメントを参照してください。 +Webhook、OAuth、HTTP ステータスコード、レート制限については、一般の [X API](/ja/x-api/introduction) および [認証](/ja/fundamentals/authentication/overview) のドキュメントを使用してください。 --- -## 鍵と安全な鍵バックアップ +## 鍵とセキュアキーバックアップ -### アンロックに失敗する(無効なパスコード) +### アンロックに失敗する(無効なパスコード) -- パスコードが `setup` で使用したものと一致するか確認する -- 試行の間で待機する;realm は間違った推測をレート制限し、多くの失敗の後に復元をロックすることがある +- パスコードが `setup` で使用したものと一致していることを確認してください +- 試行の間に待機してください。レルムは誤答をレート制限し、失敗が多すぎると復元をロックすることがあります @@ -62,80 +62,81 @@ Webhook、OAuth、HTTP ステータスコード、レート制限については -### 鍵がロードされていないため暗号化または復号に失敗する +### 鍵やアイデンティティがセットされていないため暗号化/復号が失敗する -まず秘密鍵をロードし、次に 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); ``` -### メッセージに対する会話鍵が欠落している +### メッセージに対する会話鍵が見つからない -そのメッセージの `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` に含める -2. そのバージョンの会話鍵が追加されていること、およびあなたがまだ参加者であることを確認する([Getting Started](/xchat/getting-started#4-set-up-conversation-keys) を参照) +1. `conversation_key_change_event`(ライブイベント)または `meta.conversation_key_events`(履歴)の鍵素材を `extract_conversation_keys` で復号する、**あるいは**それらのブロブを `decrypt_events` に含める。`set_cache_keys(true)` が有効なら、`decrypt_events` は各会話の最新の検証済み鍵も保持するため、以後の `decrypt_event` や `encrypt_*` の呼び出しでは省略できます +2. そのバージョンに対して会話鍵が追加されており、まだ参加者であることを確認してください([はじめに](/ja/xchat/getting-started#4-set-up-conversation-keys) を参照) -### ピアが公開鍵を持っていない +### ピアに公開鍵がない -彼らはオンボーディングを完了していない可能性があります。彼らが登録した後、**API リファレンス → Encryption keys** から `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` をロードしてください。 --- ## 復号と署名 -### 復号が失敗する +### 復号に失敗する -- 古いまたは間違った**未加工の**会話鍵、または間違った鍵バージョン -- 不完全な `encoded_event` 文字列 +- **生の**会話鍵が古い/誤っている、または鍵バージョンが間違っている +- `encoded_event` 文字列が不完全である - イベントタイプが復号可能なコンテンツとして扱える暗号化メッセージではない ### 署名が検証されない -検証は**デフォルトで fail-closed**(`reject_unverified = true`)です: SDK はすでに検証されていない署名付きイベントを拒否しているため、ここでの失敗はチェックをオンにする必要があるという意味ではなく、検証入力が間違っていることを意味します。一般的な原因: +検証は**デフォルトで失敗クローズ**です(`reject_unverified = true`):SDK は検証されていない署名付きイベントを既に拒否しているため、ここで失敗した場合は、チェックを有効にする必要があるのではなく、検証入力が誤っていることを意味します。よくある原因: -- **送信者**の署名鍵エントリが欠落または不完全(Chat XDK に必要なすべてのフィールド — [Chat XDK](/xchat/xchat-xdk) リファレンスを参照) -- 送信者がバージョンをローテーションした — 公開鍵を再取得する -- 受け入れフロアより下の鍵バージョンは検証されない +- **送信者の**署名鍵エントリが欠落または不完全(Chat XDK が必要とするすべてのフィールド——[Chat XDK](/ja/xchat/xchat-xdk) リファレンスを参照) +- 呼び出しに署名鍵が渡されておらず、`set_signing_keys` で保存されているものもない +- 送信者がバージョンをローテーションした——公開鍵を再取得してください +- 許容フロアより下の鍵バージョンは決して検証されません -`set_reject_unverified` セッターは、このデフォルトから**オプトアウト**する(`false`、推奨されません)ために存在します。以前に無効化した場合は、fail-closed のデフォルトを復元してください: +`set_reject_unverified` セッターは、このデフォルトから**オプトアウト**するために存在します(`false`。推奨されません)。以前に無効にした場合は、失敗クローズのデフォルトに戻してください。 @@ -170,45 +171,49 @@ 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 呼び出しでは治せません。これらのイベントは、再試行可能なエラーではなくトゥームストーンとして扱ってください。会話鍵をローテーションすると、その時点からクリーンで検証可能な履歴が始まります;新しいメッセージには影響しません。 +`signature missing or no matching signing key` や、**古い**イベントでの ECDSA 不一致のようなエラーは恒久的です。署名は不変であり、イベント自体から署名済みペイロードを再構築することで検証されます。そのため、異なるバイトに署名された(あるいはまったく署名されていない)イベントは、以後のロードでも毎回失敗します——リトライ、鍵の更新、API 呼び出しでは修復できません。これらのイベントはリトライ可能なエラーではなく tombstone として扱ってください。会話鍵をローテーションすれば、そこから先はクリーンで検証可能な履歴が始まります。新しいメッセージは影響を受けません。 --- ## 送信ペイロードの構築 -これらのミスは 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 だけ — すべては同じ署名バイトに正規化される | +| 鍵バイトが誤っている | API からの暗号化された鍵文字列ではなく、**生の**会話鍵バイトを Chat XDK に渡してください | +| JSON フィールド名が誤っている | `encrypted_content` → `encoded_message_create_event`、`encoded_event_signature` → `encoded_message_event_signature` をマップしてください | +| メッセージ ID が誤っている | 返されたペイロードの `message_id` を送信してください——SDK が生成し署名済みイベントに埋め込むため、他の値では失敗します。リトライ時は同じ暗号化ペイロードを再利用して、ID が二度発行されないようにしてください | +| バージョン不一致 | 使用する鍵と `conversation_key_version` を揃え、`set_identity` に渡す署名鍵バージョンを public-key レコードに揃えてください | +| パス ID の形式 | URL パスは依然としてハイフン付き会話 ID(`:` → `-`)が必要ですが、署名について SDK は任意の形式を受け付けます:`A:B`、`A-B`(順不同)、または裸の受信者ユーザー ID——すべて同じ署名バイトに正規化されます | -### 状態を変更する呼び出しに対して API が 400 を返す +### 状態を変更する呼び出しで API が 400 を返す -状態を変更するすべてのチャット呼び出し — 会話鍵の追加やローテーション、グループの作成、メンバーの追加 — は、リクエストボディに **`action_signatures`** を要求し、API 境界で検証されます。エントリの欠落や不正な形式(それぞれに `message_id`、`encoded_message_event_detail`、および `signature`、`public_key_version`、`signature_version` を持つ `message_event_signature` が必要)は、即座に HTTP 400 problem-details レスポンスを返します。SDK の prepare メソッド(`prepare_conversation_key_change`、`prepare_group_create`、`prepare_group_members_change`)を使い、返された**すべての**署名を送信してください — グループ作成とメンバー追加は 2 つ返します。 +状態を変更するすべてのチャット呼び出し(会話鍵の追加やローテーション、グループの作成、メンバーの追加)は、リクエストボディに **`action_signatures`** を必要とし、API 境界で検証されます。エントリが欠落または不正な形式(それぞれに `message_id`、`encoded_message_event_detail`、および `signature`、`public_key_version`、`signature_version` を持つ `message_event_signature` が必要)の場合、直ちに HTTP 400 problem-details レスポンスを返します。SDK の prepare メソッド(`prepare_conversation_key_change`、`prepare_group_create`、`prepare_group_members_change`)を使い、返された**すべての**署名を送信してください——グループ作成とメンバー追加は 2 つ返します。 --- ## メディアの暗号化と復号 -- 添付を参照するメッセージと**同じ**会話鍵(およびバージョン)を使用する -- ダウンロードレスポンスは `decrypt_stream` を実行するまで**暗号文**として扱う -- MIME タイプは復号**後**に推測する;ダウンロードの `Content-Type` は実際の画像タイプでないことがよくある +- 添付を参照するメッセージと**同じ**会話鍵(およびバージョン)を使用してください +- ダウンロードレスポンスは、`decrypt_stream` を実行するまで**暗号文**として扱ってください +- MIME タイプは**復号後に**推測してください。ダウンロードの `Content-Type` は多くの場合、実際の画像タイプではありません -詳細: [Media](/xchat/media)。 +詳細:[メディア](/ja/xchat/media)。 --- ## 安全なデバッグ -暗号処理の失敗を調査する際: +暗号化の失敗を調査する際は: -- 会話 ID、イベント ID、鍵の**バージョン**のみをログに記録する -- 平文、パスコード、秘密鍵、または完全な鍵 blob は**ログに記録しない** -- `set_key_version` が公開鍵レコードの `public_key_version` と一致することを確認する -- 履歴が不完全な場合、復号する前に鍵変更のメタデータがスキップされないよう、**すべての**イベントページをページングする +- 会話 ID、イベント ID、鍵の**バージョン**のみをログに出力してください +- 平文、パスコード、秘密鍵、完全な鍵ブロブは**ログに出力しないでください** +- `set_identity` に渡した署名鍵バージョンが public-key レコードの `public_key_version` と一致することを確認してください +- 履歴が不完全な場合は、鍵変更メタデータがスキップされないよう、復号前に**すべての**イベントページをページングしてください diff --git a/ja/xchat/xchat-xdk.mdx b/ja/xchat/xchat-xdk.mdx index 321d26c9f..3cfaf134e 100644 --- a/ja/xchat/xchat-xdk.mdx +++ b/ja/xchat/xchat-xdk.mdx @@ -1,13 +1,13 @@ --- 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)。 +アプリの解説:[はじめに](/ja/xchat/getting-started)。サンプルボット:[chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples)。 ### インストール @@ -17,7 +17,7 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] pip install chatxdk ``` - PyPI パッケージは `chatxdk` です。`chat_xdk` としてインポートします。Python 3.10+ が必要です。 + PyPI のパッケージ名は `chatxdk` で、`chat_xdk` としてインポートします。Python 3.10 以上が必要です。 ```bash @@ -25,14 +25,14 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] npm install juicebox-sdk # optional peer dependency — required for setup()/unlock() secure key backup ``` - コンパイル済みの WASM エンジンはパッケージに同梱されています。ビルド手順は不要です。Node.js 18+ が必要です。 + コンパイル済みの WASM エンジンはパッケージに同梱されています——ビルドステップは不要です。Node.js 18 以上が必要です。 ```toml [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] @@ -44,25 +44,25 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] go get github.com/xdevplatform/chat-xdk/go/chatxdk ``` - プリコンパイル済みの静的ライブラリが含まれています(macOS arm64/amd64、Linux amd64 glibc/musl)。C コンパイラは必要ですが、Rust は不要です。Go 1.21+ が必要です。 + プリコンパイル済みの静的ライブラリが同梱されています(macOS arm64/amd64、Linux amd64 glibc/musl)——C コンパイラは必要ですが Rust は不要です。Go 1.21 以上が必要です。 ```bash 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 ``` - Maven Central で入手できます。jar には macOS(arm64、x64)、Linux(x64)、Windows(x64)用のネイティブライブラリが同梱されており、`jna.library.path` の設定は不要です。`com.x.chatxdk` からインポートします。JDK 17+ が必要です。 + Maven Central で提供されています。jar には macOS(arm64, x64)、Linux(x64)、Windows(x64)向けのネイティブライブラリが同梱されており、`jna.library.path` のセットアップは不要です。`com.x.chatxdk` からインポートします。JDK 17 以上が必要です。 @@ -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) に接続します。 +鍵をロードし、アイデンティティを一度セットし、バックログを復号し、ライブイベントを 1 つ復号し、メッセージを暗号化します。送信ボディは、[はじめに](/ja/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 を構築し、秘密鍵を保管し(パスコードで保護されるセキュアキーバックアップまたはローカル鍵ブロブ)、Chat API に**公開**鍵を登録し、アンロックまたはインポートの後に **`set_identity(user_id, signing_key_version)`** を呼び出します——これは、署名されるアクションが既定とする送信者と署名鍵バージョンを設定するので、encrypt および prepare メソッドが呼び出し単位のアイデンティティ引数なしで動作します。デバイス/アプリのアイデンティティごとに `generate_keypairs` を一度呼び出し、登録ペイロードを public-keys エンドポイントに POST してください。セキュアキーバックアップには全バインディングで `setup` / `unlock`(および関連するパスコードヘルパー)を使用します。`export_keys` / `import_keys`(ボットとサーバー向けの生の鍵ブロブ永続化)は**ネイティブバインディングでのみ**利用できます——Python、Go、.NET、JVM、Rust。JS/WASM バインディングは生の鍵のエクスポート/インポートを公開しません:ブラウザではインスタンスに到達する任意のスクリプトがアイデンティティを持ち出しうるため、JS は鍵をセキュアキーバックアップ内に保持します。リクエストごとのバックアップレルムのラウンドトリップを避けたい JS サーバーは、リクエスト間で 1 つのアンロック済み `Chat` インスタンスを再利用するか、鍵ブロブがサポートされるネイティブバインディングを実行してください。 + +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)` を呼び出します(推奨されません)。バックアップレルム構成が変わった場合は `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 回の呼び出しで完了します:新しい会話鍵を生成し、渡された公開鍵からすべての参加者に対して暗号化し、変更に署名します。送信者のアイデンティティと署名鍵バージョンはセッションから来ます(`set_identity`)。パラメーターに `sender_id` / `signing_key_version` を設定すればオーバーライドできます。すべて同じ **`PreparedConversationChange`** 形状を返し、POST 準備が整っています——SDK のフィールド `encrypted_key` を `conversation_participant_keys` の **`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 — 両方送信 | +| 1:1 を開始する(会話 ID を省略——SDK が導出)または任意の会話の鍵をローテーションする(ID を渡す) | `prepare_conversation_key_change` | 1 | +| グループを作成する(ID は `POST /2/chat/conversations/group/initialize` で発行) | `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` の各フィールドで——差し替えられたアイデンティティ鍵が会話鍵を受け取れないようにします。 -鍵変更イベントペイロードで `extract_conversation_keys` を使い、`{ keys, latest_version }` を再構築します。`decrypt_conversation_key` は単一の ECIES blob をアンラップします。 +鍵変更イベントペイロードに対して `extract_conversation_keys` を使って `{ keys, latest_version }` を再構築します。`decrypt_conversation_key` は 1 つの ECIES ブロブをアンラップします。 @@ -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 には両方含める必要があります。 +グループ作成とメンバー追加については、各メソッドが必要とするパラメーター(`prepare_group_create` はメンバー/管理者 ID リスト、`prepare_group_members_change` は新規 + 現在の名簿)を渡してください——サンプルは[グループ](/ja/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` は会話鍵引数が省略された場合にそこにフォールバックし、暗号化ヘルパーは省略された会話鍵をそこから解決します。無効化するとキャッシュはクリアされます。 + +明示的で空でない引数は常にストアより優先されます。明示的な呼び出し単位の引数はファーストクラスのままです——リクエストがストアが空の新しいインスタンスに着地しうるサーバーレスや複数インスタンス構成では、これが正しい選択です。 + +検証はデフォルトで必須です:署名鍵を省略しても検証はスキップされません。何も渡されず何も保存されていない場合、署名付きイベントは失敗します(`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,32 +555,40 @@ 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`。送信者アイデンティティはセッション(`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_*` フィールドは、生のイベントをもはや保持していない呼び出し元向けのオーバーライドとして残されています。 -返信とリアクション(`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 を導出します。同じパラメーターでリアクションの追加と後の削除ができます。生のイベントを保持していないときにのみ、`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`** は添付ファイルのバイト列を暗号化します。[メディア](/ja/xchat/media) を参照してください。低レベルの **`sign` / `verify` / `verify_key_binding`** は高度なフローをサポートします。会話鍵の変更、グループ作成、メンバー追加は [prepare メソッド](#conversation-keys) によって署名されます。 + +`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 を構築し、秘密鍵を保存し(パスコード保護された安全 ```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`)ではありません。完全なアップロード/ダウンロードのフロー:[メディア](/ja/xchat/media)。 @@ -659,12 +774,12 @@ SDK を構築し、秘密鍵を保存し(パスコード保護された安全 -### 大きなメディア向けのインクリメンタルストリーミング +### 大きなメディア向けの増分ストリーミング -大きなファイルの場合、ペイロード全体をメモリに保持しないでください: `stream_encryptor()` / `stream_decryptor()` は、`StreamEncryptor` / `StreamDecryptor` を返します。`push(chunk)` でチャンク(約 1 MB ずつ)を供給し、最後に `finish()` を 1 回呼び出します。復号では、`finish()` は切り詰められたストリームを検出します(最終フレームより前に入力が終わった場合は失敗します)ので、成功するまでプッシュされた平文を完全なものとして扱わないでください。 +大きなファイルでは、ペイロード全体をメモリに保持することを避けてください:`stream_encryptor()` / `stream_decryptor()` は `push(chunk)` でチャンク(それぞれ約 1 MB)を送り込み、最後に一度 `finish()` を呼び出す `StreamEncryptor` / `StreamDecryptor` を返します。復号時、`finish()` は切り捨てられたストリームを検出します(最終フレームより前に入力が終わっている場合は失敗します)。したがって、成功するまで push されたプレーンテキストを完全と扱わないでください。 -**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,39 +907,39 @@ Base64/hex ヘルパー、MIME スニッフィング、および画像寸法は ## 重要な型 -これらの概念的な型は、複数の言語にわたって現れます(正確なフィールド名は異なります;JS では `message` のように camelCase のイベント識別子を使うことが多いです): +これらの概念的な型は各言語で登場します(正確なフィールド名は異なります。JS では `message` のような camelCase のイベント識別子が使われることが多いです): -- **SendPayload** — `encrypt_message` および関連する暗号化ヘルパーの戻り値;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`。 +- **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` / public-key ゲッターの出力。 +- **SigningKeyEntry** — 署名検証のために復号に渡されるか、`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`)を使用してください。 +完全なフィールドリストは [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`** を優先してください、そうすれば 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 呼び出しでは修復できません。これらは一時的なエラーではなく tombstone として扱ってください。会話鍵をローテーションすれば、そこから先はクリーンで検証可能な履歴が始まります。 --- ## 次のステップ - - Chat XDK を Chat API に接続する + + Chat XDK を Chat API に配線 - + ストリーム暗号化とメディア REST - + Webhook とアクティビティ配信 - + よくある失敗 diff --git a/ko/xchat/cryptography-primer.mdx b/ko/xchat/cryptography-primer.mdx index e50a9fe48..75686b12b 100644 --- a/ko/xchat/cryptography-primer.mdx +++ b/ko/xchat/cryptography-primer.mdx @@ -1,31 +1,31 @@ --- -title: 암호화 기초 -sidebarTitle: 암호화 기초 -description: 구현 세부 사항 없이 X Chat의 종단 간 암호화를 뒷받침하는 ECDH, 공개 키 암호화, 디지털 서명 개념을 학습합니다. +title: 암호화 입문 +sidebarTitle: 암호화 입문 +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](/ko/xchat/xchat-xdk)가 암호화, 복호화, 서명, 키 저장을 대신 수행합니다—하지만 이 멘탈 모델은 앱을 설계하거나 동작을 디버깅할 때 도움이 됩니다. -구현할 준비가 되면 전체 안내를 위해 [시작하기](/xchat/getting-started)를, 개별 경로를 위해 사이드바의 [API 참조](/x-api/chat/get-chat-conversations)를 사용하세요. +구현할 준비가 되면 전체 안내를 위해 [시작하기](/ko/xchat/getting-started)를 사용하고, 개별 경로에 대해서는 사이드바의 [API 레퍼런스](/x-api/chat/get-chat-conversations)를 참고하세요. -**이 암호화를 직접 구현하지 않습니다.** Chat XDK가 처리합니다. 이 페이지는 이해를 위한 것이며 API 체크리스트가 아닙니다. +**이 암호화를 직접 구현하지 않습니다.** Chat XDK가 처리합니다. 이 페이지는 API 체크리스트가 아니라 이해를 위한 것입니다. --- -## 큰 그림 +## 전체 그림 -X Chat은 계층화된 암호화 시스템을 사용하며, 다음과 같이 동작합니다: +X Chat은 계층화된 암호화 시스템을 사용합니다: -1. **메시지**는 **대화 키**로 암호화됩니다 (빠른 대칭 암호화) -2. **대화 키**는 각 참가자의 **신원 공개 키**로 암호화됩니다 (비대칭 키 교환) -3. **메시지는** **서명 키**로 **서명**되어 수신자가 누가 보냈는지, 그리고 변조되지 않았는지 검증할 수 있습니다 +1. **메시지**는 **대화 키**로 암호화됩니다(빠른 대칭 암호화). +2. **대화 키**는 각 참여자의 **아이덴티티 공개 키**를 사용해 암호화됩니다(비대칭 키 교환). +3. **메시지는 서명 키**로 **서명**되어, 수신자가 발신자를 확인하고 내용이 변경되지 않았음을 검증할 수 있습니다. -대칭 암호화는 대량의 메시지 트래픽에 효율적이며, 비대칭 암호화는 주로 대화 키를 **안전하게 배포**하는 데 사용됩니다. +대칭 암호화는 많은 메시지 트래픽에 효율적이며, 비대칭 암호화는 주로 대화 키를 안전하게 **배포**하는 데 사용됩니다. ```mermaid flowchart TB @@ -45,72 +45,72 @@ flowchart TB end ``` -제품 흐름상 X는 읽을 수 있는 메시지 내용이나 원시 대화 키가 아닌 **암호문과 키 봉투**를 전송합니다. 앱은 암호화에 Chat XDK를, 키 등록 및 이 암호화된 페이로드의 송수신에는 [Chat API](/xchat/introduction)(Python/TypeScript의 XDK 또는 HTTPS 사용)를 사용합니다. 이러한 구성 요소가 어떻게 조합되는지는 [시작하기](/xchat/getting-started)를 참조하세요. +제품 흐름에서 X가 전달하는 것은 **암호문과 키 봉투**이며, 읽을 수 있는 메시지 내용이나 원시 대화 키가 아닙니다. 앱은 암호화를 위해 Chat XDK를 사용하고, 키 등록 및 암호화된 페이로드 송수신을 위해 [Chat API](/ko/xchat/introduction)(Python/TypeScript의 XDK 또는 HTTPS 통해)를 사용합니다. 이 구성 요소들이 어떻게 맞물리는지는 [시작하기](/ko/xchat/getting-started)를 참고하세요. --- ## 키 유형 설명 -X Chat은 각각 특정한 목적을 가진 세 가지 유형의 키 재료를 사용합니다. +X Chat은 세 가지 종류의 키 자료를 사용하며 각각 특정 목적이 있습니다. -### 1. 신원 키페어 +### 1. 아이덴티티 키쌍 -**용도:** 사용자 간에 대화 키를 안전하게 교환 +**목적:** 사용자 간에 대화 키를 안전하게 교환 | 구성 요소 | 설명 | |:----------|:------------| -| **신원 공개 키** | 다른 사람과 공유되며, 대화 키를 사용자에게 *암호화*하는 데 사용됩니다 | -| **신원 개인 키** | 비밀로 유지되며, 사용자에게 전송된 대화 키를 복호화하는 데 사용됩니다 | +| **아이덴티티 공개 키** | 타인과 공유; 대화 키를 *당신에게* 암호화하는 데 사용 | +| **아이덴티티 개인 키** | 비밀로 유지; *당신에게* 보내진 대화 키를 복호화하는 데 사용 | -누군가 사용자를 대화에 추가하면 사용자의 신원 공개 키를 사용하여 대화 키를 암호화합니다. 오직 사용자의 신원 개인 키만이 이를 복호화할 수 있습니다. +누군가 당신을 대화에 추가할 때, 그들은 당신의 아이덴티티 공개 키로 대화 키를 암호화합니다. 오직 당신의 아이덴티티 개인 키만 이를 복호화할 수 있습니다. -공개 키는 플랫폼의 **공개 키** API를 통해 등록 및 조회됩니다 (API 참조의 암호화 키 참조). 개인 키는 Chat XDK 내부에 유지됩니다 (예: [보안 키 백업](#보안-키-백업-분산-키-저장) 또는 신중하게 보호된 키 blob). +공개 절반은 플랫폼의 **공개 키(public-key)** API를 통해 등록 및 조회됩니다(API 레퍼런스의 Encryption keys 참고). 개인 절반은 Chat XDK 안에 남습니다(예: [보안 키 백업](#secure-key-backup-distributed-key-storage) 또는 신중히 보호된 키 blob을 통해). -### 2. 서명 키페어 +### 2. 서명 키쌍 -**용도:** 메시지를 사용자가 작성했음을 증명 +**목적:** 메시지를 당신이 작성했음을 증명 | 구성 요소 | 설명 | |:----------|:------------| -| **서명 공개 키** | 다른 사람과 공유되며, 사용자의 서명을 검증하는 데 사용됩니다 | -| **서명 개인 키** | 비밀로 유지되며, 사용자의 메시지에 서명하는 데 사용됩니다 | +| **서명 공개 키** | 타인과 공유; 당신의 서명을 검증하는 데 사용 | +| **서명 개인 키** | 비밀로 유지; 메시지에 서명하는 데 사용 | -메시지를 보내면 사용자의 서명 개인 키로 서명됩니다. 수신자는 사용자의 서명 공개 키(공개 키 API를 통해서도 게시됨)를 사용하여 검증합니다. Chat XDK는 메시지를 암호화하는 과정에서 서명하며, 발신자의 공개 키 자료를 제공하면 복호화 시 검증할 수 있습니다. +메시지를 보낼 때 서명 개인 키로 서명이 이루어집니다. 수신자는 당신의 서명 공개 키(공개 키 API를 통해 게시된)를 사용하여 검증합니다. Chat XDK는 메시지를 암호화하는 과정의 일부로 서명하고, 발신자의 공개 키 자료를 제공하면 복호화 시 검증도 수행합니다. ### 3. 대화 키 -**용도:** 특정 대화 내에서 메시지(및 미디어)를 암호화 및 복호화 +**목적:** 특정 대화 내에서 메시지(및 [미디어](/ko/xchat/media))를 암호화하고 복호화 | 속성 | 설명 | |:---------|:------------| -| **대칭** | 동일한 키로 암호화 및 복호화 | -| **대화별** | 각 대화마다 자체 키가 있음 | -| **참가자 간 공유** | 대화를 읽어야 하는 모든 참가자가 사본을 가짐 | -| **버전 관리됨** | 키는 교체될 수 있으며, 앱은 시간 경과에 따라 버전을 추적해야 함 | +| **대칭** | 동일한 키로 암호화와 복호화 수행 | +| **대화별** | 각 대화는 자체 키를 가짐 | +| **참여자 간 공유** | 대화를 읽어야 하는 모든 참여자가 사본을 보유 | +| **버전 관리** | 키는 순환될 수 있으며, 앱은 시간에 따른 버전을 추적해야 함 | -대화 키는 대화가 설정될 때 또는 키가 교체될 때 생성됩니다. 각 참가자는 자신의 신원 공개 키로 만들어진 **암호화된 사본**을 받습니다. 자신의 사본을 한 번 복호화하면 **원시** 대화 키를 보관해 두고 빠른 메시지 (및 [미디어](/xchat/media)) 암호화에 사용합니다. 대화의 이러한 사본 설정은 Chat XDK와 대화 **키** 엔드포인트를 함께 사용하여 수행되며, [시작하기](/xchat/getting-started#4-set-up-conversation-keys)에서 자세히 다룹니다. +대화 키는 대화가 설정되거나 키가 순환될 때 생성됩니다. 각 참여자는 자신의 아이덴티티 공개 키로 만들어진 **암호화된 사본**을 받습니다. 자신의 사본을 한 번 복호화한 후에는 **원시** 대화 키를 보관하고 이를 빠른 메시지(및 [미디어](/ko/xchat/media)) 암호화에 사용합니다. 대화를 위한 이러한 사본 설정은 Chat XDK와 대화 **키** 엔드포인트를 함께 사용하여 수행되며, 자세한 내용은 [시작하기](/ko/xchat/getting-started#4-set-up-conversation-keys)에 있습니다. --- -## 암호화가 개념적으로 작동하는 방식 +## 암호화 동작 방식(개념적) ### 메시지 보내기 - - "Hello, how are you?"라고 입력합니다. + + "안녕, 어떻게 지내?"라고 입력합니다. - 앱은 이 채팅의 원시 대화 키(설정 또는 이전 키 배포 이벤트로부터)를 올바른 키 버전으로 사용합니다. + 앱은 해당 채팅의 원시 대화 키(설정 시 또는 이전 키 배포 이벤트에서 얻은)를 올바른 키 버전으로 사용합니다. - Chat XDK가 대화 키로 메시지를 암호화합니다. 결과는 해당 키 없이는 쓸모없는 암호문입니다. + Chat XDK가 대화 키로 메시지를 암호화합니다. 결과는 그 키 없이는 쓸모없는 암호문입니다. - Chat XDK가 사용자의 서명 개인 키로 암호화된 페이로드에 서명하여, 사용자가 이 정확한 내용을 작성했음을 증명합니다. + Chat XDK가 서명 개인 키로 암호화된 페이로드에 서명하여, 당신이 정확히 이 내용을 작성했음을 증명합니다. - 앱은 암호화된 페이로드와 서명을 Chat API의 **메시지 전송** 엔드포인트를 통해 X로 보냅니다. X는 평문으로 읽을 수 없는 바이트를 저장하고 전달합니다. + 앱은 Chat API의 **send message** 엔드포인트를 통해 암호화된 페이로드와 서명을 X로 전송합니다. X는 평문으로는 읽을 수 없는 바이트를 저장하고 전달합니다. @@ -118,73 +118,73 @@ X Chat은 각각 특정한 목적을 가진 세 가지 유형의 키 재료를 - 앱은 [웹훅 또는 활동 스트림](/xchat/real-time-events)을 통해, 또는 이력을 위한 대화 **이벤트**를 읽어 X로부터 암호문을 수신합니다. + 앱은 [웹훅 또는 활동 스트림](/ko/xchat/real-time-events)을 통해, 혹은 히스토리를 위해 대화 **events**를 읽어 X로부터 암호문을 받습니다. - 캐시된 원시 키를 사용하거나, 이것이 신규이거나 교체된 경우 키 배포(키 변경) 이벤트에서 사본을 복호화하여 획득합니다. + 캐시된 원시 키를 사용하거나, 새롭거나 순환된 경우 키 배포(키 변경) 이벤트에서 자신의 사본을 복호화하여 얻습니다. - Chat XDK가 발신자의 서명 공개 키(및 관련 신원 바인딩)를 사용하여 서명을 확인하므로, 누가 보냈고 수정되지 않았음을 알 수 있습니다. + Chat XDK가 발신자의 서명 공개 키(및 관련 아이덴티티 바인딩)를 사용해 서명을 확인하므로, 누가 보냈고 변조되지 않았음을 알 수 있습니다. - Chat XDK가 대화 키로 복호화합니다. 이제 "Hello, how are you?"를 읽을 수 있습니다. + Chat XDK가 대화 키로 복호화합니다. 이제 "안녕, 어떻게 지내?"를 읽을 수 있습니다. -암호화, 전송, 수신, 복호화 구현은 [시작하기](/xchat/getting-started)와 [Chat XDK](/xchat/xchat-xdk) 참조에 있습니다. +암호화, 전송, 수신, 복호화의 구현은 [시작하기](/ko/xchat/getting-started)와 [Chat XDK](/ko/xchat/xchat-xdk) 레퍼런스에 있습니다. --- ## 키 배포 설명 -종단 간 암호화의 핵심 과제는 **키 배포**입니다. 즉, X(또는 관찰자)가 해당 키를 평문으로 볼 수 **없도록** 참가자가 어떻게 대화 키를 얻는가입니다. +종단 간 암호화의 핵심 과제는 **키 배포**입니다: 참여자들이 X(또는 관찰자)가 평문으로 그 키를 볼 수 **없이** 어떻게 대화 키를 얻는가입니다. ### 초기 키 설정 -메시지 전달을 위해 대화가 준비될 때: +메시징을 위해 대화가 준비될 때: -1. 임의의 대화 키가 (Chat XDK 내에서) 생성됩니다 -2. **각 참가자**에 대해 해당 키는 참가자의 **신원 공개 키**로 암호화됩니다 -3. 이 암호화된 사본은 X의 Chat API를 통해 저장 및 전송됩니다 -4. 각 참가자는 (Chat XDK 내에서) 자신의 신원 개인 키로 **자신의** 사본을 복호화합니다 +1. Chat XDK가 임의의 대화 키를 생성합니다 +2. Chat XDK가 그 키를 **각 참여자의 아이덴티티 공개 키**로 암호화합니다 +3. 앱이 그 암호화된 사본들을 X의 Chat API를 통해 게시합니다 +4. 각 참여자는 자신의 아이덴티티 개인 키로 **자신의** 사본을 (Chat XDK 안에서) 복호화합니다 -X는 원시 대화 키가 아닌 **래핑된** 사본만 처리합니다. +X는 오직 **감싼(wrapped)** 사본만 다루며, 원시 대화 키는 다루지 않습니다. ### 키 변경 이벤트 -대화 키가 교체되면(예: 멤버십이 변경될 때) 참가자는 각 멤버에 대한 새로운 암호화된 사본과 함께 **키 변경** 이벤트를 수신합니다. +대화 키가 순환될 때(예: 멤버십이 변경될 때) 참여자들은 각 멤버에 대한 새 암호화된 사본이 포함된 **키 변경** 이벤트를 받습니다. 앱은 다음을 수행해야 합니다: -1. 실시간 이벤트 또는 대화 이력에서 키 변경 자료를 감지 +1. 실시간 이벤트 또는 대화 히스토리에서 키 변경 자료를 감지 2. 새 대화 키(및 버전)를 복호화하고 저장 -3. 이후 전송에는 최신 버전을 사용 +3. 이후 전송에는 최신 버전 사용 -[시작하기](/xchat/getting-started#6-receive-and-decrypt)와 [실시간 이벤트](/xchat/real-time-events)는 이러한 이벤트가 실제로 어디에 나타나는지 설명합니다. +[시작하기](/ko/xchat/getting-started#6-receive-and-decrypt)와 [실시간 이벤트](/ko/xchat/real-time-events)에서 이러한 이벤트가 실제로 어디에 나타나는지를 설명합니다. --- -## 보안 키 백업: 분산 키 저장 +## 보안 키 백업: 분산 키 저장소 -**개인** 신원 및 서명 키는 신중하게 저장되어야 합니다. X Chat에는 **보안 키 백업** 시스템(Juicebox로 구현됨)이 포함되어 있어, 어떤 단일 서버에도 전체 비밀을 제공하지 않고도 여러 기기에서 패스코드로 키를 복구할 수 있습니다. +**개인** 아이덴티티 및 서명 키는 신중하게 저장되어야 합니다. X Chat에는 어떤 단일 서버에도 전체 비밀을 주지 않고, 기기 간 패스코드로 키를 복구할 수 있도록 하는 **보안 키 백업** 시스템이 포함되어 있습니다. -### 전통적인 키 저장의 문제 +### 전통적인 키 저장소의 문제점 -| 접근 방식 | 문제 | +| 접근 방식 | 문제점 | |:---------|:--------| -| 기기에만 저장 | 기기 분실 = 키 분실 = 메시지 이력 접근 상실 | -| 일반 클라우드 백업에 저장 | 공급자가 키 자료에 접근할 수 있음 | -| 긴 키를 기억 | 사람은 엔트로피가 높은 키를 안정적으로 기억할 수 없음 | +| 기기에만 저장 | 기기를 잃으면 키를 잃고 = 메시지 기록에 접근 불가 | +| 일반 클라우드 백업에 저장 | 제공자가 키 자료에 접근할 수 있음 | +| 긴 키를 기억 | 사람은 고엔트로피 키를 안정적으로 기억할 수 없음 | -### 보안 키 백업의 해결 방식 +### 보안 키 백업이 해결하는 방법 -보안 키 백업은 **비밀 공유**와 **패스코드 보호**를 결합합니다: +보안 키 백업은 **비밀 공유(secret sharing)**와 **패스코드 보호**를 결합합니다: -1. 개인 키는 **여러 조각으로 나뉩니다** -2. 조각은 **독립적인 realm**(별도의 서버)에 보관됩니다 -3. **어떤 단일 realm**도 혼자서 키를 재구성할 충분한 정보를 갖지 않습니다 -4. 복구에는 사용자의 **패스코드**와 **충분한 realm**의 협력이 필요합니다 -5. 잘못된 패스코드는 추측을 늦추기 위해 **속도 제한**됩니다 +1. 개인 키가 **여러 조각(share)로 분할**됩니다 +2. 조각들은 **독립된 realm**(별개 서버)이 보관합니다 +3. **어떤 단일 realm**도 단독으로 키를 재구성하기에 충분한 정보를 갖지 않습니다 +4. 복구는 **패스코드**와 **충분한 수의 realm** 협조가 필요합니다 +5. 잘못된 패스코드는 추측을 지연시키기 위해 **속도 제한**됩니다 ```mermaid flowchart LR @@ -198,10 +198,10 @@ flowchart LR end ``` -단일 주체가 전체 비밀을 보유하지 않고도 복구 가능성(새 기기 + 패스코드)을 얻을 수 있습니다. +단일 당사자가 전체 비밀을 보유하지 않으면서도 복구 가능성(새 기기 + 패스코드)을 얻을 수 있습니다. -일반적인 경로에서는 키 백업 서버를 직접 구성하지 않습니다. 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 호출입니다—시작하기의 [기존 키로 초기화](/ko/xchat/getting-started#2-initialize-the-chat-xdk-with-existing-keys) 및 [키 생성 및 등록](/ko/xchat/getting-started#3-create-and-register-keys-first-time-setup)을 참고하세요. 일부 앱(특히 서버와 봇)은 보안 키 백업 대신 내보낸 키 blob을 사용합니다. 그 자료는 비밀번호처럼 보호하세요. --- @@ -211,51 +211,53 @@ flowchart LR 모든 X Chat 메시지에는 다음을 지원하는 **디지털 서명**이 포함됩니다: 1. **진위성** — 발신자의 서명 개인 키로 생성되었음 -2. **무결성** — 서명 후에 암호화된 내용이 수정되지 않았음 +2. **무결성** — 서명 후 암호화된 내용이 수정되지 않았음 -### 서명이 개념적으로 작동하는 방식 +### 서명 동작 방식(개념적) | 동작 | 사용된 키 | 결과 | |:-------|:---------|:-------| -| **서명** | 발신자의 서명 개인 키 | 이 정확한 암호화된 메시지에 바인딩된 서명 | +| **서명** | 발신자의 서명 개인 키 | 정확히 이 암호화된 메시지에 결합된 서명 | | **검증** | 발신자의 서명 공개 키 | 서명이 메시지 및 키와 일치함을 확인 | -서명된 자료의 무엇이든 변경되면 검증이 실패합니다. 오직 서명 개인 키를 가진 사람만이 해당 키에 대한 유효한 서명을 생성할 수 있습니다. +서명 대상 자료 중 하나라도 변경되면 검증에 실패합니다. 오직 서명 개인 키를 가진 사람만이 해당 키에 대한 유효한 서명을 생성할 수 있습니다. ### 앱에서 -Chat XDK는 발신 메시지를 암호화할 때 서명하고, 수신 메시지를 복호화할 때 발신자의 공개 키 자료(공개 키 API에서 가져옴)로 검증합니다. 검증은 **기본적으로 필수**입니다: 명시적으로 검사를 비활성화(권장하지 않음)하지 않는 한 SDK는 검증되지 않은 서명 이벤트를 거부합니다. 세부 사항은 [Chat XDK](/xchat/xchat-xdk) 참조에 있습니다. +Chat XDK는 발신 메시지를 암호화할 때 서명하며, 수신 메시지를 복호화할 때 (공개 키 API에서 얻은) 발신자의 공개 키 자료에 대해 검증합니다. 검증은 기본적으로 **필수**입니다: SDK는 명시적으로 검사를 비활성화하지 않는 한(권장하지 않음) 검증되지 않은 서명 이벤트를 거부합니다. 자세한 내용은 [Chat XDK](/ko/xchat/xchat-xdk) 레퍼런스에 있습니다. -### 서명된 상태 변경 (action signatures) +서명은 인용된 내용에도 적용됩니다. 답장은 인용하는 원본 **서명된** 메시지를 원시 형태로 포함합니다. Chat XDK가 답장을 복호화할 때 그 포함된 원본을 검증하고 인용을 이에 대조하여 결과를 `reply_preview_validation`(`Valid` / `Invalid`)으로 보고합니다. `Invalid` 결과는 인용이 서명된 원본과 일치하지 않는다는 의미입니다—답장 자체는 별도로 검증되지만, 인용된 자료는 신뢰할 수 없는 것으로 취급하세요—따라서 어떤 참여자도 다른 사람에게 조작된 말을 귀속시킬 수 없습니다. -메시지만 서명되는 자료가 아닙니다. 대화 상태를 변경하는 모든 호출(대화 키 추가 또는 교체, 그룹 생성, 멤버 추가)에는 하나 이상의 **action signatures**가 포함되어야 합니다: 발신자는 변경 사항이 정확히 무엇을 하는지 설명하는 페이로드에 서명하고(키 변경의 경우 이 페이로드에는 새 대화 키 자체가 포함됨), API는 서명이 누락되거나 잘못된 형식이면 요청을 거부합니다. +### 서명된 상태 변경(액션 서명) -서버는 평문 대화 키를 보유하지 않으므로 키 변경의 서명을 암호학적으로 검사할 수 없습니다. 대신 서명된, 인코딩된 변경 설명이 받은 요청과 일치하는지 검증합니다. **암호학적** 검사는 가장자리에서 발생합니다: 각 수신자의 Chat XDK가 키 변경 이벤트를 복호화할 때 발신자의 서명 공개 키에 대해 서명을 검증합니다. Chat XDK의 `prepare` 메서드는 이러한 서명을 자동으로 생성합니다—그룹 생성 및 멤버 추가는 **두 개**(키 변경과 그룹 액션)를 반환하며, 둘 다 전송되어야 합니다. +메시지만이 서명 대상은 아닙니다. 대화 상태를 변경하는 모든 호출—대화 키 추가 또는 순환, 그룹 생성, 멤버 추가—은 하나 이상의 **액션 서명(action signature)**을 포함해야 합니다: 발신자는 변경 사항이 정확히 무엇을 하는지를 기술하는 페이로드에 서명하며(키 변경의 경우 그 페이로드에는 새로운 대화 키 자체가 포함됨), 서명이 없거나 잘못된 형식이면 API는 요청을 거부합니다. -서명은 이벤트 내용에 바인딩되며 불변입니다: 서명이 검증되지 않는 이벤트는 나중에 유효해질 수 없습니다. 처리 방법은 [문제 해결](/xchat/troubleshooting)을 참조하세요. +서버는 평문 대화 키를 결코 보관하지 않으므로 키 변경의 서명을 암호학적으로 검사할 수 없습니다. 서버는 서명된, 인코딩된 변경 설명이 수신한 요청과 일치하는지를 검증합니다. **암호학적** 검사는 경계에서 이루어집니다: 각 수신자의 Chat XDK가 키 변경 이벤트를 복호화할 때 발신자의 서명 공개 키에 대해 서명을 검증합니다. Chat XDK의 `prepare` 메서드가 이러한 서명을 생성해 줍니다—그룹 생성과 멤버 추가는 **두 개**를 반환하며(키 변경과 그룹 액션), 둘 모두를 전송해야 합니다. + +서명은 이벤트 내용에 결합되어 있으며 불변입니다: 서명이 검증되지 않는 이벤트는 결코 나중에 유효해질 수 없습니다. 이를 어떻게 다룰지는 [문제 해결](/ko/xchat/troubleshooting)을 참고하세요. --- ## 보안 속성 -### X Chat이 방어하는 것 +### X Chat이 방어하는 위협 | 위협 | 보호 | |:-------|:-----------| -| **X가 메시지 본문을 읽는 것** | 콘텐츠는 X로 전송되기 전에 암호화됩니다 | -| **네트워크 도청자** | 전송 보안과 종단 간 암호화된 콘텐츠 | -| **메시지 변조** | 서명이 수정을 감지합니다 | -| **사소한 발신자 사칭** | 유효한 서명에는 발신자의 서명 개인 키가 필요합니다 | -| **단일 서버 키 도난 (보안 키 백업 사용 시)** | 조각이 realm 전체에 분산되고 패스코드로 보호됩니다 | +| **X가 메시지 본문을 읽음** | 내용은 X로 전송되기 전에 암호화됨 | +| **네트워크 도청자** | 전송 계층 보안과 종단 간 암호화된 내용 | +| **메시지 변조** | 서명이 수정을 감지 | +| **간단한 발신자 위장** | 유효한 서명은 발신자의 서명 개인 키가 필요 | +| **단일 서버 키 도난(보안 키 백업 사용 시)** | 조각들이 realm 간에 분할되고 패스코드로 보호 | -### X Chat이 방어하지 **않는** 것 +### X Chat이 **방어하지 않는** 위협 | 위협 | 이유 | |:-------|:--------| -| **손상된 기기** | 잠금 해제된 클라이언트에서 평문과 키가 노출될 수 있습니다 | -| **메타데이터** | X는 누가 누구에게 언제 메시지를 보냈는지 알 수 있습니다—메시지 텍스트는 알지 못합니다 | -| **순방향 비밀성** | 신원 키의 손상은 그 키로 래핑된 대화 키를 노출할 수 있습니다 | -| **손상 후 보안** | 키를 교체해도 이력을 다시 쓰지는 않습니다 | +| **손상된 기기** | 잠금 해제된 클라이언트에서 평문과 키가 노출될 수 있음 | +| **메타데이터** | X는 누가 누구에게 언제 메시지를 보냈는지 알 수 있음—메시지 텍스트는 아님 | +| **전방향 비밀성(forward secrecy)** | 아이덴티티 키가 손상되면 그 키로 감싸진 대화 키가 노출될 수 있음 | +| **포스트-컴프로마이즈 보안** | 키 순환은 히스토리를 다시 쓰지 않음 | --- @@ -263,33 +265,33 @@ Chat XDK는 발신 메시지를 암호화할 때 서명하고, 수신 메시지 | 용어 | 정의 | |:-----|:-----------| -| **대칭 암호화** | 동일한 키로 암호화 및 복호화 (메시지 및 미디어 스트림에 사용) | -| **비대칭 암호화** | 암호화와 복호화를 위한 서로 다른 키 (대화 키 래핑에 사용) | -| **공개 키** | 공유해도 안전; 누군가에게 *암호화*하거나 그들의 서명을 검증하는 데 사용 | -| **개인 키** | 비밀로 유지되어야 함; 복호화 또는 서명에 사용 | -| **키페어** | 연결된 공개 키와 개인 키 | -| **ECDH / ECIES** | 대화 키를 신원 키로 래핑할 때 사용되는 알고리즘 | -| **ECDSA** | 메시지 작성자 확인에 사용되는 서명 알고리즘 | -| **P-256** | X Chat에서 사용되는 타원 곡선 (secp256r1) | -| **대화 키** | 하나의 대화 참가자가 공유하는 대칭 키 (시간 경과에 따라 버전 관리됨) | -| **비밀 공유** | 재구성하기 위해 여러 조각이 필요하도록 비밀을 분할하는 것 | -| **Realm** | 키 자료의 한 조각을 보관하는 독립적인 보안 키 백업 서버 | +| **대칭 암호화** | 동일 키로 암호화와 복호화(메시지와 미디어 스트림에 사용) | +| **비대칭 암호화** | 암호화와 복호화에 서로 다른 키(대화 키 교환에 사용) | +| **공개 키** | 공유해도 안전; 누군가에게 *암호화*하거나 그들의 서명을 검증할 때 사용 | +| **개인 키** | 비밀로 유지해야 함; 복호화 또는 서명에 사용 | +| **키쌍** | 연결된 공개 키와 개인 키 | +| **ECDH / ECIES** | 아이덴티티 키를 통해 대화 키를 교환할 때 사용되는 알고리즘 | +| **ECDSA** | 메시지 작성 증명에 사용되는 서명 알고리즘 | +| **P-256** | X Chat에서 사용되는 타원 곡선(secp256r1) | +| **대화 키** | 하나의 대화에서 참여자들이 공유하는 대칭 키(시간에 따라 버전 관리됨) | +| **비밀 공유** | 재구성을 위해 여러 조각이 필요하도록 비밀을 분할 | +| **Realm** | 키 자료의 한 조각을 보관하는 독립된 보안 키 백업 서버 | --- ## 다음 단계 - - 단계별로 키 구현, 전송, 수신 + + 키, 전송, 수신을 단계별로 구현하기 - - 암호화 SDK 메서드 및 타입 + + 암호화 SDK 메서드와 타입 - + 제품 개요 및 아키텍처 - + 암호화된 이벤트가 전달되는 방식 diff --git a/ko/xchat/getting-started.mdx b/ko/xchat/getting-started.mdx index 4d504f255..ab1e276d5 100644 --- a/ko/xchat/getting-started.mdx +++ b/ko/xchat/getting-started.mdx @@ -1,29 +1,29 @@ --- title: Chat API 시작하기 sidebarTitle: 시작하기 -description: Python, TypeScript, Go, Rust, C#, Java용 Chat XDK로 종단 간 암호화된 X Chat 메시징을 구축하는 단계별 튜토리얼입니다. +description: Python, TypeScript, Go, Rust, C#, Java에서 Chat XDK를 사용해 종단 간 암호화된 X Chat 메시징을 구축하는 단계별 튜토리얼입니다. keywords: ["X Chat tutorial", "X Chat quickstart", "Chat XDK", "encrypted DM", "Python", "TypeScript", "Go", "Rust", "C#", "Java"] --- -X에서 종단 간 암호화된 다이렉트 메시지를 주고받으세요: 키를 설정하고, 대화를 초기화하고, 메시지를 보내고, 수신 트래픽을 복호화합니다. +X에서 종단 간 암호화된 다이렉트 메시지를 주고받기: 키를 설정하고, 대화를 초기화하며, 메시지를 보내고, 수신 트래픽을 복호화합니다. -X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: +X Chat 앱은 두 가지 요소를 함께 사용합니다: | 구성 요소 | 역할 | |:----------|:-----| -| **[Chat XDK](/xchat/xchat-xdk)** | 암호화, 복호화, 서명 및 개인 키 저장 (보안 키 백업 또는 키 blob) | -| **X API** | 공개 키, 대화 키, 메시지 및 이벤트—[Python](/xdks/python/overview) 또는 [TypeScript](/xdks/typescript/overview) XDK를 통해서나, 사용자 액세스 토큰을 사용한 HTTPS로 제공 | +| **[Chat XDK](/ko/xchat/xchat-xdk)** | 암호화, 복호화, 서명, 개인 키 저장(보안 키 백업 또는 키 blob) | +| **X API** | 공개 키, 대화 키, 메시지, 이벤트—[Python](/xdks/python/overview) 또는 [TypeScript](/xdks/typescript/overview) XDK, 또는 사용자 액세스 토큰을 사용한 HTTPS를 통해 | -**사전 요구 사항** +**전제 조건** -- [개발자 계정](https://developer.x.com/en/portal/petition/essential/basic-info) 및 OAuth 2.0으로 구성된 앱 -- `dm.read`, `dm.write`, `tweet.read`, `users.read`가 포함된 사용자 액세스 토큰 +- [개발자 계정](https://developer.x.com/en/portal/petition/essential/basic-info)과 OAuth 2.0용으로 구성된 앱 +- `dm.read`, `dm.write`, `tweet.read`, `users.read` 권한이 있는 사용자 액세스 토큰 --- -## 1. 종속성 설치 +## 1. 의존성 설치 @@ -31,7 +31,7 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: pip install chatxdk xdk ``` - PyPI 패키지는 `chatxdk`이며, `chat_xdk`로 임포트합니다. Python 3.10+ 필요. + PyPI 패키지는 `chatxdk`이며 `chat_xdk`로 import합니다. Python 3.10+이 필요합니다. ```bash @@ -39,17 +39,16 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: npm install juicebox-sdk # optional peer dependency — required for setup()/unlock() secure key backup ``` - 컴파일된 WASM 엔진이 `@xdevplatform/chat-xdk`에 포함되어 있어 빌드 단계가 없습니다. Node.js 18+ 필요. + 컴파일된 WASM 엔진은 `@xdevplatform/chat-xdk` 내부에 포함되어 있어 별도의 빌드 단계가 없습니다. Node.js 18+가 필요합니다. ```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] @@ -61,25 +60,25 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: go get github.com/xdevplatform/chat-xdk/go/chatxdk ``` - 미리 컴파일된 정적 라이브러리가 포함되어 있습니다 (macOS arm64/amd64, Linux amd64 glibc/musl). C 컴파일러는 필요하지만 Rust는 필요 없습니다. Go 1.21+ 필요. + 사전 컴파일된 정적 라이브러리가 포함되어 있습니다(macOS arm64/amd64, Linux amd64 glibc/musl)—C 컴파일러는 필요하지만 Rust는 필요하지 않습니다. Go 1.21+이 필요합니다. ```bash 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 ``` - Maven Central에서 제공됩니다. jar에 macOS (arm64, x64), Linux (x64), Windows (x64)용 네이티브 라이브러리가 번들되어 있어 `jna.library.path` 설정이 필요 없습니다. `com.x.chatxdk`에서 임포트하세요. JDK 17+ 필요. + Maven Central에서 사용할 수 있습니다. jar에 macOS(arm64, x64), Linux(x64), Windows(x64)용 네이티브 라이브러리가 번들되어 있어 `jna.library.path` 설정이 필요 없습니다. `com.x.chatxdk`에서 import하세요. JDK 17+이 필요합니다. @@ -133,14 +132,14 @@ X Chat 앱은 두 가지 구성 요소를 함께 사용합니다: ## 2. 기존 키로 Chat XDK 초기화 -이 단계는 **이미 보유한 키를 로드**합니다—이 신원이 이전에 최초 설정을 완료한 경우 사용하세요: +이 단계는 **이미 가지고 있는 키를 로드**합니다—이 아이덴티티가 이전에 최초 설정을 완료한 경우 사용하세요: -- **보안 키 백업:** 공개 키 레코드의 `juicebox_config`로 SDK를 구성한 다음, 패스코드로 `unlock`하여 개인 키를 복구합니다 (예: 새 기기에서). -- **키 blob:** 이전에 `export_keys`로 내보낸 blob으로 `import_keys`를 호출합니다. +- **보안 키 백업:** 공개 키 레코드의 `juicebox_config`로 SDK를 구성한 다음 패스코드로 `unlock`하여 개인 키를 복구합니다(예: 새 기기에서). +- **키 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`으로 한 번 호출합니다. 이는 세션 아이덴티티를 저장합니다: 이후 모든 encrypt와 prepare 호출은 이 아이덴티티로 서명되므로, 호출마다 발신자 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 앱은 두 가지 구성 요소를 함께 사용합니다: 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,28 +237,34 @@ 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); } ``` -서버 및 봇 샘플에서는 종종 **키 blob**(`export_keys` / `import_keys`)을 사용합니다. 클라이언트 앱에서는 종종 **보안 키 백업**(패스코드로 `setup` / `unlock`)을 사용합니다. 두 경로 모두에 대해 [Chat XDK](/xchat/xchat-xdk) 참조를 확인하세요. +서버와 봇 샘플은 종종 **키 blob**(`export_keys` / `import_keys`)을 사용합니다. 클라이언트 앱은 종종 **보안 키 백업**(패스코드를 사용하는 `setup` / `unlock`)을 사용합니다. 두 경로 모두에 대해서는 [Chat XDK](/ko/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은 import에 실패합니다. --- -## 3. 키 생성 및 등록 (최초 설정) +## 3. 키 생성 및 등록(최초 설정) + +[2단계](#2-initialize-the-chat-xdk-with-existing-keys)에서 기존 키를 로드한 경우 이 단계를 건너뛰세요. 그렇지 않은 경우, 새 아이덴티티에 대한 일회성 설정은 **세 가지**를 수행합니다: + +1. **키쌍 생성** — `generate_keypairs`가 아이덴티티 및 서명 키쌍을 생성합니다. +2. **개인 키 저장** — 패스코드로 `setup`을 호출하면 보안 키 백업에 저장하고(클라이언트), `export_keys`는 안전하게 저장할 키 blob을 반환합니다(서버 및 봇). +3. **공개 키 등록** — add-public-key 엔드포인트에 등록 페이로드를 POST하여 다른 사람이 당신에게 암호화하고 당신의 서명을 검증할 수 있게 합니다. -[2단계](#2-기존-키로-chat-xdk-초기화)에서 기존 키를 로드했다면 이 단계를 건너뛰세요. 그렇지 않은 경우, 새 신원에 대한 일회성 설정은 **세 가지 작업**을 수행합니다: +등록의 키 버전으로 `set_identity`를 호출하여 마무리하면, 이 세션은 새 아이덴티티로 서명합니다. -1. **키페어 생성** — `generate_keypairs`가 신원 및 서명 키페어를 생성합니다. -2. **공개 키 등록** — 다른 사람이 사용자에게 암호화하고 서명을 검증할 수 있도록 등록 페이로드를 공개 키 추가 엔드포인트에 POST합니다. -3. **개인 키 저장** — 패스코드로 `setup`하면 보안 키 백업에 기록됩니다(클라이언트). 또는 `export_keys`가 안전하게 저장할 키 blob을 반환합니다(서버 및 봇). + +모든 바인딩(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,25 +384,25 @@ 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"); ``` -보안 키 백업에 강력한 패스코드를 사용하세요. 패스코드나 보호되지 않은 키 blob을 분실하면 이전 메시지를 복호화할 수 없게 될 수 있습니다. +보안 키 백업에는 강력한 패스코드를 사용하세요. 패스코드를 잃거나 보호되지 않은 키 blob을 잃으면 과거 메시지를 복호화하지 못할 수 있습니다. --- ## 4. 대화 키 설정 -사용자 ID, 서명 키 버전, 그리고 모든 참가자의 신원 공개 키와 함께 **`prepare_conversation_key_change`**를 호출합니다. 한 번의 호출로 새로운 대화 키가 생성되고, 각 참가자에 대해 암호화되며, 변경 사항이 서명됩니다. 결과를 **대화 키 추가** 엔드포인트(`POST /2/chat/conversations/{id}/keys`)에 POST합니다—본문에는 `conversation_key_version`, `conversation_participant_keys`(SDK `encrypted_key` → API `encrypted_conversation_key`), 그리고 **`action_signatures`**(필수; 없으면 API가 호출을 거부함)가 필요합니다. 전송에 사용할 **원시** 대화 키를 보관하세요. +**`prepare_conversation_key_change`**를 모든 참여자의 아이덴티티 공개 키와 함께 호출합니다. 발신자 아이덴티티는 2단계에서 설정한 세션에서 옵니다. 한 번의 호출로 새 대화 키가 생성되고, 각 참여자에 대해 암호화되며, 변경에 서명이 이루어집니다. 결과를 **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합니다. 대화 키가 노출되었다고 의심될 때 교체하세요—교체는 **향후** 메시지만 보호합니다. 이전 키 버전으로 암호화된 메시지는 그 버전을 보유한 사람이라면 누구나 계속 읽을 수 있습니다. +응답은 정규 대화 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,22 +822,26 @@ API가 요구할 때 URL 경로에 **하이픈으로 연결된** 대화 ID를 + +스니펫은 이 흐름에서 방금 4단계에서 대화 키를 생성했기 때문에 명시적으로 전달합니다. 키 캐시가 켜져 있고 `decrypt_events` 실행이 대화의 키를 검증한 후에는([6단계](#6-receive-and-decrypt)), `encrypt_message(conversation_id, text)`만으로 충분합니다—SDK가 최신 검증된 키를 채웁니다. 재시도는 **같은** 암호화된 페이로드를 다시 보내야 하므로 ID가 두 번 생성되지 않습니다. + + --- ## 6. 수신 및 복호화 -실시간 트래픽에는 [웹훅 또는 활동 스트림](/xchat/real-time-events)을, 이력 조회에는 대화 **이벤트**의 페이지 처리를 사용하세요. +실시간 트래픽에는 [웹훅 또는 활동 스트림](/ko/xchat/real-time-events)을 사용하고, 히스토리에는 대화 **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는 카멜케이스 이벤트 타입(`message`)을 사용하고, 다른 언어는 JSON에서 `"Message"`와 스네이크케이스 필드를 사용합니다. ```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를 -모든 언어에 대한 전체 폴링-응답 봇 예제: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). + +**서버리스나 멀티 인스턴스인가요?** 서명 키 저장소와 키 캐시는 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). --- ## 모범 사례 -- 원시 대화 키와 발신자 공개 키를 캐시하고, 서명 검증 실패 시 갱신하세요 -- `event_uuid`로 실시간 전달을 중복 제거하세요 -- 페이지네이션이 완료될 때까지 이벤트 이력을 페이지 처리하여 키 변경 메타데이터를 놓치지 마세요 -- 프로덕션에서 패스코드, 개인 키, 메시지 평문을 로그에 남기지 마세요 -- 웹 앱에서는 OAuth 토큰(및 키 백업 realm 토큰 발급)을 서버에 유지하세요; 개인 키는 클라이언트 Chat XDK에만 보관하는 것이 좋습니다 - ---- - -## 다음 단계 - - - - 모든 언어 바인딩의 메서드 및 타입 - - - 암호화된 이미지 및 파일 첨부 - - - 다자간 대화 및 메타데이터 - - - 웹훅 및 활동 전달 - - +- 서명 키 저장소를 최신 상태로 유지하세요: 발신자가 새 키 버전을 등록할 때 전체 참여자 세트로 `set_signing_keys`를 다시 호출하고, 서명 검증 실패 시 새로 고치세요 +- 실시간 전달은 `event_uuid`로 중복 제거하세요 diff --git a/ko/xchat/groups.mdx b/ko/xchat/groups.mdx index d47f18c6a..8be56ec55 100644 --- a/ko/xchat/groups.mdx +++ b/ko/xchat/groups.mdx @@ -1,45 +1,46 @@ --- 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 참조 → 대화 및 메시지** 아래에 있습니다. +1:1 흐름은 [시작하기](/ko/xchat/getting-started)에 있습니다. 엔드포인트 세부 정보는 **API 레퍼런스 → Conversations and messages** 아래에 있습니다. --- -## 그룹과 1:1의 차이점 +## 그룹이 1:1과 어떻게 다른가 | 주제 | 1:1 | 그룹 | |:------|:----|:------| -| 식별 | 경로에서 종종 상대방 사용자 ID로 지정 | 대화 ID가 일반적으로 `g`로 시작 | -| 생성 | 사용자에 대한 키 + 메시징 | 그룹 생성 / 초기화 API, 그런 다음 키 | -| 참가자 | 나 + 상대방 한 명 | 여러 사용자; 멤버십이 변경 가능 | -| 메타데이터 | 최소 | 이름, 아바타 등은 **암호문**일 수 있음 (대화 키로 복호화) | -| 키 교체 | 덜 빈번 | 사람이 참여하거나 떠날 때 흔함 | +| 아이덴티티 | 종종 경로에서 상대방 사용자 ID로 지정 | 대화 ID가 일반적으로 `g`로 시작 | +| 생성 | 사용자에게 키 + 메시징 | Create / initialize group API 후 키 | +| 참여자 | 당신 + 상대방 한 명 | 여러 사용자; 멤버십이 변경될 수 있음 | +| 메타데이터 | 최소 | 이름, 아바타 등이 **암호문**일 수 있음(대화 키로 복호화) | +| 키 순환 | 덜 빈번 | 사람이 참여하거나 나갈 때 일반적 | -암호화는 여전히: 키와 페이로드에는 **Chat XDK**를, 그룹 생성, 참가자 키 래핑 게시, 메시지 전송, 이벤트 로드에는 **X API**를 사용합니다. +암호화는 여전히 다음과 같습니다: 키와 페이로드를 위한 **Chat XDK**; 그룹 생성, 참여자 키 감싸기 게시, 메시지 전송, 이벤트 로드를 위한 **X API**. --- ## 그룹 생성 및 키 설정 -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(대화 키 변경과 그룹 생성)를 반환합니다. -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."`. +1. `POST /2/chat/conversations/group/initialize`로 그룹 ID를 만드세요—응답의 `data.conversation_id`는 아래 모든 곳에서 사용하는 g가 접두된 ID입니다. +2. 각 멤버의 아이덴티티 공개 키와 `public_key_version`을 로드하세요(**Encryption keys** 아래의 `GET` 공개 키 경로; [`GET /2/users/public_keys`](/x-api/users/get-public-keys-for-multiple-users)는 한 요청으로 여러 사용자를 가져옵니다). 사용하기 전에 `verify_key_binding`으로 각 레코드를 검증하세요([시작하기](/ko/xchat/getting-started#4-set-up-conversation-keys)의 경고 참고). +3. **모든** 멤버(자신 포함), g가 접두된 ID, 멤버/관리자 ID 목록과 함께 **`prepare_group_create`**을 한 번 실행하세요. 한 번의 호출로 대화 키를 생성하고, 모든 멤버에게 감싸며, `set_identity`의 세션 아이덴티티로 create에 서명합니다—**두 개**의 액션 서명(대화 키 변경과 그룹 create)을 반환합니다. +4. 그룹 members/admins, `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 @@ -107,19 +106,19 @@ 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와 동일합니다. +참여자 키와 액션 서명(`message_id`, `encoded_message_event_detail`, 중첩된 `message_event_signature`)의 본문 매핑은 [시작하기 — 대화 키](/ko/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와 현재 명단(members, admins, pending members, 현재 title/avatar/TTL이 설정되어 있다면 포함)으로 **`prepare_group_members_change`**를 호출하세요. 이는 대화 키를 순환하고, 그룹 create와 마찬가지로 **두 개**의 액션 서명을 반환합니다—모두를 **add members**(`POST /2/chat/conversations/{id}/members`)에 POST하세요. 그런 다음 **키 변경** 트래픽을 예상하세요: [시작하기의 키 순환](/ko/xchat/getting-started#6-receive-and-decrypt)처럼 취급하세요(`extract_conversation_keys` / `decrypt_events`, 그 후 최신 버전으로 암호화). -`prepare_group_members_change`는 전달한 로스터에게만 래핑된 **새로운** 대화 키를 생성하므로, 새 멤버는 새 키 버전을 받고 이전 버전으로 전송된 메시지를 복호화할 수 없습니다. 반대로는 성립하지 않습니다: 교체는 **이전** 버전에 대한 접근을 결코 취소하지 않습니다 — 이미 이전 키를 보유한 사람은 누구나 그 키로 암호화된 메시지를 계속 읽을 수 있습니다. 대화 키가 노출되었다고 의심되면 `prepare_conversation_key_change`로 교체하세요. 이는 향후 메시지만 보호합니다. +`prepare_group_members_change`는 전달한 명단에만 감싸진 **새로운** 대화 키를 생성하므로, 새 멤버는 새 키 버전을 받으며 이전 버전으로 전송된 메시지는 복호화할 수 없습니다. 반대는 성립하지 않습니다: 순환은 **이전** 버전에 대한 접근을 결코 취소하지 않습니다—오래된 키를 이미 가진 사람은 그 키로 암호화된 메시지를 여전히 읽을 수 있습니다. 대화 키가 노출된 것으로 의심되면 `prepare_conversation_key_change`로 순환하세요. 이는 미래의 메시지만 보호합니다. --- ## 암호화된 그룹 메타데이터 -일부 대화 필드(예: 표시 **이름** 또는 **아바타 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`는 제공한 대로 정확히 title을 서명하고 전송합니다(대화 키는 그 호출이 생성할 때까지 존재하지 않으므로, 생성 시 title은 그 키로 암호화될 수 없습니다). 필드가 암호문인 대화를 읽을 때는, 필드가 작성될 당시 활성화되어 있던 키 버전으로 `decrypt`를 사용해 복호화하세요. @@ -166,27 +165,27 @@ keywords: ["X Chat groups", "group DM", "conversation keys", "group name encrypt -해당 메타데이터에 적용되는 **현재** 대화 키 버전을 사용하세요. 키가 교체되었다면, 필드가 쓰여진 시점에 활성화되어 있던 버전으로 복호화하세요(또는 메타데이터가 항상 교체 시 재작성되는 경우 제품 규칙을 따르세요). +해당 메타데이터에 적용되는 **현재** 대화 키 버전을 사용하세요. 키가 순환된 경우 필드가 작성된 당시 활성화되어 있던 버전으로 복호화하세요(또는 메타데이터가 항상 순환 시 다시 쓰인다면 제품 규칙을 따르세요). --- -## 메시지 및 이벤트 +## 메시지와 이벤트 -원시 대화 키를 가지고 있다면, 그룹에서 송수신은 1:1과 동일합니다: +원시 대화 키를 얻은 후 그룹에서의 전송과 수신은 1:1과 동일합니다: -- **전송:** `encrypt_message` → 메시지 전송 API ([시작하기](/xchat/getting-started#5-send-a-message)) -- **수신:** 이벤트 API 또는 [실시간 전달](/xchat/real-time-events) → `decrypt_event` / `decrypt_events` -- **미디어:** 그룹 대화 ID와 함께 [미디어](/xchat/media) +- **전송:** `encrypt_message` → send-message API([시작하기](/ko/xchat/getting-started#5-send-a-message)) +- **수신:** events API 또는 [실시간 전달](/ko/xchat/real-time-events) → `decrypt_event` / `decrypt_events` +- **미디어:** 그룹 대화 ID와 함께 [미디어](/ko/xchat/media) -멤버십 기반 교체 이후에는 항상 **최신** 키 버전으로 암호화하세요. +멤버십에 의한 순환 이후에는 항상 **최신** 키 버전으로 암호화하세요. --- ## 체크리스트 -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`; 참여자 키 감싸기와 **두 개 모두**의 액션 서명을 `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..708317a68 100644 --- a/ko/xchat/media.mdx +++ b/ko/xchat/media.mdx @@ -1,15 +1,15 @@ --- -title: 미디어 및 첨부 파일 +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 레퍼런스 → Media**)로 업로드한 다음, `encrypt_message`에 **`media_hash_key`**를 첨부하세요. -업로드 시 DM 스코프와 함께 **`media.write`**를 포함하세요. 경로에는 하이픈으로 연결된 대화 ID를 사용하세요(`:` → `-`). MIME/치수는 **복호화된** 바이트에서 가져오는 것이 좋습니다. +업로드 시 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`가 오류를 발생시킵니다. --- @@ -112,33 +112,29 @@ flowchart LR | 단계 | 메서드 | 경로 | |:-----|:-------|:-----| -| 초기화 | `POST` | `/2/chat/media/upload/initialize` | -| 추가 | `POST` | `/2/chat/media/upload/{id}/append` | -| 완료 | `POST` | `/2/chat/media/upload/{id}/finalize` | +| Initialize | `POST` | `/2/chat/media/upload/initialize` | +| Append | `POST` | `/2/chat/media/upload/{id}/append` | +| Finalize | `POST` | `/2/chat/media/upload/{id}/finalize` | -**API 참조 → 미디어** 아래의 OpenAPI 페이지에 있는 요청 본문을 사용하세요. 크기가 필요한 곳에는 **암호화된** blob 크기를 사용하는 것이 좋습니다. 완료는 첨부 및 다운로드용 **`media_hash_key`**를 반환합니다. 일시적인 `5xx`는 백오프로 재시도하세요. Python/TypeScript는 미디어 헬퍼가 존재할 때 XDK를 사용할 수 있습니다; 그렇지 않으면 어떤 언어에서든 Bearer 토큰과 함께 POST하세요. +**API 레퍼런스 → Media** 아래의 OpenAPI 페이지에서 요청 본문을 사용하세요. 크기가 필요한 경우 **암호화된** blob 크기를 선호하세요. Finalize는 첨부 및 다운로드에 사용할 **`media_hash_key`**를 반환합니다. 일시적인 `5xx`는 백오프로 재시도하세요. Python/TypeScript는 미디어 헬퍼가 존재할 때 XDK를 사용할 수 있고, 그렇지 않으면 어떤 언어에서든 Bearer 토큰으로 POST하세요. --- ## 첨부 파일과 함께 전송 -미디어 첨부와 함께 암호화한 다음, 메시지 전송 본문을 POST합니다([시작하기](/xchat/getting-started#5-send-a-message)와 동일한 필드 매핑). +미디어 첨부와 함께 암호화한 다음 send-message 본문을 POST하세요([시작하기](/ko/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`가 대화의 최신 검증된 키 변경에서 키와 버전을 해석합니다([시작하기](/ko/xchat/getting-started) 참고). + --- ## 다운로드 및 복호화 -경로: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/download-chat-media). 응답 본문은 암호문입니다. 수신 메시지에서는 복호화된 첨부 파일 / `media_hashes`에서 `media_hash_key`를 읽습니다. +경로: [`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]`—최신 버전이 아닙니다. 키 순환(예: 멤버 추가) 후에는 최신 키가 이전 메시지에 첨부된 미디어를 복호화할 수 없습니다. @@ -358,9 +368,9 @@ flowchart LR ## 팁 -- 미디어가 암호화된 시점과 동일한 **대화 키 버전**을 사용하세요 -- 평문 미디어나 원시 키를 로그에 남기지 마세요 +- 미디어가 암호화될 때와 **동일한 대화 키 버전**을 사용하세요 +- 평문 미디어나 원시 키를 로깅하지 마세요 - MIME은 복호화 **후**에 감지하세요 -- 웹 클라이언트: 가능하면 클라이언트에서 암호화/복호화; OAuth 토큰은 서버에 유지하세요 +- 웹 클라이언트: 가능한 경우 클라이언트에서 암호화/복호화하고 OAuth 토큰은 서버에 유지하세요 -각 미디어 경로의 전체 요청 및 응답 스키마는 사이드바의 **API 참조 → 미디어**에 있습니다 (업로드 초기화, 청크 추가, 업로드 완료, 미디어 다운로드). +각 미디어 경로에 대한 전체 요청 및 응답 스키마는 사이드바의 **API 레퍼런스 → Media**(initialize upload, append chunk, finalize upload, download media)에 있습니다. diff --git a/ko/xchat/real-time-events.mdx b/ko/xchat/real-time-events.mdx index 693b01a5c..ec8390eff 100644 --- a/ko/xchat/real-time-events.mdx +++ b/ko/xchat/real-time-events.mdx @@ -1,45 +1,45 @@ --- title: 실시간 X Chat 이벤트 sidebarTitle: 실시간 이벤트 -description: 웹훅 또는 활동 스트림으로 chat.received, chat.sent 등 암호화된 X Chat 활동을 수신하고 Chat XDK로 페이로드를 복호화합니다. +description: 웹훅이나 활동 스트림을 통해 chat.received, chat.sent 및 기타 암호화된 X Chat 활동을 수신한 다음 Chat XDK로 페이로드를 복호화합니다. --- -X는 페이로드에 **암호문**을 포함하여 **`chat.received`**, **`chat.sent`** 및 관련 X Chat 활동을 전달합니다. [Chat XDK](/xchat/xchat-xdk)로 복호화하세요. +X는 **`chat.received`**, **`chat.sent`** 및 관련 X Chat 활동을 페이로드의 **암호문**과 함께 전달합니다. [Chat XDK](/ko/xchat/xchat-xdk)로 복호화하세요. | 계층 | 역할 | |:------|:-----| -| **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` | +| **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** | `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`가 아닙니다. --- -## 이벤트 유형 +## 이벤트 타입 -| 이벤트 | 시점 | +| 이벤트 | 발생 시점 | |:------|:-----| -| `chat.received` | 구독된 사용자가 암호화된 DM을 수신할 때 | -| `chat.sent` | 구독된 사용자가 암호화된 DM을 보낼 때 | -| `chat.conversation_join` | 구독된 사용자가 그룹에 참여할 때 (제공되는 경우) | +| `chat.received` | 구독한 사용자가 암호화된 DM을 수신할 때 | +| `chat.sent` | 구독한 사용자가 암호화된 DM을 전송할 때 | +| `chat.conversation_join` | 구독한 사용자가 그룹에 참여할 때(제안된 경우) | --- ## 1. 전달 방식 선택 -**활동 스트림 (봇에 종종 가장 간단함):** 앱 Bearer 토큰과 함께 `GET /2/activity/stream`을 사용합니다 (OpenAPI에 따라 선택적 `backfill_minutes`, `start_time`, `end_time`). 클라이언트 측에서 `chat.received` / `chat.sent`를 필터링하세요. +**활동 스트림(봇에 가장 간단한 경우가 많음):** 앱 Bearer 토큰과 함께 `GET /2/activity/stream`(OpenAPI에 따른 선택적 `backfill_minutes`, `start_time`, `end_time`). 클라이언트 측에서 `chat.received` / `chat.sent`로 필터링하세요. -**활동 구독:** 다음으로 지속적인 구독을 관리합니다: +**활동 구독:** 다음으로 지속적인 구독을 관리하세요: -- `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) 구독을 생성하려면 활동을 모니터링할 사용자에 대한 **사용자 컨텍스트 권한 부여**(관련 스코프—예: 채팅 이벤트에 대한 `dm.read`—를 갖춘 OAuth 2.0 사용자 컨텍스트)가 필요합니다. -**웹훅:** 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 버전에 포함된 경우 웹훅과 활동에 대한 헬퍼를 노출할 수 있습니다. @@ -74,130 +74,88 @@ X는 페이로드에 **암호문**을 포함하여 **`chat.received`**, **`chat. -발신 사본이 필요한 경우 `chat.sent`도 구독하세요. 다른 언어: 동일한 `/2/activity/*` HTTPS 경로를 직접 호출하세요 (구독 생성에는 사용자 컨텍스트 토큰, 스트림에는 앱 Bearer 토큰). +발신 사본이 필요하다면 `chat.sent`도 구독하세요. 다른 언어: 동일한 `/2/activity/*` HTTPS 경로를 직접 호출하세요(구독 생성에는 사용자 컨텍스트 토큰, 스트림에는 앱 Bearer 토큰). --- -## 2. CRC (웹훅 전용) +## 2. CRC(웹훅 전용) -웹훅을 사용하는 경우, 소비자 시크릿을 사용한 토큰의 HMAC-SHA256으로 챌린지-응답 검사(GET `crc_token`)에 응답하세요. 이는 웹훅 제품이 기대하는 JSON 형태(일반적으로 `sha256=`)여야 합니다. +웹훅을 사용하는 경우, 웹훅 제품이 기대하는 JSON 형식(일반적으로 `sha256=`)으로 컨슈머 시크릿을 사용해 토큰의 HMAC-SHA256으로 Challenge-Response Check(GET `crc_token`)에 응답하세요. --- ## 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`**로 중복 제거하세요—이는 서명된 내용의 일부이며, sequence ID는 백엔드가 할당한 서명되지 않은 메타데이터입니다. -JavaScript는 camelCase 이벤트 유형(`message`)을 사용합니다; 다른 바인딩은 `"Message"`와 snake_case 필드를 사용합니다. +아래 스니펫은 가장 짧은 핸들러를 위해 두 개의 **선택적** 세션 저장소를 사용합니다: `set_signing_keys`는 참여자의 공개 키([공개 키 엔드포인트](/x-api/chat/get-user-public-keys)에서 한 번 가져옴)를 보관하고, `set_cache_keys(true)`는 각 대화의 검증된 키를 보관하므로 `decrypt_event`가 이벤트만으로 충분합니다. 페이로드에 `conversation_key_change_event`가 포함되면 먼저 `decrypt_events`를 통해 실행하세요: 이는 키 변경을 검증하고, 캐싱이 켜져 있으면 `decrypt_event` 호출을 위해 그 키를 보존합니다. 인스턴스 상태를 원하지 않으신가요? 대신 호출별로 키를 전달하세요—이 섹션 끝의 노트를 참고하세요. + +JavaScript는 카멜케이스 이벤트 타입(`message`)을 사용하고, 다른 바인딩은 `"Message"`와 스네이크케이스 필드를 사용합니다. ```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,11 +197,13 @@ JavaScript는 camelCase 이벤트 유형(`message`)을 사용합니다; 다른 -이력: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — [시작하기](/xchat/getting-started#6-receive-and-decrypt)를 참조하세요. +대신 키 맵을 직접 관리하려면, `extract_conversation_keys`가 `conversation_key_change_event`에서 키를 복호화하고 `decrypt_event`는 이를(그리고 발신자의 서명 키를) 명시적 인수로 받습니다—명시적으로 비어 있지 않은 인수는 항상 저장소보다 우선합니다. + +히스토리: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — [시작하기](/ko/xchat/getting-started#6-receive-and-decrypt) 참고. --- -## 페이로드 형태 (실시간) +## 페이로드 형태(실시간) ```json { @@ -256,7 +225,7 @@ JavaScript는 camelCase 이벤트 유형(`message`)을 사용합니다; 다른 ## 관행 -- 플랫폼 요구 사항에 따라 웹훅 서명을 검증하세요 -- 대화 키와 발신자 공개 키를 캐시하세요 -- 의존 메시지를 복호화하기 전에 키 변경 blob을 적용하세요 -- `event_uuid`로 중복을 제거하세요 +- 플랫폼 요구사항에 따라 웹훅 서명을 검증하세요 +- 세션 저장소를 한 번 설정하세요: 모든 참여자에 대한 `set_signing_keys`, 대화 키에 대한 `set_cache_keys(true)` +- 종속된 메시지를 복호화하기 전에 키 변경 blob을(`decrypt_events`를 통해) 적용하세요 +- 전달은 `event_uuid`로, 메시지는 서명된 `message_id`로 중복 제거하세요 diff --git a/ko/xchat/troubleshooting.mdx b/ko/xchat/troubleshooting.mdx index 49d7e280b..7794ac74b 100644 --- a/ko/xchat/troubleshooting.mdx +++ b/ko/xchat/troubleshooting.mdx @@ -1,22 +1,22 @@ --- 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) 문서를 사용하세요. +웹훅, OAuth, HTTP 상태 코드, 속도 제한에 대해서는 일반 [X API](/ko/x-api/introduction)와 [인증](/ko/fundamentals/authentication/overview) 문서를 사용하세요. --- -## 키 및 보안 키 백업 +## 키와 보안 키 백업 -### 잠금 해제 실패 (잘못된 패스코드) +### 잠금 해제 실패(잘못된 패스코드) -- 패스코드가 `setup`에서 사용한 것과 일치하는지 확인하세요 -- 시도 사이에 기다리세요; realm은 잘못된 추측에 대해 속도 제한을 두고 너무 많은 실패 후에는 복구를 잠글 수 있습니다 +- 패스코드가 `setup`에 사용된 것과 일치하는지 확인하세요 +- 시도 간에 대기하세요; realm은 잘못된 추측을 속도 제한하며 실패가 너무 많으면 복구를 잠글 수 있습니다 @@ -62,60 +62,60 @@ 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); ``` ### 메시지에 대한 대화 키 누락 -해당 메시지의 `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을 포함하세요 -2. 해당 버전에 대해 대화 키가 추가되었고 여전히 참가자인지 확인하세요 ([시작하기](/xchat/getting-started#4-set-up-conversation-keys) 참조) +1. `conversation_key_change_event`(실시간 이벤트) 또는 `meta.conversation_key_events`(히스토리)에서 `extract_conversation_keys`로 키 자료를 복호화하거나, `decrypt_events`에 해당 blob들을 포함시키세요—`set_cache_keys(true)`이 활성화되면 `decrypt_events`가 각 대화의 최신 검증된 키도 보존하므로 이후 `decrypt_event`와 `encrypt_*` 호출이 이를 생략할 수 있습니다 +2. 해당 버전에 대한 대화 키가 추가되었고 여전히 참여자인지 확인하세요([시작하기](/ko/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`을 로드하세요. --- @@ -123,19 +123,20 @@ keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encr ### 복호화 실패 -- 오래되었거나 잘못된 **원시** 대화 키, 또는 잘못된 키 버전 +- 오래되거나 잘못된 **원시** 대화 키, 또는 잘못된 키 버전 - 불완전한 `encoded_event` 문자열 -- 이벤트 유형이 복호화 가능한 콘텐츠로 처리할 수 있는 암호화된 메시지가 아님 +- 이벤트 타입이 복호화 가능한 콘텐츠로 취급할 수 있는 암호화된 메시지가 아님 -### 서명 검증 실패 +### 서명이 검증되지 않음 -검증은 **기본적으로 실패-폐쇄**입니다(`reject_unverified = true`): SDK는 이미 검증되지 않은 서명 이벤트를 거부하므로, 여기서 실패가 발생한다면 검사를 켜야 한다는 뜻이 아니라 검증 입력이 잘못되었다는 뜻입니다. 일반적인 원인: +검증은 기본적으로 **실패 시 거부(fail-closed)** 상태입니다(`reject_unverified = true`): SDK가 이미 검증되지 않은 서명 이벤트를 거부하므로, 여기서의 실패는 검사를 켜야 한다는 뜻이 아니라 검증 입력이 잘못되었다는 뜻입니다. 일반적인 원인: -- **발신자**에 대한 서명 키 항목이 누락되었거나 불완전함 (Chat XDK가 요구하는 모든 필드—[Chat XDK](/xchat/xchat-xdk) 참조 참조) -- 발신자가 버전을 교체함—공개 키를 다시 가져오세요 -- 허용된 최소값보다 낮은 키 버전은 절대 검증되지 않습니다 +- **발신자**에 대한 서명 키 항목이 누락되거나 불완전(Chat XDK가 요구하는 모든 필드—[Chat XDK](/ko/xchat/xchat-xdk) 레퍼런스 참고) +- 호출에 서명 키가 전달되지 않았고 `set_signing_keys`로 저장된 것도 없음 +- 발신자가 버전을 순환함—공개 키를 다시 가져오세요 +- 허용 최소값 아래의 키 버전은 결코 검증되지 않습니다 -`set_reject_unverified` 세터는 이 기본값을 **비활성화**(`false`, 권장하지 않음)하기 위해 존재합니다. 이전에 비활성화했다면 실패-폐쇄 기본값으로 복원하세요: +`set_reject_unverified` setter는 이 기본값에서 **옵트 아웃**하기 위해 존재합니다(`false`, 권장하지 않음). 이전에 비활성화했다면, 실패 시 거부 기본값을 복원하세요: @@ -170,37 +171,41 @@ keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encr -### 오래된 이벤트가 영구적으로 검증에 실패함 +### 답장이 `reply_preview_validation: "Invalid"`를 가짐 -**오래된** 이벤트에서 `signature missing or no matching signing key`나 ECDSA 불일치 같은 오류는 영구적입니다. 서명은 불변이며 이벤트 자체에서 서명된 페이로드를 재구축하여 검증되므로, 다른 바이트로 서명된 (또는 서명되지 않은) 이벤트는 이후 로드 시마다 실패합니다—재시도, 키 갱신, 또는 API 호출로도 치유할 수 없습니다. 이러한 이벤트를 재시도 가능한 오류가 아닌 툼스톤으로 취급하세요. 대화 키를 교체하면 그 시점 이후부터 깨끗하고 검증 가능한 이력이 시작됩니다; 새 메시지는 영향을 받지 않습니다. +복호화된 답장은 `reply_preview_validation`(`"Valid"` / `"Invalid"`; JavaScript는 `'valid'` / `'invalid'` 사용)을 가질 수 있습니다. `Invalid`는 메시지 내에 인용된 미리보기가 임베드된 서명된 원본 이벤트와 일치하지 않는다는 뜻입니다—인용을 신뢰할 수 없는 것으로 취급하고 검증된 원본에서만 인용된 콘텐츠를 렌더링하세요. 메시지 자체는 별도로 검증되며 여전히 진짜입니다; 잘못된 미리보기에 대해 예외가 발생하지 않습니다. + +### 오래된 이벤트가 영구적으로 검증 실패 + +`signature missing or no matching signing key` 또는 **오래된** 이벤트에 대한 ECDSA 불일치와 같은 오류는 영구적입니다. 서명은 불변이며 이벤트 자체에서 서명된 페이로드를 재구성하여 검증되므로, 다른 바이트로 서명되었거나(혹은 결코 서명되지 않은) 이벤트는 이후 모든 로드에서 실패합니다—어떤 재시도, 키 새로 고침, API 호출도 이를 치유할 수 없습니다. 이러한 이벤트는 재시도 가능한 오류가 아니라 묘비(tombstone)로 취급하세요. 대화 키를 순환하면 그 시점부터 깨끗하고 검증 가능한 히스토리가 시작됩니다; 새 메시지는 영향을 받지 않습니다. --- -## 전송 페이로드 구축 +## 전송 페이로드 구성 -이 실수들은 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을 반환함 +### 상태 변경 호출에 API가 400 반환 -모든 상태 변경 채팅 호출—대화 키 추가 또는 교체, 그룹 생성, 멤버 추가—에는 API 경계에서 검증되는 요청 본문의 **`action_signatures`**가 필요합니다. 누락되거나 잘못된 형식의 항목(각각 `message_id`, `encoded_message_event_detail`, 그리고 `signature`, `public_key_version`, `signature_version`이 있는 `message_event_signature`가 필요함)은 즉시 HTTP 400 problem-details 응답을 반환합니다. SDK prepare 메서드(`prepare_conversation_key_change`, `prepare_group_create`, `prepare_group_members_change`)를 사용하고 반환된 **모든** 서명을 보내세요—그룹 생성과 멤버 추가는 두 개를 반환합니다. +모든 상태 변경 채팅 호출—대화 키 추가 또는 순환, 그룹 생성, 멤버 추가—은 요청 본문에 **`action_signatures`**가 필요하며 API 경계에서 검증됩니다. 누락되거나 잘못된 형식의 항목(각각은 `message_id`, `encoded_message_event_detail`, 그리고 `signature`, `public_key_version`, `signature_version`을 가진 `message_event_signature`가 필요)은 즉시 HTTP 400 problem-details 응답을 반환합니다. SDK prepare 메서드(`prepare_conversation_key_change`, `prepare_group_create`, `prepare_group_members_change`)를 사용하고 반환된 **모든** 서명을 보내세요—그룹 생성과 멤버 추가는 두 개를 반환합니다. --- ## 미디어 암호화 및 복호화 - 첨부 파일을 참조하는 메시지와 **동일한** 대화 키(및 버전)를 사용하세요 -- `decrypt_stream`을 실행하기 전까지 다운로드 응답을 **암호문**으로 취급하세요 -- MIME 유형은 복호화 **후**에 추론하세요; 다운로드 `Content-Type`은 종종 실제 이미지 유형이 아닙니다 +- 다운로드 응답을 `decrypt_stream`을 실행할 때까지 **암호문**으로 취급하세요 +- MIME 타입은 복호화 **후**에 추론하세요; 다운로드 `Content-Type`은 종종 실제 이미지 타입이 아닙니다 -세부 사항: [미디어](/xchat/media). +세부 사항: [미디어](/ko/xchat/media). --- @@ -208,7 +213,7 @@ keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encr 암호화 실패를 조사할 때: -- 대화 ID, 이벤트 ID, 키 **버전**만 로그에 남기세요 -- 평문, 패스코드, 개인 키, 또는 전체 키 blob을 로그에 **남기지 마세요** -- `set_key_version`이 공개 키 레코드의 `public_key_version`과 일치하는지 확인하세요 -- 불완전한 이력의 경우, 복호화하기 전에 키 변경 메타데이터를 건너뛰지 않도록 **모든** 이벤트 페이지를 페이지 처리하세요 +- 대화 ID, 이벤트 ID, 그리고 키 **버전**만 로깅하세요 +- 평문, 패스코드, 개인 키, 또는 전체 키 blob은 로깅하지 **마세요** +- `set_identity`에 전달된 서명 키 버전이 공개 키 레코드의 `public_key_version`과 일치하는지 확인하세요 +- 불완전한 히스토리의 경우, 복호화 전에 키 변경 메타데이터가 건너뛰어지지 않도록 **모든** 이벤트 페이지를 페이지 조회하세요 diff --git a/ko/xchat/xchat-xdk.mdx b/ko/xchat/xchat-xdk.mdx index 694e20fea..69c63dce7 100644 --- a/ko/xchat/xchat-xdk.mdx +++ b/ko/xchat/xchat-xdk.mdx @@ -1,13 +1,13 @@ --- -title: Chat XDK 참조 +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와 함께 사용하세요. -앱 안내: [시작하기](/xchat/getting-started). 샘플 봇: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). +앱 상세 안내: [시작하기](/ko/xchat/getting-started). 샘플 봇: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). ### 설치 @@ -17,7 +17,7 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] pip install chatxdk ``` - PyPI 패키지는 `chatxdk`이며, `chat_xdk`로 임포트합니다. Python 3.10+ 필요. + PyPI 패키지는 `chatxdk`이며 `chat_xdk`로 import합니다. Python 3.10+이 필요합니다. ```bash @@ -25,14 +25,14 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] npm install juicebox-sdk # optional peer dependency — required for setup()/unlock() secure key backup ``` - 컴파일된 WASM 엔진이 패키지에 포함되어 있어 빌드 단계가 없습니다. Node.js 18+ 필요. + 컴파일된 WASM 엔진이 패키지 내부에 포함되어 있어 별도의 빌드 단계가 없습니다. Node.js 18+이 필요합니다. ```toml [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] @@ -44,25 +44,25 @@ keywords: ["Chat XDK", "chat-xdk", "encryption SDK", "E2EE SDK"] go get github.com/xdevplatform/chat-xdk/go/chatxdk ``` - 미리 컴파일된 정적 라이브러리가 포함되어 있습니다 (macOS arm64/amd64, Linux amd64 glibc/musl). C 컴파일러는 필요하지만 Rust는 필요 없습니다. Go 1.21+ 필요. + 사전 컴파일된 정적 라이브러리가 포함되어 있습니다(macOS arm64/amd64, Linux amd64 glibc/musl)—C 컴파일러는 필요하지만 Rust는 필요하지 않습니다. Go 1.21+이 필요합니다. ```bash 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 ``` - Maven Central에서 제공됩니다. jar에 macOS (arm64, x64), Linux (x64), Windows (x64)용 네이티브 라이브러리가 번들되어 있어 `jna.library.path` 설정이 필요 없습니다. `com.x.chatxdk`에서 임포트하세요. JDK 17+ 필요. + Maven Central에서 사용할 수 있습니다. jar에 macOS(arm64, x64), Linux(x64), Windows(x64)용 네이티브 라이브러리가 번들되어 있어 `jna.library.path` 설정이 필요 없습니다. `com.x.chatxdk`에서 import하세요. JDK 17+이 필요합니다. @@ -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)에 연결하세요. +키를 로드하고, 아이덴티티를 한 번 설정하고, 백로그를 복호화하고, 하나의 실시간 이벤트를 복호화하고, 메시지를 암호화합니다. [시작하기](/ko/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와 텍스트만 필요하게 합니다. 둘 중 하나를 건너뛰고 동일한 값을 호출별로 전달하세요—두 스타일 모두 동일하게 검증합니다; [Decrypt](#decrypt)를 참고하세요. ```python from chat_xdk import Chat - chat = Chat(juicebox_config_json) # or Chat() + import_keys(blob) + chat = Chat(juicebox_config_json) # or Chat() + import_keys(blob, version) chat.unlock("YOUR_PASSCODE") - chat.set_key_version(signing_key_version) - result = chat.decrypt_events(raw_events, signing_keys) + # Session defaults: identity for signing, stored signing keys for + # verification, opt-in cache for conversation keys + chat.set_identity(my_user_id, signing_key_version) + chat.set_signing_keys(signing_keys) # all participants + chat.set_cache_keys(True) + + # Batch-decrypt the backlog; senders verify against the stored keys + result = chat.decrypt_events(raw_events) for dm in result["messages"]: ev = dm["event"] - if ev.get("type") == "Message": - print(ev.get("sender_id"), ev.get("content", {}).get("text")) + if ev["type"] == "Message": + print(ev["sender_id"], ev["content"]["text"]) - cached = result["conversation_keys"]["keys"] - event = chat.decrypt_event(one_event_b64, cached, sender_signing_keys) + # Decrypt one live event with the cached conversation key + event = chat.decrypt_event(one_event_b64) - raw_key = cached[result["conversation_keys"]["latest_version"]] - payload = chat.encrypt_message( - message_id, sender_id, conversation_id, raw_key, "Hi!", - conversation_key_version, signing_key_version, - ) + # Encrypt and sign as the session identity, under the cached key + payload = chat.encrypt_message(event["conversation_id"], "Hi!") + message_id = payload.message_id # SDK-generated — send as message_id ``` @@ -106,38 +112,53 @@ 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 repo](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 | +| 그룹 생성(ID는 `POST /2/chat/conversations/group/initialize`가 생성) | `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`에는 신규 및 현재 명단)—샘플은 [그룹](/ko/xchat/groups#create-the-group-and-establish-keys)을 참고하세요. 둘 다 **두 개**의 액션 서명을 반환합니다; POST에는 둘 모두 포함해야 합니다. --- -## 복호화 +## Decrypt + +**`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`. -**`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`. 검증은 기본적으로 필수입니다: 서명 키 목록을 생략하거나 빈 목록을 전달해도 이를 건너뛰지 **않습니다**—서명된 이벤트는 실패합니다(`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가 이로부터 인용된 미리보기(sequence ID, 발신자, 텍스트, entities, attachments)를 도출하고 서명된 원본을 발신 메시지에 포함하여 수신자가 인용을 검증할 수 있게 합니다. 원본이 답장보다 이전 키 버전으로 암호화된 경우 원시 키 변경 이벤트를 `reply_to_ckces`로 전달하세요. 원본이 **편집된** 경우 원시 편집 이벤트를 `reply_to_edit_event`로 전달하세요: 그러면 미리보기가 메시지가 현재 말하는 것을 인용하며(텍스트와 entities는 편집에서 옴), 편집은 수신자가 확인할 수 있도록 원본과 함께 이동합니다. 명시적 `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와 대상 sequence 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`**은 첨부 파일 바이트를 암호화합니다; [미디어](/ko/xchat/media) 참고. 저수준 **`sign` / `verify` / `verify_key_binding`**은 고급 흐름을 지원합니다; 대화 키 변경, 그룹 생성, 멤버 추가는 [prepare 메서드](#conversation-keys)에 의해 서명됩니다. + +`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`**를 첨부하세요. 이는 Posts 미디어 모델(`expansions=attachments.media_keys`)이 아닙니다. 전체 업로드/다운로드 흐름: [미디어](/ko/xchat/media). @@ -659,12 +774,12 @@ SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. A -### 큰 미디어를 위한 증분 스트리밍 +### 대용량 미디어를 위한 증분 스트리밍 -큰 파일의 경우, 전체 페이로드를 메모리에 보관하지 마세요: `stream_encryptor()` / `stream_decryptor()`는 청크(각 약 1 MB)로 `push(chunk)`를 통해 공급한 다음 마지막에 `finish()`를 한 번 호출하는 `StreamEncryptor` / `StreamDecryptor`를 반환합니다. 복호화 시 `finish()`는 잘린 스트림을 감지합니다(마지막 프레임 전에 입력이 끝났으면 실패), 그러므로 성공하기 전까지 푸시된 평문을 완료된 것으로 취급하지 마세요. +대용량 파일의 경우 전체 페이로드를 메모리에 유지하지 않도록 하세요: `stream_encryptor()` / `stream_decryptor()`는 청크(약 1 MB 각각)로 `push(chunk)`를 공급하고 마지막에 `finish()`를 한 번 호출하는 `StreamEncryptor` / `StreamDecryptor`를 반환합니다. 복호화 시 `finish()`는 잘린 스트림을 감지합니다(마지막 프레임 전에 입력이 끝나면 실패). 따라서 성공할 때까지는 푸시된 평문을 완전한 것으로 취급하지 마세요. -**JS/WASM 전용:** `finish()`는 기본 WASM 객체를 소비하고 해제합니다—`finish()` 후에는 절대 `free()`를 호출하지 마세요(예외를 던집니다). `finish()` *이전*에 스트림을 포기하는 경우에만 `free()`를 호출하세요(예: 오류 경로에서). +**JS/WASM 전용:** `finish()`는 내부 WASM 객체를 소비하고 해제합니다—`finish()` 후에는 `free()`를 절대 호출하지 마세요(예외를 던집니다). `free()`는 finish 전에 스트림을 중단할 때만(예: 오류 경로에서) 호출하세요. @@ -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)로 제공됩니다—추가 라이브러리를 가져오지 않고 첨부 메타데이터를 구성할 때 유용합니다. @@ -792,39 +907,39 @@ Base64/hex 헬퍼, MIME 감지, 이미지 치수는 모듈 수준 함수(Python/ ## 중요한 타입 -이러한 개념적 타입은 언어 전반에 걸쳐 나타납니다(정확한 필드 이름은 다릅니다; JS는 종종 `message`와 같은 camelCase 이벤트 판별자를 사용합니다): +이러한 개념적 타입은 언어 전반에 걸쳐 나타납니다(정확한 필드 이름은 다르며, JS는 종종 `message`와 같은 카멜케이스 이벤트 판별자를 사용합니다): -- **SendPayload** — `encrypt_message` 및 관련 암호화 헬퍼의 반환값; 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`. +- **SendPayload** — `encrypt_message`와 다른 encrypt 헬퍼의 반환 값: 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` / 공개 키 getter의 출력. +- **SigningKeyEntry** — 서명 검증을 위해 decrypt에 전달되거나 `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** — messages, 선택적 errors, 그리고 추출된 `conversation_keys`. 답장을 인용하는 복호화된 메시지는 `reply_preview_validation`을 가집니다([암호화 및 전송 헬퍼](#encrypt-and-send-helpers) 참고). -전체 필드 목록은 [chat-xdk 저장소](https://github.com/xdevplatform/chat-xdk)의 언어 스텁(`docs/API.md`, `*.pyi`, `index.d.ts`)을 사용하세요. +전체 필드 목록은 [chat-xdk repo](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 호출도 이를 치유할 수 없습니다. 이러한 오류는 일시적 오류가 아니라 묘비(tombstone)로 취급하세요. 대화 키를 순환하면 그 시점부터 깨끗하고 검증 가능한 히스토리가 시작됩니다. --- ## 다음 단계 - - Chat XDK를 Chat API에 연결하기 + + Chat XDK를 Chat API에 연결 - - 스트림 암호화 및 미디어 REST + + 스트림 암호화와 미디어 REST - - 웹훅 및 활동 전달 + + 웹훅과 활동 전달 - + 일반적인 실패 diff --git a/pt/xchat/cryptography-primer.mdx b/pt/xchat/cryptography-primer.mdx index 00dd36511..af916ed61 100644 --- a/pt/xchat/cryptography-primer.mdx +++ b/pt/xchat/cryptography-primer.mdx @@ -1,18 +1,18 @@ --- -title: Primer de criptografia -sidebarTitle: Primer de criptografia -description: Aprenda os conceitos de ECDH, criptografia de chave pública e assinaturas digitais por trás da criptografia de ponta a ponta do X Chat. +title: Introdução à Criptografia +sidebarTitle: Introdução à Criptografia +description: Conheça os conceitos de ECDH, criptografia de chave pública e assinaturas digitais por trás da criptografia de ponta a ponta do X Chat, sem detalhes de implementação. keywords: ["X Chat cryptography", "E2EE primer", "encryption basics", "public key encryption", "ECDH", "digital signatures", "conversation keys"] --- import { Button } from '/snippets/button.mdx'; -Este primer explica as ideias criptográficas por trás do X Chat em nível conceitual. Você não precisa dessa profundidade para desenvolver — o [Chat XDK](/xchat/xchat-xdk) realiza a criptografia, descriptografia, assinatura e armazenamento de chaves para você — mas o modelo mental ajuda ao projetar seu app ou depurar comportamentos. +Esta introdução explica as ideias criptográficas por trás do X Chat em nível conceitual. Você não precisa desse aprofundamento para desenvolver — o [Chat XDK](/pt/xchat/xchat-xdk) faz a criptografia, descriptografia, assinatura e o armazenamento de chaves por você — mas o modelo mental ajuda quando você projeta seu app ou depura o comportamento. -Quando estiver pronto para implementar, use o [Guia de introdução](/xchat/getting-started) para um passo a passo completo e a [Referência da API](/x-api/chat/get-chat-conversations) na barra lateral para rotas individuais. +Quando estiver pronto para implementar, consulte [Primeiros passos](/pt/xchat/getting-started) para um passo a passo completo e a [referência da API](/x-api/chat/get-chat-conversations) na barra lateral para rotas individuais. -**Você não implementa essa criptografia por conta própria.** O Chat XDK cuida disso. Esta página é para entendimento, não uma checklist de API. +**Você não implementa esta criptografia por conta própria.** O Chat XDK cuida disso. Esta página é para entendimento, não uma lista de verificação de API. --- @@ -21,11 +21,11 @@ Quando estiver pronto para implementar, use o [Guia de introdução](/xchat/gett O X Chat usa um sistema de criptografia em camadas onde: -1. **Mensagens** são criptografadas com uma **chave de conversa** (criptografia simétrica rápida) -2. **Chaves de conversa** são criptografadas para cada participante usando sua **chave pública de identidade** (troca de chaves assimétrica) -3. **Mensagens são assinadas** com a **chave de assinatura** para que os destinatários possam verificar quem as enviou e que nada foi alterado +1. As **mensagens** são criptografadas com uma **chave da conversa** (criptografia simétrica rápida) +2. As **chaves da conversa** são criptografadas para cada participante usando sua **chave pública de identidade** (troca de chaves assimétrica) +3. As **mensagens são assinadas** com a **chave de assinatura**, para que os destinatários possam verificar quem as enviou e que nada foi alterado -A criptografia simétrica é eficiente para muito tráfego de mensagens; a assimétrica é usada principalmente para **distribuir** chaves de conversa com segurança. +A criptografia simétrica é eficiente para grandes volumes de tráfego de mensagens; a criptografia assimétrica é usada principalmente para **distribuir** com segurança as chaves de conversa. ```mermaid flowchart TB @@ -45,7 +45,7 @@ flowchart TB end ``` -No fluxo do produto, o X transporta **texto cifrado e envelopes de chaves** — não conteúdo legível de mensagens nem a chave de conversa em bruto. Seu app usa o Chat XDK para a criptografia e a [Chat API](/xchat/introduction) (via XDK em Python/TypeScript, ou HTTPS) para registrar chaves e enviar ou receber esses payloads criptografados. Veja o [Guia de introdução](/xchat/getting-started) para como essas peças se encaixam. +No fluxo do produto, o X transporta **texto cifrado e envelopes de chave** — não conteúdo de mensagem legível nem a chave da conversa em bruto. Seu app usa o Chat XDK para criptografia e a [Chat API](/pt/xchat/introduction) (via XDK em Python/TypeScript ou HTTPS) para registrar chaves e enviar ou receber esses payloads criptografados. Consulte [Primeiros passos](/pt/xchat/getting-started) para entender como essas peças se encaixam. --- @@ -55,40 +55,40 @@ O X Chat usa três tipos de material de chave, cada um com um propósito especí ### 1. Par de chaves de identidade -**Propósito:** Trocar chaves de conversa entre usuários com segurança +**Propósito:** trocar com segurança chaves de conversa entre usuários | Componente | Descrição | |:-----------|:----------| | **Chave pública de identidade** | Compartilhada com outros; usada para criptografar chaves de conversa *para* você | -| **Chave privada de identidade** | Mantida em segredo; usada para descriptografar chaves de conversa enviadas *a* você | +| **Chave privada de identidade** | Mantida em segredo; usada para descriptografar chaves de conversa enviadas *para* você | -Quando alguém adiciona você a uma conversa, essa pessoa criptografa a chave de conversa usando sua chave pública de identidade. Somente sua chave privada de identidade pode descriptografá-la. +Quando alguém adiciona você a uma conversa, essa pessoa criptografa a chave da conversa usando sua chave pública de identidade. Apenas sua chave privada de identidade pode descriptografá-la. -As metades públicas são registradas e descobertas por meio das APIs de **chaves públicas** da plataforma (veja Chaves de criptografia na Referência da API). As metades privadas ficam no Chat XDK (por exemplo via [backup seguro de chaves](#backup-seguro-de-chaves-armazenamento-distribuido-de-chaves) ou um blob de chave cuidadosamente protegido). +As metades públicas são registradas e descobertas por meio das APIs de **chave pública** da plataforma (veja Chaves de criptografia na referência de API). As metades privadas ficam no Chat XDK (por exemplo, via [backup seguro de chave](#secure-key-backup-distributed-key-storage) ou um blob de chave cuidadosamente protegido). ### 2. Par de chaves de assinatura -**Propósito:** Provar que você é o autor de uma mensagem +**Propósito:** provar que você é o autor de uma mensagem | Componente | Descrição | |:-----------|:----------| | **Chave pública de assinatura** | Compartilhada com outros; usada para verificar suas assinaturas | | **Chave privada de assinatura** | Mantida em segredo; usada para assinar suas mensagens | -Quando você envia uma mensagem, ela é assinada com sua chave privada de assinatura. Os destinatários verificam usando sua chave pública de assinatura (também publicada pelas APIs de chaves públicas). O Chat XDK assina como parte da criptografia de uma mensagem e pode verificar na descriptografia quando você fornece o material de chave pública do remetente. +Quando você envia uma mensagem, ela é assinada com sua chave privada de assinatura. Os destinatários verificam usando sua chave pública de assinatura (também publicada pelas APIs de chave pública). O Chat XDK assina como parte da criptografia de uma mensagem e pode verificar na descriptografia quando você fornece o material de chave pública do remetente. -### 3. Chave de conversa +### 3. Chave da conversa -**Propósito:** Criptografar e descriptografar mensagens (e mídia) dentro de uma conversa específica +**Propósito:** criptografar e descriptografar mensagens (e [mídia](/pt/xchat/media)) dentro de uma conversa específica | Propriedade | Descrição | |:------------|:----------| | **Simétrica** | A mesma chave criptografa e descriptografa | | **Por conversa** | Cada conversa tem sua própria chave | | **Compartilhada entre participantes** | Todos os participantes que devem ler a conversa têm uma cópia | -| **Versionada** | As chaves podem ser rotacionadas; apps devem acompanhar versões ao longo do tempo | +| **Versionada** | As chaves podem ser rotacionadas; os apps devem rastrear versões ao longo do tempo | -As chaves de conversa são geradas quando uma conversa é configurada ou quando as chaves são rotacionadas. Cada participante recebe uma **cópia criptografada** da chave, produzida com sua chave pública de identidade. Depois de descriptografar sua cópia uma vez, você mantém a chave de conversa **em bruto** e a usa para criptografia rápida de mensagens (e [mídia](/xchat/media)). Configurar essas cópias para uma conversa é feito por meio do Chat XDK junto com os endpoints de **chaves** de conversa — passo a passo em [Guia de introdução](/xchat/getting-started#4-set-up-conversation-keys). +As chaves de conversa são geradas quando uma conversa é configurada ou quando as chaves são rotacionadas. Cada participante recebe uma **cópia criptografada** da chave, produzida com sua chave pública de identidade. Depois de descriptografar sua cópia uma vez, você guarda a chave da conversa em **bruto** e a usa para criptografia rápida de mensagens (e [mídia](/pt/xchat/media)). A configuração dessas cópias para uma conversa é feita por meio do Chat XDK juntamente com os endpoints de **chave** de conversa — abordados em [Primeiros passos](/pt/xchat/getting-started#4-set-up-conversation-keys). --- @@ -98,19 +98,19 @@ As chaves de conversa são geradas quando uma conversa é configurada ou quando - Você digita: "Olá, como você está?" + Você digita: "Olá, tudo bem?" - - Seu app usa a chave de conversa em bruto para este chat (da configuração ou de um evento anterior de distribuição de chave), na versão correta da chave. + + Seu app usa a chave da conversa em bruto para este chat (obtida na configuração ou em um evento anterior de distribuição de chaves), para a versão de chave correta. - O Chat XDK criptografa sua mensagem com a chave de conversa. O resultado é texto cifrado que é inútil sem essa chave. + O Chat XDK criptografa sua mensagem com a chave da conversa. O resultado é texto cifrado que é inútil sem essa chave. - O Chat XDK assina o payload criptografado com sua chave privada de assinatura, provando que você é o autor exato deste conteúdo. + O Chat XDK assina o payload criptografado com sua chave privada de assinatura, provando que você é o autor deste conteúdo exato. - - Seu app envia o payload criptografado e a assinatura ao X pelo endpoint **send message** da Chat API. O X armazena e entrega bytes que não consegue ler em texto simples. + + Seu app envia o payload criptografado e a assinatura para o X por meio do endpoint **send message** da Chat API. O X armazena e entrega bytes que ele não consegue ler em texto simples. @@ -118,73 +118,73 @@ As chaves de conversa são geradas quando uma conversa é configurada ou quando - Seu app recebe texto cifrado do X — via [webhooks ou um stream de atividades](/xchat/real-time-events), ou lendo **eventos** de conversa para o histórico. + Seu app recebe texto cifrado do X — via [webhooks ou um activity stream](/pt/xchat/real-time-events), ou lendo os **eventos** da conversa para obter o histórico. - - Use sua chave em bruto em cache, ou obtenha-a descriptografando sua cópia de um evento de distribuição (mudança) de chave se esta for nova ou rotacionada. + + Use sua chave em bruto em cache, ou obtenha-a descriptografando sua cópia a partir de um evento de distribuição de chave (mudança de chave), caso seja nova ou tenha sido rotacionada. - O Chat XDK verifica a assinatura usando a chave pública de assinatura do remetente (e o vínculo de identidade relacionado), para que você saiba quem enviou e que não foi modificado. + O Chat XDK verifica a assinatura usando a chave pública de assinatura do remetente (e a vinculação de identidade relacionada), para que você saiba quem a enviou e que ela não foi modificada. - O Chat XDK descriptografa com a chave de conversa. Agora você pode ler: "Olá, como você está?" + O Chat XDK descriptografa com a chave da conversa. Agora você pode ler: "Olá, tudo bem?" -A implementação de criptografar, enviar, receber e descriptografar está em [Guia de introdução](/xchat/getting-started) e na referência do [Chat XDK](/xchat/xchat-xdk). +A implementação de criptografia, envio, recebimento e descriptografia está em [Primeiros passos](/pt/xchat/getting-started) e na referência do [Chat XDK](/pt/xchat/xchat-xdk). --- ## Distribuição de chaves explicada -Um desafio central na criptografia de ponta a ponta é a **distribuição de chaves**: como os participantes obtêm a chave de conversa **sem** que o X (ou um observador) veja essa chave em claro. +Um desafio central em criptografia de ponta a ponta é a **distribuição de chaves**: como os participantes obtêm a chave da conversa **sem** que o X (ou um observador) veja essa chave em texto claro. -### Configuração inicial de chaves +### Configuração inicial da chave -Quando uma conversa é preparada para mensagens: +Quando uma conversa é preparada para envio de 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. +O X só lida com as cópias **empacotadas**, nunca com a chave da conversa em bruto. ### Eventos de mudança de chave -Quando a chave de conversa é rotacionada (por exemplo, quando a composição muda), os participantes recebem um evento de **mudança de chave** com novas cópias criptografadas para cada membro. +Quando a chave da conversa é rotacionada (por exemplo, quando a composição do grupo muda), os participantes recebem um evento de **mudança de chave** com novas cópias criptografadas para cada membro. Seu app deve: 1. Notar material de mudança de chave em eventos ao vivo ou no histórico da conversa -2. Descriptografar e armazenar a nova chave de conversa (e versão) +2. Descriptografar e armazenar a nova chave da conversa (e a versão) 3. Usar a versão mais recente para envios subsequentes -[Guia de introdução](/xchat/getting-started#6-receive-and-decrypt) e [Eventos em tempo real](/xchat/real-time-events) descrevem onde esses eventos aparecem na prática. +[Primeiros passos](/pt/xchat/getting-started#6-receive-and-decrypt) e [Eventos em tempo real](/pt/xchat/real-time-events) descrevem onde esses eventos aparecem na prática. --- -## Backup seguro de chaves: armazenamento distribuído de chaves +## Backup seguro de chave: 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 assinatura devem ser armazenadas com cuidado. O X Chat inclui um sistema de **backup seguro de chave** para que as chaves possam ser recuperadas com um código de acesso entre dispositivos, sem que nenhum servidor detenha o segredo completo. ### O problema com o armazenamento tradicional de chaves | Abordagem | Problema | |:----------|:---------| -| Armazenar apenas no dispositivo | Perder o dispositivo = perder as chaves = perder acesso ao histórico de mensagens | +| Armazenar apenas no dispositivo | Perder o dispositivo = perder as chaves = perder o acesso ao histórico de mensagens | | Armazenar em um backup em nuvem comum | O provedor pode acessar o material da chave | -| Memorizar uma chave longa | As pessoas não conseguem memorizar chaves de alta entropia de forma confiável | +| Lembrar uma chave longa | As pessoas não conseguem memorizar de forma confiável chaves de alta entropia | -### Como o backup seguro de chaves resolve isso +### Como o backup seguro de chave resolve isso -O backup seguro de chaves combina **compartilhamento de segredos** com **proteção por código de acesso**: +O backup seguro de chave combina **compartilhamento de segredo** com **proteção por código de acesso**: -1. As chaves privadas são **divididas em partes** +1. As chaves privadas são **divididas em partes (shares)** 2. As partes são mantidas por **realms independentes** (servidores separados) -3. **Nenhum realm sozinho** tem informações suficientes para reconstruir as chaves -4. A recuperação requer seu **código de acesso** e cooperação de **realms suficientes** -5. Códigos de acesso incorretos têm **limite de taxa** para retardar tentativas +3. **Nenhum realm sozinho** tem informação suficiente para reconstruir as chaves +4. A recuperação exige seu **código de acesso** e a cooperação de **realms suficientes** +5. Códigos incorretos têm **limitação de taxa** para retardar tentativas ```mermaid flowchart LR @@ -198,17 +198,17 @@ flowchart LR end ``` -Você obtém recuperabilidade (novo dispositivo + código de acesso) sem que um único agente detenha o segredo inteiro. +Você obtém capacidade de recuperação (novo dispositivo + código de acesso) sem que um único participante 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 chave manualmente no caminho normal. O Chat XDK inclui o cliente de backup; a configuração dos realms vem da API do X pelo campo **`juicebox_config`** do 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](/pt/xchat/getting-started#2-initialize-the-chat-xdk-with-existing-keys) e [criar e registrar chaves](/pt/xchat/getting-started#3-create-and-register-keys-first-time-setup) em Primeiros passos. Alguns apps (especialmente servidores e bots) usam um blob de chave exportado em vez do backup seguro de chave; proteja esse material como uma senha. --- ## Assinaturas explicadas -Toda mensagem do X Chat inclui uma **assinatura digital** que oferece: +Toda mensagem do X Chat inclui uma **assinatura digital** que apoia: 1. **Autenticidade** — foi produzida com a chave privada de assinatura do remetente 2. **Integridade** — o conteúdo criptografado não foi modificado após a assinatura @@ -220,19 +220,21 @@ Toda mensagem do X Chat inclui uma **assinatura digital** que oferece: | **Assinar** | Chave privada de assinatura do remetente | Uma assinatura vinculada a esta mensagem criptografada exata | | **Verificar** | Chave pública de assinatura do remetente | Confirma que a assinatura corresponde à mensagem e à chave | -Se algo no material assinado muda, a verificação falha. Somente alguém com a chave privada de assinatura pode produzir uma assinatura válida para essa chave. +Se qualquer coisa no material assinado mudar, a verificação falha. Apenas alguém com a chave privada de assinatura pode produzir uma assinatura válida para essa chave. ### 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 descriptografa as de entrada, comparando com o material de chave pública do remetente (das APIs de chave pública). A verificação é **obrigatória por padrão**: o SDK rejeita eventos assinados não verificados, a menos que você desabilite explicitamente a checagem (não recomendado). Detalhes estão na referência do [Chat XDK](/pt/xchat/xchat-xdk). + +As assinaturas também cobrem conteúdo citado. Uma resposta incorpora a mensagem original **assinada** em bruto que ela cita; quando o Chat XDK descriptografa a resposta, ele verifica essa original incorporada e compara a citação com ela, reportando o resultado como `reply_preview_validation` (`Valid` / `Invalid`). Um resultado `Invalid` significa que a citação não corresponde ao original assinado — trate o material citado como não confiável, mesmo que a resposta em si seja verificada separadamente — para que nenhum participante possa atribuir palavras falsas a outro. ### Mudanças de estado assinadas (assinaturas de ação) -Mensagens não são o único material assinado. Toda chamada que muda o estado da conversa — adicionar ou rotacionar chaves de conversa, criar um grupo, adicionar membros — deve carregar uma ou mais **assinaturas de ação**: o remetente assina um payload descrevendo exatamente o que a mudança faz (para uma mudança de chave, esse payload inclui a nova chave de conversa em si), e a API rejeita a requisição se as assinaturas estiverem ausentes ou malformadas. +Mensagens não são o único material assinado. Toda chamada que muda o estado da conversa — adicionar ou rotacionar chaves de conversa, criar um grupo, adicionar membros — deve carregar uma ou mais **assinaturas de ação**: o remetente assina um payload descrevendo exatamente o que a mudança faz (para uma mudança de chave, esse payload inclui a própria nova chave da conversa), e a API rejeita a solicitação se as assinaturas estiverem ausentes ou malformadas. -Como o servidor nunca detém a chave de conversa em texto simples, ele não pode verificar criptograficamente a assinatura de uma mudança de chave; ele valida que a descrição assinada e codificada da mudança corresponde à requisição recebida. A verificação **criptográfica** acontece nas extremidades: o Chat XDK de cada destinatário verifica a assinatura contra a chave pública de assinatura do remetente ao descriptografar o evento de mudança de chave. Os métodos `prepare` do Chat XDK produzem essas assinaturas para você — criações de grupo e adições de membros retornam **duas** (a mudança de chave mais a ação de grupo), e ambas precisam ser enviadas. +Como o servidor nunca detém a chave da conversa em texto claro, ele não pode verificar criptograficamente a assinatura de uma mudança de chave; ele valida que a descrição assinada e codificada da mudança corresponde à solicitação recebida. A verificação **criptográfica** acontece nas extremidades: o Chat XDK de cada destinatário verifica a assinatura contra a chave pública de assinatura do remetente ao descriptografar o evento de mudança de chave. Os métodos `prepare` do Chat XDK produzem essas assinaturas para você — criação de grupo e adição de membros retornam **duas** (a mudança de chave mais a ação de grupo), e ambas devem ser enviadas. -Assinaturas são vinculadas ao conteúdo do evento e são imutáveis: um evento cuja assinatura não verifica nunca poderá se tornar válido depois. Veja [Solução de problemas](/xchat/troubleshooting) para saber como tratá-los. +As assinaturas são vinculadas ao conteúdo do evento e são imutáveis: um evento cuja assinatura não verifica nunca poderá se tornar válido depois. Veja [Solução de problemas](/pt/xchat/troubleshooting) para saber como lidar com esses casos. --- @@ -242,19 +244,19 @@ 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 | -| **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 | +| **O X ler o corpo das mensagens** | O conteúdo é criptografado antes de ser enviado ao X | +| **Interceptadores de rede** | Segurança de transporte mais conteúdo criptografado de ponta a ponta | +| **Adulteração de mensagens** | Assinaturas detectam modificação | +| **Falsificação trivial de remetente** | Assinaturas válidas exigem a chave privada de assinatura do remetente | +| **Roubo de chave em um único servidor (com backup seguro de chave)** | As partes são divididas entre realms e protegidas por código de acesso | ### Contra o que o X Chat **não** protege -| Ameaça | Por que não | -|:-------|:------------| +| Ameaça | Por quê | +|:-------|:--------| | **Dispositivo comprometido** | Texto simples e chaves podem ser expostos em um cliente desbloqueado | -| **Metadados** | O X pode saber quem enviou mensagem para quem e quando — não o texto da mensagem | -| **Sigilo direto (forward secrecy)** | O comprometimento das chaves de identidade pode expor chaves de conversa que foram envelopadas para essas chaves | +| **Metadados** | O X pode saber quem enviou mensagens para quem e quando — não o texto da mensagem | +| **Sigilo futuro (forward secrecy)** | Comprometer chaves de identidade pode expor chaves de conversa empacotadas com essas chaves | | **Segurança pós-comprometimento** | Rotacionar chaves não reescreve o histórico | --- @@ -263,33 +265,33 @@ 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 simétrica** | A mesma chave criptografa e descriptografa (usada para mensagens e fluxos de mídia) | +| **Criptografia assimétrica** | Chaves diferentes para criptografar e 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 | +| **Par de chaves (keypair)** | Uma chave pública e uma chave privada vinculadas | +| **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) | -| **Compartilhamento de segredos** | Dividir um segredo de forma que várias partes sejam necessárias para reconstruí-lo | -| **Realm** | Um servidor independente de backup seguro de chaves que detém uma parte do seu material de chave | +| **Chave da conversa** | Chave simétrica compartilhada pelos participantes de uma conversa (versionada ao longo do tempo) | +| **Compartilhamento de segredo** | Dividir um segredo de modo que várias partes sejam necessárias para reconstruí-lo | +| **Realm** | Um servidor de backup seguro de chave independente que detém uma parte do seu material de chave | --- ## Próximos passos - + Implemente chaves, envio e recebimento passo a passo - + Métodos e tipos do SDK de criptografia - + Visão geral do produto e arquitetura - - Como eventos criptografados são entregues + + Como os eventos criptografados são entregues diff --git a/pt/xchat/getting-started.mdx b/pt/xchat/getting-started.mdx index 0cfc7ad86..53ac23c6b 100644 --- a/pt/xchat/getting-started.mdx +++ b/pt/xchat/getting-started.mdx @@ -1,29 +1,29 @@ --- title: Primeiros passos com a Chat API -sidebarTitle: Guia de introdução -description: Tutorial passo a passo para criar mensagens X Chat com criptografia de ponta a ponta usando o Chat XDK em Python, TypeScript, Go, Rust, C# ou Java. +sidebarTitle: Primeiros passos +description: Tutorial passo a passo para criar mensagens do X Chat com criptografia de ponta a ponta usando o Chat XDK em Python, TypeScript, Go, Rust, C# ou Java. keywords: ["X Chat tutorial", "X Chat quickstart", "Chat XDK", "encrypted DM", "Python", "TypeScript", "Go", "Rust", "C#", "Java"] --- -Envie e receba mensagens diretas com criptografia de ponta a ponta no X: configure chaves, inicialize uma conversa, envie uma mensagem e descriptografe o tráfego de entrada. +Envie e receba mensagens diretas criptografadas de ponta a ponta no X: configure chaves, inicialize uma conversa, envie uma mensagem e descriptografe o tráfego de entrada. -Os apps do X Chat usam duas peças em conjunto: +Os apps de X Chat usam duas partes em conjunto: | Componente | Papel | |:-----------|:------| -| **[Chat XDK](/xchat/xchat-xdk)** | Criptografia, descriptografia, assinatura e armazenamento de chave privada (backup seguro de chaves ou blob de chave) | -| **X API** | Chaves públicas, chaves de conversa, mensagens e eventos — via o XDK [Python](/xdks/python/overview) ou [TypeScript](/xdks/typescript/overview), ou HTTPS com um token de acesso do usuário | +| **[Chat XDK](/pt/xchat/xchat-xdk)** | Criptografia, descriptografia, assinatura e armazenamento de chave privada (backup seguro de chave ou um blob de chave) | +| **API do X** | Chaves públicas, chaves de conversa, mensagens e eventos — via XDK em [Python](/xdks/python/overview) ou [TypeScript](/xdks/typescript/overview), ou HTTPS com um token de acesso de usuário | **Pré-requisitos** - [Conta de desenvolvedor](https://developer.x.com/en/portal/petition/essential/basic-info) e um app configurado para OAuth 2.0 -- Token de acesso do usuário com `dm.read`, `dm.write`, `tweet.read` e `users.read` +- Token de acesso de usuário com `dm.read`, `dm.write`, `tweet.read` e `users.read` --- -## 1. Instalar dependências +## 1. Instale as dependências @@ -39,17 +39,16 @@ Os apps do X Chat usam duas peças em conjunto: npm install juicebox-sdk # optional peer dependency — required for setup()/unlock() secure key backup ``` - O motor WASM compilado vem dentro de `@xdevplatform/chat-xdk` — sem etapa de build. Requer Node.js 18+. + O mecanismo WASM compilado é entregue dentro de `@xdevplatform/chat-xdk` — sem etapa de build. Requer Node.js 18+. ```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] @@ -61,25 +60,25 @@ Os apps do X Chat usam duas peças em conjunto: go get github.com/xdevplatform/chat-xdk/go/chatxdk ``` - Bibliotecas estáticas pré-compiladas estão incluídas (macOS arm64/amd64, Linux amd64 glibc/musl) — você precisa de um compilador C, mas não de Rust. Requer Go 1.21+. + Bibliotecas estáticas pré-compiladas estão incluídas (macOS arm64/amd64, Linux amd64 glibc/musl) — você precisa de um compilador C, mas não do Rust. Requer Go 1.21+. ```bash dotnet add package XDevPlatform.ChatXdk ``` - O pacote é autossuficiente: inclui as bibliotecas nativas para macOS (arm64, x64), Linux (x64) e Windows (x64). Requer .NET 8+. + O pacote é autocontido: bibliotecas nativas para macOS (arm64, x64), Linux (x64) e Windows (x64) são entregues dentro dele. Requer .NET 8+. ```xml com.x chatxdk - 0.2.1 + 0.4.0 ``` - Disponível no Maven Central. O jar inclui a biblioteca nativa para macOS (arm64, x64), Linux (x64) e Windows (x64) — não é preciso configurar `jna.library.path`. Importe de `com.x.chatxdk`. Requer JDK 17+. + Disponível no Maven Central. O jar inclui a biblioteca nativa para macOS (arm64, x64), Linux (x64) e Windows (x64) — não é necessário configurar `jna.library.path`. Importe de `com.x.chatxdk`. Requer JDK 17+. @@ -131,16 +130,16 @@ Crie um cliente de API com seu token de acesso OAuth 2.0 de **usuário**: --- -## 2. Inicializar o Chat XDK com chaves existentes +## 2. Inicialize o Chat XDK com chaves existentes -Este passo **carrega chaves que você já tem** — use-o quando esta identidade já concluiu a configuração inicial antes: +Esta etapa **carrega chaves que você já possui** — use-a quando esta identidade já concluiu a configuração inicial: -- **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`. +- **Backup seguro de chave:** construa o SDK com o `juicebox_config` do seu registro de chave pública e depois `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 previamente 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: cada chamada posterior de criptografia e preparação assina como esta 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 a [etapa 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,28 +237,34 @@ 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); } ``` -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. +Amostras de servidor e bot geralmente usam um **blob de chave** (`export_keys` / `import_keys`). Apps cliente geralmente usam **backup seguro de chave** (`setup` / `unlock` com um código de acesso). Veja a referência do [Chat XDK](/pt/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 este blob por conta própria: gere chaves com `generate_keypairs` ([etapa 3](#3-create-and-register-keys-first-time-setup)), exporte o blob uma vez e armazene-o codificado em base64. Blobs feitos à mão ou modificados falham ao importar. --- -## 3. Criar e registrar chaves (configuração inicial) +## 3. Crie e registre as chaves (configuração inicial) + +Pule esta etapa se você carregou chaves existentes na [etapa 2](#2-initialize-the-chat-xdk-with-existing-keys). Caso contrário, a configuração única para uma nova identidade faz **três coisas**: + +1. **Criar os pares de chaves** — `generate_keypairs` produz os pares de identidade e de assinatura. +2. **Armazenar as chaves privadas** — `setup` com um código de acesso as grava no backup seguro de chave (clientes), ou `export_keys` retorna um blob de chave para você armazenar de forma segura (servidores e bots). +3. **Registrar as chaves públicas** — faça POST do payload de registro no endpoint add-public-key para que outros possam criptografar para você e verificar suas assinaturas. -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**: +Finalize chamando `set_identity` com a versão de chave do registro, para que esta sessão assine como a nova identidade. -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). + +Scripts prontos de registro único para todos os bindings ficam 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 montar o fluxo abaixo à mão quando você só precisa integrar 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,25 +384,25 @@ 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"); ``` -Use um código de acesso forte para o backup seguro de chaves. Perder o código de acesso ou um blob de chave desprotegido pode impedir a descriptografia de mensagens antigas. +Use um código de acesso forte para o backup seguro de chave. Perder o código de acesso ou um blob de chave desprotegido pode impedir a descriptografia de mensagens antigas. --- -## 4. Configurar chaves de conversa +## 4. Configure as 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ê definiu na etapa 2. Uma 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 elas). Guarde a chave da 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. +A resposta retorna o ID canônico da conversa (`data.conversation_id` — o par unido por hífen para 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 solicitações posteriores 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 versão de chave mais nova. Rotacione quando suspeitar que a chave da conversa foi exposta — a rotação protege apenas mensagens **futuras**; mensagens criptografadas com versões anteriores permanecem legíveis para quem possui essas versões. -**Verifique as chaves buscadas antes de envelopar.** `prepare_conversation_key_change` criptografa a nova chave de conversa para quaisquer chaves públicas que você passar. Verifique cada registro obtido primeiro com `verify_key_binding(identity, signing, signature)` — passando os campos `public_key`, `signing_public_key` e `identity_public_key_signature` do registro 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 obtidas antes de empacotar.** `prepare_conversation_key_change` criptografa a nova chave de conversa para quaisquer chaves públicas que você passar. Verifique cada registro obtido primeiro com `verify_key_binding(identity, signing, signature)` — passando os campos `public_key`, `signing_public_key` e `identity_public_key_signature` do registro da API de chaves públicas — para que uma chave de identidade substituída não possa receber a chave da conversa. @@ -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) { @@ -671,38 +676,34 @@ A resposta retorna o ID canônico da conversa (`data.conversation_id` — o par --- -## 5. Enviar uma mensagem +## 5. Envie 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 da conversa em **bruto** da etapa 4. O SDK gera o ID da mensagem (um UUID), incorpora-o no evento assinado e o retorna no payload — você nunca cria um por conta própria. Na requisição de envio, mapeie: -| Campo Chat XDK | Campo do corpo da requisição | -|:---------------|:------------------------------| +| Campo do 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. +Use um ID de conversa **com hífen** no caminho da URL quando a API exigir (`:` → `-`). O SDK em si é 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 caminhos de URL (em qualquer ordem), ou apenas o ID de usuário do destinatário — e o canonizam antes de assinar. IDs de grupo (com prefixo `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,22 +822,26 @@ Use um ID de conversa **com hífen** no path da URL quando a API exigir (`:` → + +Os snippets passam a chave da conversa explicitamente porque, neste fluxo, você acabou de criá-la na etapa 4. Uma vez que o cache de chaves esteja ativo e uma passagem por `decrypt_events` tenha verificado a chave da conversa ([etapa 6](#6-receive-and-decrypt)), `encrypt_message(conversation_id, text)` sozinho já basta — o SDK preenche a chave verificada mais recente. Retentativas devem reenviar o **mesmo** payload criptografado, para que um ID nunca seja gerado duas vezes. + + --- -## 6. Receber e descriptografar +## 6. Receba e descriptografe -Use [webhooks ou o stream de atividades](/xchat/real-time-events) para tráfego ao vivo, ou pagine os **eventos** de conversa para o histórico. +Use [webhooks ou o activity stream](/pt/xchat/real-time-events) para tráfego ao vivo, ou pagine os **eventos** da conversa para obter o histórico. -- 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)) -- JavaScript usa tipos de evento em camelCase (`message`); outras linguagens usam `"Message"` e campos snake_case no JSON +- Campos do 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 mais `meta.conversation_key_events` +- Descriptografar precisa das **chaves de assinatura** dos remetentes para que o SDK possa verificar quem escreveu cada mensagem. Estas são as chaves *públicas* dos outros participantes — obtenha-as do mesmo endpoint de chaves públicas que você usou na etapa 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 opcionais de sessão uma vez e usar as formas curtas de chamada. Os snippets abaixo usam os armazenamentos: `set_signing_keys(entries)` guarda as chaves dos participantes, e `set_cache_keys(true)` (desligado por padrão) mantém a chave mais recente **verificada por assinatura** de cada conversa, para que chamadas posteriores possam omitir argumentos de chave. Ambos os estilos verificam de forma idêntica +- JavaScript usa tipos de evento em camelCase (`message`); outras linguagens usam `"Message"` e campos em 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 -Bots completos de poll-and-reply para cada linguagem: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). + +**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. Onde 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 os overrides `conversation_key`/`conversation_key_version` nos métodos de criptografia. Persista você mesmo os `conversation_keys` retornados por `decrypt_events` e passe-os de volta. + + +Bots completos que fazem poll e reply em todas as linguagens: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). --- -## Melhores práticas +## Boas 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 novamente `set_signing_keys` 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..2fc0281d8 100644 --- a/pt/xchat/groups.mdx +++ b/pt/xchat/groups.mdx @@ -1,45 +1,46 @@ --- 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 com vários participantes no X Chat, com chaves de conversa compartilhadas, títulos criptografados, gerenciamento de membros e mensagens assinadas. keywords: ["X Chat groups", "group DM", "conversation keys", "group name encryption"] --- -Os chats em grupo usam o **mesmo modelo de criptografia** do X Chat 1:1: uma **chave de conversa** compartilhada pelos membros, envelopada para a **chave pública de identidade** de cada membro, com mensagens criptografadas e assinadas pelo Chat XDK. O que muda é a **composição**, a **forma como você cria a conversa** e frequentemente os campos de **título/avatar criptografados** na conversa. +Bate-papos em grupo usam o **mesmo modelo de criptografia** do X Chat 1:1: uma **chave de conversa** compartilhada pelos membros, empacotada para a **chave pública de identidade** de cada membro, com mensagens criptografadas e assinadas pelo Chat XDK. O que muda é a **composição de membros**, **como você cria a conversa** e, muitas vezes, campos de **título/avatar criptografados** na conversa. -Os fluxos 1:1 estão em [Guia de introdução](/xchat/getting-started). Detalhes de endpoints estão em **Referência da API → Conversas e mensagens**. +Fluxos 1:1 estão em [Primeiros passos](/pt/xchat/getting-started). Detalhes de endpoints estão em **Referência da API → Conversas e mensagens**. --- -## Como grupos diferem de 1:1 +## Como os grupos diferem de 1:1 | Tópico | 1:1 | Grupo | |:-------|:----|:------| -| Identidade | Frequentemente endereçado pelo ID de usuário do par nos paths | ID da conversa normalmente começa com `g` | -| Criar | Chaves + envio de mensagem a um usuário | APIs de criar / inicializar grupo, depois chaves | +| Identidade | Frequentemente endereçado por ID de usuário do par nos caminhos | O ID da conversa normalmente começa com `g` | +| Criar | Chaves + envio de mensagens para um usuário | APIs de criar / inicializar grupo, depois chaves | | Participantes | Você + um par | Muitos usuários; a composição pode mudar | -| Metadados | Mínimos | Nome, avatar, etc. podem ser **texto cifrado** (descriptografe com a chave de conversa) | +| Metadados | Mínimos | Nome, avatar etc. podem ser **texto cifrado** (descriptografe com a chave da conversa) | | Rotação de chave | Menos frequente | Comum quando pessoas entram ou saem | -A criptografia ainda é: **Chat XDK** para chaves e payloads; **X API** para criar o grupo, publicar envelopes de chave para participantes, enviar mensagens e carregar eventos. +A criptografia continua sendo: **Chat XDK** para chaves e payloads; **API do X** para criar o grupo, publicar os empacotamentos de chave dos participantes, enviar mensagens e carregar eventos. --- ## Crie o grupo e estabeleça as chaves -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). -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. +1. Gere o ID do grupo com `POST /2/chat/conversations/group/initialize` — o `data.conversation_id` da resposta é o ID com prefixo `g` que você usará em todos os passos abaixo. +2. Carregue a chave pública de identidade de cada membro e o `public_key_version` (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 requisição). Verifique cada registro com `verify_key_binding` antes de usá-lo (veja o aviso em [Primeiros passos](/pt/xchat/getting-started#4-set-up-conversation-keys)). +3. Execute **`prepare_group_create`** uma vez, com **todos** os membros (incluindo você), o ID com prefixo `g` e as listas de IDs de membros/administradores. Uma chamada gera a chave da conversa, empacota-a para cada membro e assina a criação com a identidade da sessão de `set_identity` — retorna **duas** assinaturas de ação (a mudança de chave da conversa e a criação do grupo). +4. `POST /2/chat/conversations/group` com os membros/administradores do grupo, `conversation_key_version`, `conversation_participant_keys` (SDK **`encrypted_key`** → API **`encrypted_conversation_key`**) e **ambas** as `action_signatures`. Falhas de validação vêm como mensagens estáveis e legíveis, 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. Guarde a chave da conversa em **bruto** e a **versão** para criptografia/descriptografia. -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 a `avatar_url` que você passa e os incorpora literalmente no evento group-create. O servidor compara isso com sua requisição, então os valores de `group_name` / `group_avatar_url` no corpo do POST devem ser **byte-idênticos** ao que você passou ao 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 @@ -107,19 +106,19 @@ O `title` e o `avatar_url` que você passa para `prepare_group_create` são assi -O mapeamento do corpo para chaves de participantes e assinaturas de ação (`message_id`, `encoded_message_event_detail`, `message_event_signature` aninhado) é o mesmo do POST de chaves em [Guia de introdução — chaves de conversa](/xchat/getting-started#4-set-up-conversation-keys). +O mapeamento do corpo para chaves de participantes e assinaturas de ação (`message_id`, `encoded_message_event_detail`, `message_event_signature` aninhado) é o mesmo do POST de chaves em [Primeiros passos — chaves de conversa](/pt/xchat/getting-started#4-set-up-conversation-keys). -Quando a composição muda, chame **`prepare_group_members_change`** com os novos IDs de membros mais a lista atual (membros, admins, membros pendentes e o título/avatar/TTL atuais, se definidos). Ela rotaciona a chave de conversa e, como a criação de grupo, retorna **duas** assinaturas de ação — faça POST de tudo em **add members** (`POST /2/chat/conversations/{id}/members`). Depois, espere tráfego de **mudança de chave**: trate-o como a [rotação de chave no Guia de introdução](/xchat/getting-started#6-receive-and-decrypt) (`extract_conversation_keys` / `decrypt_events`, depois criptografe com a versão mais recente). +Quando a composição mudar, chame **`prepare_group_members_change`** com os novos IDs de membros mais a lista atual (membros, administradores, membros pendentes e o título/avatar/TTL atuais, se definidos). Ele rotaciona a chave da conversa e, assim como group-create, retorna **duas** assinaturas de ação — faça POST de tudo isso para **add members** (`POST /2/chat/conversations/{id}/members`). Depois, espere tráfego de **mudança de chave**: trate-o como [rotação de chave em Primeiros passos](/pt/xchat/getting-started#6-receive-and-decrypt) (`extract_conversation_keys` / `decrypt_events` e depois criptografe com a versão mais recente). -Como `prepare_group_members_change` gera uma **nova** chave de conversa envelopada apenas para a lista que você passa, novos membros recebem a nova versão da chave e não podem descriptografar mensagens enviadas sob versões anteriores. O inverso não é verdadeiro: a rotação nunca revoga o acesso a versões **anteriores** — quem já possui uma chave antiga ainda pode ler as mensagens criptografadas sob ela. Se você suspeitar que uma chave de conversa foi exposta, rotacione com `prepare_conversation_key_change`; isso protege apenas mensagens futuras. +Como `prepare_group_members_change` gera uma chave de conversa **nova** empacotada apenas para os membros que você passa, novos membros recebem a nova versão da chave e não conseguem descriptografar mensagens enviadas com versões anteriores. O contrário não é verdadeiro: a rotação nunca revoga o acesso a versões **anteriores** — qualquer pessoa que já possua uma chave antiga ainda pode ler as mensagens criptografadas com ela. Se você suspeitar que uma chave de conversa foi exposta, rotacione com `prepare_conversation_key_change`; isso protege apenas mensagens futuras. --- ## Metadados de grupo criptografados -Alguns campos de conversa (por exemplo, **nome** de exibição ou **URL do avatar**) podem chegar **criptografados** sob a chave de conversa. Isso **não** é `encrypt_message`; é o par genérico **`encrypt` / `decrypt`** do Chat XDK (string UTF-8 na entrada, texto cifrado base64 na saída, com a chave de conversa **em bruto**). +Alguns campos da conversa (por exemplo o **nome** de exibição ou a **URL do avatar**) podem chegar **criptografados** sob a chave da conversa. Isso **não** é `encrypt_message`; é o par genérico **`encrypt` / `decrypt`** do Chat XDK (string UTF-8 na entrada, texto cifrado em base64 na saída, com a chave da conversa em **bruto**). -Se um dado campo é armazenado criptografado é decidido pelo cliente que o escreve: `prepare_group_create` assina e envia o título exatamente como você o fornece (a chave de conversa não existe até que essa chamada a gere, então um título no momento da criação não pode ser criptografado sob ela). Quando você lê uma conversa cujos campos são texto cifrado, descriptografe-os com `decrypt` e a versão de chave que estava ativa quando o campo foi escrito. +Se um determinado campo é armazenado criptografado é decidido pelo cliente que o escreve: `prepare_group_create` assina e envia o título exatamente como você o fornece (a chave da conversa não existe até essa chamada gerá-la, então um título no momento da criação não pode ser criptografado com ela). Ao ler uma conversa cujos campos são texto cifrado, descriptografe-os com `decrypt` e a versão de chave que estava ativa quando o campo foi escrito. @@ -166,27 +165,27 @@ Se um dado campo é armazenado criptografado é decidido pelo cliente que o escr -Use a versão **atual** da chave de conversa que se aplica a esse metadado. Se as chaves foram rotacionadas, descriptografe com a versão que estava ativa quando o campo foi escrito (ou siga as regras do produto se os metadados são sempre reescritos na rotação). +Use a versão **atual** da chave da conversa que se aplica a esse metadado. Se as chaves foram rotacionadas, descriptografe com a versão que estava ativa quando o campo foi escrito (ou siga as regras do produto se os metadados forem sempre reescritos na rotação). --- ## Mensagens e eventos -Enviar e receber em um grupo é igual ao 1:1 uma vez que você tem a chave de conversa em bruto: +Enviar e receber em um grupo é igual a 1:1 uma vez que você tenha a chave da conversa em bruto: -- **Enviar:** `encrypt_message` → API de enviar mensagem ([Guia de introdução](/xchat/getting-started#5-send-a-message)) -- **Receber:** API de eventos ou [entrega em tempo real](/xchat/real-time-events) → `decrypt_event` / `decrypt_events` -- **Mídia:** [Mídia](/xchat/media) com o ID de conversa do grupo +- **Enviar:** `encrypt_message` → API send-message ([Primeiros passos](/pt/xchat/getting-started#5-send-a-message)) +- **Receber:** API de eventos ou [entrega em tempo real](/pt/xchat/real-time-events) → `decrypt_event` / `decrypt_events` +- **Mídia:** [Mídia](/pt/xchat/media) com o ID da conversa de grupo -Sempre criptografe com a versão de chave **mais recente** após uma rotação motivada por mudança de composição. +Sempre criptografe com a versão **mais recente** da chave após uma rotação motivada por mudança de composição. --- ## Checklist -1. Emita o ID prefixado com g com `POST /2/chat/conversations/group/initialize` -2. `prepare_group_create` com **cada** membro; POST dos envelopes de chave dos participantes e **ambas** as assinaturas de ação para `POST /2/chat/conversations/group` -3. Faça cache da chave em bruto + versão; atualize em eventos de mudança de chave +1. Gere o ID com prefixo `g` com `POST /2/chat/conversations/group/initialize` +2. `prepare_group_create` com **todos** os membros; faça POST dos empacotamentos de chave dos participantes e **ambas** as assinaturas de ação para `POST /2/chat/conversations/group` +3. Faça cache da chave em bruto + versão; atualize nos eventos de mudança de chave 4. Em mudanças de composição, `prepare_group_members_change` (duas assinaturas) → `POST /2/chat/conversations/{id}/members` -5. Descriptografe metadados de grupo com `decrypt` quando os campos são texto cifrado -6. Envie/receba com os mesmos padrões do 1:1 +5. Descriptografe os metadados do grupo com `decrypt` quando os campos forem texto cifrado +6. Envie/receba com os mesmos padrões de 1:1 diff --git a/pt/xchat/media.mdx b/pt/xchat/media.mdx index e7678a06a..e73f20ae9 100644 --- a/pt/xchat/media.mdx +++ b/pt/xchat/media.mdx @@ -1,15 +1,15 @@ --- title: Mídia e anexos sidebarTitle: Mídia -description: Criptografe, envie, entregue, baixe e descriptografe imagens e anexos no X Chat com o Chat XDK, criptografia de streams e endpoints de upload. +description: Criptografe, faça upload, envie, baixe e descriptografe imagens e anexos de arquivos no X Chat usando a criptografia de stream do Chat XDK e os endpoints de upload de mídia. keywords: ["X Chat media", "encrypted images", "attachments", "encrypt_stream", "media upload"] --- -Imagens e outros arquivos usam a **mesma chave de conversa** que o texto. Criptografe os bytes com o Chat XDK (`encrypt_stream` / `decrypt_stream`), envie via rotas **`/2/chat/media/upload`** (barra lateral **Referência da API → Mídia**) e depois anexe **`media_hash_key`** em `encrypt_message`. +Imagens e outros arquivos usam a **mesma chave de conversa** do texto. Criptografe bytes com o Chat XDK (`encrypt_stream` / `decrypt_stream`), faça upload via as rotas **`/2/chat/media/upload`** (barra lateral **Referência da API → Mídia**) e depois anexe **`media_hash_key`** em `encrypt_message`. -Inclua **`media.write`** entre seus escopos de DM ao fazer upload. Use IDs de conversa com hífen nos paths (`:` → `-`). Prefira MIME/dimensões dos bytes **descriptografados**. +Inclua **`media.write`** junto com seus escopos de DM ao fazer upload. Use IDs de conversa com hífen nos caminhos (`:` → `-`). Prefira MIME/dimensões dos bytes **descriptografados**. -Este caminho **não** é o modelo de mídia de Posts (`expansions=attachments.media_keys`, `media.fields=variants`, etc.). Esses parâmetros se aplicam a **Posts**; blobs E2EE do X Chat são endereçados por **`media_hash_key`** e o download de mídia do X Chat. +Este caminho **não** é o modelo de mídia de Posts (`expansions=attachments.media_keys`, `media.fields=variants` etc.). Esses parâmetros se aplicam a **Posts**; blobs E2EE do X Chat são endereçados por **`media_hash_key`** e download de mídia do X Chat. ```mermaid flowchart LR @@ -104,41 +104,37 @@ flowchart LR -`encrypt_stream` / `decrypt_stream` processam o payload inteiro em memória. Para arquivos grandes, `stream_encryptor()` / `stream_decryptor()` retornam objetos incrementais (`StreamEncryptor` / `StreamDecryptor`): alimente chunks com `push` e depois chame `finish` uma vez — `finish` retorna erro se o stream foi truncado. +`encrypt_stream` / `decrypt_stream` processam o payload inteiro em memória. Para arquivos grandes, `stream_encryptor()` / `stream_decryptor()` retornam objetos incrementais (`StreamEncryptor` / `StreamDecryptor`): alimente com chunks via `push` e depois chame `finish` uma vez — `finish` gera erro se o stream foi truncado. --- ## Upload -| Passo | Método | Path | -|:------|:-------|:-----| +| Etapa | Método | Caminho | +|:------|:-------|:--------| | Inicializar | `POST` | `/2/chat/media/upload/initialize` | -| Anexar | `POST` | `/2/chat/media/upload/{id}/append` | +| Append | `POST` | `/2/chat/media/upload/{id}/append` | | Finalizar | `POST` | `/2/chat/media/upload/{id}/finalize` | -Use os corpos de requisição nas páginas OpenAPI em **Referência da API → Mídia**. Prefira o tamanho do blob **criptografado** onde o tamanho é requerido. A finalização retorna **`media_hash_key`** para anexos e download. Repita `5xx` transitórios com backoff. Python/TypeScript podem usar o XDK quando existirem helpers de mídia; caso contrário, faça POST com um token Bearer em qualquer linguagem. +Use os corpos de requisição nas páginas OpenAPI em **Referência da API → Mídia**. Prefira o tamanho do blob **criptografado** onde o tamanho for necessário. Finalizar retorna **`media_hash_key`** para anexos e download. Refaça tentativas de `5xx` transitórios com backoff. Python/TypeScript podem usar o XDK quando há helpers de mídia; caso contrário, faça POST com um token Bearer em qualquer linguagem. --- ## 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 send-message (mesmo mapeamento de campo de [Primeiros passos](/pt/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 retentativas para que um ID nunca seja gerado 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,49 +224,51 @@ 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 chave/versão da conversa pode ser totalmente omitido: com `set_cache_keys(true)` habilitado, `encrypt_message` resolve a chave e a versão a partir da mudança de chave mais recente verificada da conversa (veja [Primeiros passos](/pt/xchat/getting-started)). + --- ## Baixar e descriptografar -Path: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/download-chat-media). O corpo da resposta é texto cifrado. Em mensagens de entrada, leia `media_hash_key` dos anexos descriptografados / `media_hashes`. +Caminho: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/download-chat-media). O corpo da resposta é texto cifrado. Em mensagens de entrada, leia `media_hash_key` dos anexos descriptografados / `media_hashes`. -**Escolha a chave pela versão de chave do evento.** Cada evento de mensagem descriptografado carrega o `keyVersion` (JS; `key_version` nos outros bindings) sob o qual seu conteúdo foi criptografado. Descriptografe um anexo com a chave de conversa para **essa** versão — `conversationKeys.keys[event.keyVersion]` — e não com a mais recente. Após uma rotação de chave (por exemplo, uma adição de membro), a chave mais recente não consegue descriptografar mídia anexada a mensagens mais antigas. +**Escolha a chave pela versão da chave do evento.** Cada evento de mensagem descriptografado carrega a `keyVersion` (JS; `key_version` nos outros bindings) sob a qual seu conteúdo foi criptografado. Descriptografe um anexo com a chave da conversa para **essa** versão — `conversationKeys.keys[event.keyVersion]` — não a mais recente. Após uma rotação de chave (por exemplo, uma adição de membro), a chave mais recente não conseguirá descriptografar mídia anexada a mensagens mais antigas. @@ -358,9 +368,9 @@ Path: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/downl ## Dicas -- Use a mesma **versão da chave de conversa** que foi usada quando a mídia foi criptografada +- Use a **mesma versão** da chave da conversa usada quando a mídia foi criptografada - Não registre mídia em texto simples nem chaves em bruto -- Detecte MIME **após** descriptografar -- Clientes web: criptografe/descriptografe no cliente quando possível; mantenha tokens OAuth no seu servidor +- Detecte o MIME **após** descriptografar +- Clientes web: criptografe/descriptografe no cliente quando possível; mantenha os tokens OAuth no seu servidor -Os esquemas completos de requisição e resposta para cada rota de mídia estão em **Referência da API → Mídia** na barra lateral (inicializar upload, anexar chunk, finalizar upload e baixar mídia). +Esquemas completos de requisição e resposta para cada rota de mídia estão em **Referência da API → Mídia** na barra lateral (inicializar upload, append de chunk, finalizar upload e baixar mídia). diff --git a/pt/xchat/real-time-events.mdx b/pt/xchat/real-time-events.mdx index 9e9ab0ab0..671828bd6 100644 --- a/pt/xchat/real-time-events.mdx +++ b/pt/xchat/real-time-events.mdx @@ -1,45 +1,45 @@ --- -title: Eventos do X Chat em tempo real +title: Eventos em tempo real do X Chat sidebarTitle: Eventos em tempo real -description: Receba eventos chat.received, chat.sent e outras atividades criptografadas do X Chat via webhooks ou activity stream e descriptografe com o Chat XDK. +description: Receba chat.received, chat.sent e outras atividades criptografadas do X Chat via webhooks ou activity stream, depois descriptografe os payloads com o Chat XDK. --- -O X entrega **`chat.received`**, **`chat.sent`** e atividades relacionadas do X Chat com **texto cifrado** no payload. Descriptografe com o [Chat XDK](/xchat/xchat-xdk). +O X entrega **`chat.received`**, **`chat.sent`** e atividades relacionadas do X Chat com **texto cifrado** no payload. Descriptografe com o [Chat XDK](/pt/xchat/xchat-xdk). | Camada | Papel | |:-------|:------| -| **X Activity API** | `GET /2/activity/stream`; `POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions` (veja segurança OpenAPI por operação) | +| **API de Activity do X** | `GET /2/activity/stream`; `POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions` (veja a 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. +Tipos privados de evento do X Chat requerem autorização do usuário monitorado. Anexos de arquivos criptografados do X Chat usam **`media_hash_key`** e o download de mídia do X Chat — não os parâmetros da API de Posts `expansions=attachments.media_keys` / `media.fields=variants`. --- -## Tipos de eventos +## Tipos de evento | Evento | Quando | |:-------|:-------| -| `chat.received` | O usuário inscrito recebe um DM criptografado | -| `chat.sent` | O usuário inscrito envia um DM criptografado | -| `chat.conversation_join` | O usuário inscrito entra em um grupo (quando oferecido) | +| `chat.received` | Usuário inscrito recebe um DM criptografado | +| `chat.sent` | Usuário inscrito envia um DM criptografado | +| `chat.conversation_join` | Usuário inscrito entra em um grupo (quando oferecido) | --- ## 1. Escolha a entrega -**Stream de atividades (geralmente o mais simples para bots):** `GET /2/activity/stream` com um Bearer token de app (opcional `backfill_minutes`, `start_time`, `end_time` por OpenAPI). Filtre no lado do cliente para `chat.received` / `chat.sent`. +**Activity stream (geralmente mais simples para bots):** `GET /2/activity/stream` com um Bearer token de app (opcional `backfill_minutes`, `start_time`, `end_time` conforme OpenAPI). Filtre no cliente por `chat.received` / `chat.sent`. **Assinaturas de atividade:** gerencie assinaturas duráveis com: - `POST /2/activity/subscriptions` — criar - `GET /2/activity/subscriptions` — listar (paginado) - `PUT /2/activity/subscriptions/{subscription_id}` — atualizar -- `DELETE /2/activity/subscriptions/{subscription_id}` ou `DELETE /2/activity/subscriptions?ids=` — excluir +- `DELETE /2/activity/subscriptions/{subscription_id}` ou `DELETE /2/activity/subscriptions?ids=` — deletar -Corpos de requisição e escopos requeridos estão definidos na operação OpenAPI para cada rota. Criar uma assinatura da X Activity API (XAA) requer **autorização de contexto de usuário** (OAuth 2.0 de contexto de usuário com os escopos relevantes, como `dm.read` para eventos de chat) para o usuário cuja atividade você monitora. +Os corpos de requisição e os escopos necessários são definidos na operação OpenAPI de cada rota. Criar uma assinatura X Activity API (XAA) requer **autorização em contexto de usuário** (OAuth 2.0 em contexto de usuário com os escopos relevantes, como `dm.read` para eventos de chat) para o usuário cuja atividade você monitora. -**Webhooks:** se você terminar eventos em seu endpoint HTTPS, registre um webhook com `POST /2/webhooks`, responda aos desafios CRC e depois crie suas assinaturas de atividade com `POST /2/activity/subscriptions`, referenciando seu `webhook_id` (veja operações de Webhooks e Activity no OpenAPI). O XDK Python/TypeScript pode expor helpers para webhooks e atividade quando sua versão do SDK os incluir. +**Webhooks:** se você terminar eventos em seu endpoint HTTPS, registre um webhook com `POST /2/webhooks`, passe pelos desafios de CRC e depois crie suas assinaturas de atividade com `POST /2/activity/subscriptions`, referenciando seu `webhook_id` (veja as operações de Webhooks e Activity no OpenAPI). Os XDKs de Python/TypeScript podem expor helpers para webhooks e atividade quando sua versão do SDK os incluir. @@ -74,130 +74,88 @@ Corpos de requisição e escopos requeridos estão definidos na operação OpenA -Inscreva-se também em `chat.sent` se precisar de cópias de saída. Outras linguagens: chame diretamente as mesmas rotas HTTPS `/2/activity/*` (token de contexto de usuário para criar assinaturas, Bearer token de app para o stream). +Assine também `chat.sent` se você precisar de cópias de saída. Outras linguagens: chame as mesmas rotas HTTPS `/2/activity/*` diretamente (token em contexto de usuário para criar assinaturas, Bearer token de app para o stream). --- ## 2. CRC (apenas webhooks) -Se você usa webhooks, responda aos Challenge-Response Checks (GET `crc_token`) com HMAC-SHA256 do token usando seu consumer secret, no formato JSON que seu produto de webhook espera (tipicamente `sha256=`). +Se você usar webhooks, responda aos Challenge-Response Checks (GET `crc_token`) com HMAC-SHA256 do token usando seu consumer secret, no formato JSON esperado pelo seu produto de webhook (normalmente `sha256=`). --- -## 3. Descriptografar com o Chat XDK +## 3. Descriptografe 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 sequence ids são metadados não assinados atribuídos pelo backend. -JavaScript usa tipos de evento em camelCase (`message`); outros bindings usam `"Message"` e campos snake_case. +Os snippets abaixo usam os dois armazenamentos de sessão **opcionais** para o handler mais curto: `set_signing_keys` guarda 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, de modo que `decrypt_event` só precisa do evento. Quando um payload traz `conversation_key_change_event`, execute-o antes por `decrypt_events`: isso verifica a mudança de chave e, com caching ativado, retém sua chave para a chamada de `decrypt_event`. Prefere nenhum 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 em 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,7 +197,9 @@ JavaScript usa tipos de evento em camelCase (`message`); outros bindings usam `" -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). +Para manter os mapas de chave em suas próprias mãos, `extract_conversation_keys` descriptografa as chaves de `conversation_key_change_event` e `decrypt_event` as aceita (junto com as chaves de assinatura do remetente) como argumentos explícitos — um argumento explícito e não vazio sempre prevalece sobre os armazenamentos. + +Histórico: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — veja [Primeiros passos](/pt/xchat/getting-started#6-receive-and-decrypt). --- @@ -256,7 +225,7 @@ 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` +- Verifique assinaturas de webhook conforme os requisitos da plataforma +- Defina os armazenamentos de sessão uma vez: `set_signing_keys` para todos os participantes, `set_cache_keys(true)` para 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..38196a793 100644 --- a/pt/xchat/troubleshooting.mdx +++ b/pt/xchat/troubleshooting.mdx @@ -1,22 +1,22 @@ --- 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 comuns de criptografia do X Chat, incluindo erros do Chat XDK, recuperação do backup seguro de chave, falhas de descriptografia e montagem de payloads de envio assinados. keywords: ["X Chat errors", "Chat XDK", "decryption", "secure key backup", "encryption"] --- -Esta página cobre problemas que são **específicos da criptografia do X Chat e do Chat XDK** — chaves, backup seguro de chaves, descriptografia/verificação e construção de payloads criptografados de envio. +Esta página cobre problemas **específicos da criptografia do X Chat e do Chat XDK** — chaves, backup seguro de chave, descriptografar/verificar e montagem de payloads de envio criptografados. -Para webhooks, OAuth, códigos de status HTTP e limites de taxa, use a documentação geral da [X API](/x-api/introduction) e de [autenticação](/fundamentals/authentication/overview). +Para webhooks, OAuth, códigos de status HTTP e limites de taxa, use a documentação geral da [API do X](/pt/x-api/introduction) e de [autenticação](/pt/fundamentals/authentication/overview). --- -## Chaves e backup seguro de chaves +## Chaves e backup seguro de chave -### O desbloqueio falha (código de acesso inválido) +### Unlock falha (código de acesso inválido) - Confirme que o código de acesso corresponde ao usado com `setup` -- Aguarde entre tentativas; realms limitam tentativas incorretas e podem travar a recuperação após muitas falhas +- Aguarde entre tentativas; os realms limitam a taxa de tentativas incorretas e podem bloquear a recuperação após falhas demais @@ -62,80 +62,81 @@ 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 +### Criptografia ou descriptografia falha porque as chaves ou a identidade não estão 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 uma identidade de sessão (e sem um override explícito 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); ``` ### 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` -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)) +1. Descriptografe o material de chave 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, de modo que chamadas posteriores de `decrypt_event` e `encrypt_*` podem omiti-la +2. Confirme que as chaves de conversa foram adicionadas para aquela versão e que você ainda é um participante (veja [Primeiros passos](/pt/xchat/getting-started#4-set-up-conversation-keys)) -### O par não tem chaves públicas +### O peer não tem chaves públicas -Pode ser que ele não tenha concluído o onboarding. Depois que ele se registrar, carregue `public_key`, `signing_public_key`, `identity_public_key_signature` e `public_key_version` a partir de **Referência da API → Chaves de criptografia**. +Talvez ele não tenha concluído o onboarding. Depois que ele se registrar, carregue `public_key`, `signing_public_key`, `identity_public_key_signature` e `public_key_version` em **Referência da API → Chaves de criptografia**. --- ## Descriptografia e assinaturas -### A descriptografia falha +### Descriptografia falha -- Chave de conversa **em bruto** obsoleta ou errada, ou versão de chave errada +- Chave da conversa em **bruto** obsoleta ou incorreta, ou versão de chave errada - String `encoded_event` incompleta -- O tipo de evento não é uma mensagem criptografada que você pode tratar como conteúdo descriptografável +- O tipo de evento não é uma mensagem criptografada que você possa tratar como conteúdo descriptografável -### A assinatura não verifica +### A assinatura não é verificada -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: +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, não que você precisa ligar a checagem. 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)) -- O remetente rotacionou versões — busque as chaves públicas dele novamente +- 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](/pt/xchat/xchat-xdk)) +- Nenhuma chave de assinatura passada na chamada nem armazenada via `set_signing_keys` +- O remetente rotacionou as versões — busque suas chaves públicas novamente - Uma versão de chave abaixo do piso aceito nunca é verificada -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: +O setter `set_reject_unverified` existe para **desabilitar** esse padrão (`false`, não recomendado). Se você o desabilitou anteriormente, restaure o padrão fail-closed: @@ -170,37 +171,41 @@ O setter `set_reject_unverified` existe para você **desabilitar** esse padrão +### Uma resposta traz `reply_preview_validation: "Invalid"` + +Respostas descriptografadas podem trazer `reply_preview_validation` (`"Valid"` / `"Invalid"`; JavaScript usa `'valid'` / `'invalid'`). `Invalid` significa que o preview citado dentro da mensagem não corresponde ao evento original assinado que ela incorpora — 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 um preview inválido. + ### 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. +Erros como `signature missing or no matching signing key` ou um mismatch de 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 assinado sobre bytes diferentes (ou nunca assinado) falha em toda carga futura — nenhuma retentativa, atualização de chave ou chamada de API pode consertá-lo. Trate esses eventos como tombstones, não como erros com retentativa. Rotacionar a chave da conversa inicia um histórico limpo e verificável a partir desse ponto; novas mensagens não são afetadas. --- -## Construindo o payload de envio +## Montando o payload de envio -Estes erros são específicos da criptografia do X Chat (não são erros HTTP gerais): +Estes erros são específicos da criptografia do X Chat (não erros HTTP gerais): | Problema | Correção | |:---------|:---------| -| 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 | -| 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 | +| Bytes de chave incorretos | Passe os bytes da chave da conversa em **bruto** para o Chat XDK, não a string de chave criptografada da API | +| Nomes de campo JSON errados | Mapeie `encrypted_content` → `encoded_message_create_event` e `encoded_event_signature` → `encoded_message_event_signature` | +| ID de mensagem errado | Envie o `message_id` do payload retornado — o SDK o gera e o incorpora ao evento assinado, então qualquer outro valor falha. Em retentativas, reutilize o mesmo payload criptografado para que o ID nunca seja gerado duas vezes | +| Descompasso de versão | Alinhe `conversation_key_version` com a chave que você usa; alinhe a versão da chave de assinatura passada para `set_identity` com seu registro de chave pública | +| Forma do ID no caminho | Caminhos de URL ainda precisam do ID da conversa com hífen (`:` → `-`), mas para assinatura o SDK aceita qualquer forma: `A:B`, `A-B` (qualquer ordem) ou apenas o ID do usuário destinatário — todos canonizam para os mesmos bytes assinados | -### A API retorna 400 para uma chamada que altera estado +### A API retorna 400 para uma chamada que muda estado -Toda chamada de chat que altera estado — adicionar ou rotacionar chaves de conversa, criar um grupo, adicionar membros — requer **`action_signatures`** no corpo da requisição, validado na fronteira da API. Uma entrada ausente ou malformada (cada uma precisa de `message_id`, `encoded_message_event_detail` e um `message_event_signature` com `signature`, `public_key_version` e `signature_version`) retorna imediatamente uma resposta HTTP 400 problem-details. Use os métodos prepare do SDK (`prepare_conversation_key_change`, `prepare_group_create`, `prepare_group_members_change`) e envie **todas** as assinaturas retornadas — criar grupo e adicionar membros retornam duas. +Toda chamada de chat que muda estado — adicionar ou rotacionar chaves de conversa, criar um grupo, adicionar membros — requer **`action_signatures`** no corpo da requisição, validado na borda da API. Uma entrada ausente ou malformada (cada uma precisa de `message_id`, `encoded_message_event_detail` e um `message_event_signature` com `signature`, `public_key_version` e `signature_version`) retorna uma resposta problem-details HTTP 400 imediatamente. Use os métodos prepare do SDK (`prepare_conversation_key_change`, `prepare_group_create`, `prepare_group_members_change`) e envie **todas** as assinaturas retornadas — criação de grupo e adição de membros retornam duas. --- -## Criptografar e descriptografar mídia +## Criptografia e descriptografia de mídia - Use a **mesma** chave de conversa (e versão) da mensagem que referencia o anexo - Trate respostas de download como **texto cifrado** até executar `decrypt_stream` -- Deduza o tipo MIME **após** descriptografar; o `Content-Type` do download frequentemente não é o tipo real da imagem +- Infira o tipo MIME **após** descriptografar; o `Content-Type` do download frequentemente não é o tipo real da imagem -Detalhes: [Mídia](/xchat/media). +Detalhes: [Mídia](/pt/xchat/media). --- @@ -210,5 +215,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 -- 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 +- Confirme que a versão de chave de assinatura passada para `set_identity` corresponde ao `public_key_version` do seu registro de chave pública +- Para histórico incompleto, pagine **todas** as páginas de eventos para que 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..cba2bdfeb 100644 --- a/pt/xchat/xchat-xdk.mdx +++ b/pt/xchat/xchat-xdk.mdx @@ -1,13 +1,13 @@ --- 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 cuida do gerenciamento de chaves, criptografia, descriptografia e assinatura do 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 do gerenciamento de chaves, criptografia, descriptografia e assinatura do X Chat. Ele **não** chama a API HTTP do X — combine-o com o **XDK** de [Python](/xdks/python/overview) ou [TypeScript](/xdks/typescript/overview), ou com HTTPS e um token de acesso de 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). +Passo a passo do app: [Primeiros passos](/pt/xchat/getting-started). Bots de exemplo: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples). ### Instalação @@ -25,14 +25,14 @@ Passo a passo do app: [Guia de introdução](/xchat/getting-started). Bots de ex npm install juicebox-sdk # optional peer dependency — required for setup()/unlock() secure key backup ``` - O motor WASM compilado vem dentro do pacote — sem etapa de build. Requer Node.js 18+. + O mecanismo WASM compilado é entregue dentro do pacote — sem etapa de build. Requer Node.js 18+. ```toml [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] @@ -44,25 +44,25 @@ Passo a passo do app: [Guia de introdução](/xchat/getting-started). Bots de ex go get github.com/xdevplatform/chat-xdk/go/chatxdk ``` - Bibliotecas estáticas pré-compiladas estão incluídas (macOS arm64/amd64, Linux amd64 glibc/musl) — você precisa de um compilador C, mas não de Rust. Requer Go 1.21+. + Bibliotecas estáticas pré-compiladas estão incluídas (macOS arm64/amd64, Linux amd64 glibc/musl) — você precisa de um compilador C, mas não do Rust. Requer Go 1.21+. ```bash dotnet add package XDevPlatform.ChatXdk ``` - O pacote é autossuficiente: inclui as bibliotecas nativas para macOS (arm64, x64), Linux (x64) e Windows (x64). Requer .NET 8+. + O pacote é autocontido: bibliotecas nativas para macOS (arm64, x64), Linux (x64) e Windows (x64) são entregues dentro dele. Requer .NET 8+. ```xml com.x chatxdk - 0.2.1 + 0.4.0 ``` - Disponível no Maven Central. O jar inclui a biblioteca nativa para macOS (arm64, x64), Linux (x64) e Windows (x64) — não é preciso configurar `jna.library.path`. Importe de `com.x.chatxdk`. Requer JDK 17+. + Disponível no Maven Central. O jar inclui a biblioteca nativa para macOS (arm64, x64), Linux (x64) e Windows (x64) — sem necessidade de configurar `jna.library.path`. Importe de `com.x.chatxdk`. Requer JDK 17+. @@ -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 [Primeiros passos](/pt/xchat/getting-started). + +Os snippets usam os dois armazenamentos de sessão **opcionais** para as formas mais curtas de chamada: `set_signing_keys` guarda as chaves públicas dos outros participantes (buscadas do [endpoint de chaves públicas](/x-api/chat/get-user-public-keys)) para que chamadas de descriptografia possam verificar remetentes sem um argumento por chamada, e `set_cache_keys(true)` permite que o SDK lembre a chave verificada de cada conversa, de modo que chamadas de criptografia só precisam do ID da conversa e do texto. Pule qualquer um e passe os mesmos valores por chamada — ambos os 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 chaves privadas (backup seguro de chave 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 o unlock ou o import — isso define o remetente e a versão de chave de assinatura padrão de cada ação assinada, para que os métodos de criptografia e preparação funcionem sem argumentos de identidade por chamada. Chame `generate_keypairs` uma vez por identidade de dispositivo/app; poste o payload de registro no endpoint de chaves públicas. Use `setup` / `unlock` (e os helpers de código de acesso relacionados) para backup seguro de chave em cada binding. `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 chave em bruto: em um navegador, qualquer script que alcance a instância pode exfiltrar a identidade, então o JS mantém as chaves dentro do backup seguro de chave. Um servidor JS que queira evitar uma viagem ao realm de backup por requisição deve reutilizar uma instância `Chat` desbloqueada entre requisições, ou executar um binding nativo onde blobs de chave são suportados. + +O SDK também precisa da versão que a API do X reporta para sua chave pública registrada, para que entradas de mudança de chave direcionadas a outras versões sejam ignoradas. `set_identity` a registra junto com o ID de usuário; `import_keys` a aceita diretamente como um 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,29 +350,29 @@ 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 do backup seguro de chave aceita três formatos: o objeto `juicebox_config` da API do X (recomendado — passado literalmente), um wrapper completo `sdk_config` 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á **ligada por padrão** (`reject_unverified = true`) — chame `set_reject_unverified(false)` para desabilitá-la (não recomendado); `update_config` se a configuração do realm de backup mudar; `is_unlocked` / `has_identity_key` para estado de UI. As listas completas de campos estão 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**, cada um faz com que uma chamada realize 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 parâmetros 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 | |:--------|:-------|:-------------------------------| -| Iniciar uma 1:1 (omita o ID da conversa — o SDK o deriva) ou rotacionar a chave de qualquer conversa (passe o ID) | `prepare_conversation_key_change` | 1 | -| Criar um grupo (ID emitido por `POST /2/chat/conversations/group/initialize`) | `prepare_group_create` | 2 — envie ambas | +| Iniciar um 1:1 (omita o ID de conversa — o SDK o deriva) ou rotacionar a chave de qualquer conversa (passe o ID) | `prepare_conversation_key_change` | 1 | +| Criar um grupo (ID gerado por `POST /2/chat/conversations/group/initialize`) | `prepare_group_create` | 2 — envie ambas | | Adicionar membros a um grupo | `prepare_group_members_change` | 2 — envie ambas | -Mantenha os bytes da chave **em bruto** para `encrypt_message` e mídia; nunca passe o envelope criptografado da API para encrypt. +Guarde os bytes da chave em **bruto** para `encrypt_message` e mídia; nunca passe o envelope criptografado da API para a criptografia. -**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 obtidas antes de empacotar.** Os métodos prepare criptografam a nova chave da 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 da 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` desempacota 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ção de membros, passe os parâmetros que cada método precisa (listas de IDs de membros/administradores para `prepare_group_create`; nova + composição atual para `prepare_group_members_change`) — veja [Grupos](/pt/xchat/groups#create-the-group-and-establish-keys) para exemplos. Ambos retornam **duas** assinaturas de ação; o POST deve incluir as duas. --- ## 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: extrai as chaves de conversa do fluxo, 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/aciona exceção em caso de falha. + +Passe **chaves de assinatura** para que o SDK possa verificar remetentes. Mapeie os campos da API de chaves públicas 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 opcionais 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 descriptografia que omita (ou passe vazio) o argumento de chaves de assinatura usa o armazenamento no lugar. A verificação em si permanece inalterada — chaves entram no armazenamento apenas por essa chamada, nunca a partir dos eventos que estão sendo descriptografados. Cada chamada substitui o conjunto anterior. +- **`set_cache_keys(true)`** habilita o cache de chave de conversa (desligado por padrão). Enquanto habilitado, `decrypt_events` faz cache, por conversa, da chave mais recente cuja mudança de chave tinha uma assinatura válida; `decrypt_event` recorre a ela quando seu argumento de chaves de conversa é omitido, e os helpers de criptografia resolvem uma chave de conversa omitida a partir dele. Desabilitar limpa o cache. + +Um argumento explícito e não vazio sempre prevalece sobre os armazenamentos. Argumentos explícitos por chamada continuam sendo cidadãos 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 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 de fato pular a verificação, você deve 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)`** monta 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 da conversa, a partir do cache opcional de chaves (`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 incorporado no evento assinado) e o retorna no payload — nunca crie o seu próprio; reutilize o mesmo payload em retentativas para que um ID nunca seja gerado duas vezes. Mapeie o payload para o corpo send-message: `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 codificado em base64 sendo respondido. O SDK deriva o preview citado (sequence id, remetente, texto, entidades, anexos) a partir dele e incorpora o original assinado na mensagem enviada 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 foi criptografado em uma versão de chave mais antiga que a resposta. Quando o original foi **editado**, passe o evento de edição em bruto como `reply_to_edit_event`: o preview 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 verificar. Os campos explícitos `reply_to_*` permanecem como overrides para callers que não têm mais 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 a partir do evento em bruto sendo reagido; os mesmos parâmetros podem adicionar e depois remover uma reação. Defina `conversation_id` e `target_message_sequence_id` explicitamente apenas 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. +No lado do recebimento, uma mensagem descriptografada que cita uma resposta traz **`reply_preview_validation`** (`"Valid"` / `"Invalid"`; o binding JS usa `'valid'` / `'invalid'`): o SDK verificou a assinatura do original incorporado contra suas chaves de assinatura — nunca contra uma chave carregada no evento — descriptografou-o e comparou o conteúdo citado e o autor com ele. Quando o preview incorpora 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, em vez do texto pré-edição. O campo está ausente quando a mensagem não traz preview ou o preview não incorpora um original. Trate previews `Invalid` como não confiáveis: a mensagem em si é autêntica, mas o material citado não é — renderize as citações apenas a partir do original validado. + +**`encrypt` / `decrypt`** são para metadados UTF-8 sob a chave da conversa (por exemplo, um nome de grupo criptografado) — não envelopes de mensagem. **`encrypt_stream` / `decrypt_stream`** criptografam bytes de anexo; veja [Mídia](/pt/xchat/media). Os **`sign` / `verify` / `verify_key_binding`** de baixo nível dão suporte a 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 caminhos de URL (em qualquer ordem), ou o ID de usuário do destinatário puro — o SDK o canoniza antes de assinar. IDs de grupo (com prefixo `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 os bytes de arquivo com a **mesma** chave de conversa usada para texto, faça upload pelas APIs de mídia de Chat e anexe **`media_hash_key`** em `encrypt_message`. Isso não é o modelo de mídia de Posts (`expansions=attachments.media_keys`). Fluxo completo de upload/download: [Mídia](/pt/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 o texto simples empurrado 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()` depois de `finish()` (lança erro). Chame `free()` apenas para abandonar um stream *antes* de finalizar (por exemplo, em um caminho 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 em nível de módulo (Python/JS/Rust/Go) ou `ChatXdkUtilities` (C#/Java) — úteis ao construir metadados de anexo sem trazer bibliotecas extras. @@ -794,37 +909,37 @@ Helpers de Base64/hex, detecção de MIME e dimensões de imagem estão disponí Estes tipos conceituais aparecem em todas as linguagens (os nomes exatos dos campos diferem; JS frequentemente usa 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 criptografia: o **`message_id`** gerado pelo SDK (um UUID incorporado no evento assinado — envie-o como `message_id` da mensagem e guarde-o para deduplicação), `encrypted_content`, `encoded_event_signature`, metadados da 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 descriptografia 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 incorpora a chave em texto claro). +- **DecryptEventsResult** — mensagens, erros opcionais e `conversation_keys` extraídas. Mensagens descriptografadas que citam uma resposta trazem `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 no [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 com um mismatch de ECDSA falhará em toda carga futura — nenhuma retentativa, atualização de chave ou chamada de API pode consertá-lo. Trate esses casos como tombstones, não como erros transitórios. Rotacionar a chave da conversa inicia um histórico limpo e verificável a partir desse ponto. --- ## Próximos passos - + Conecte o Chat XDK à Chat API - - Criptografia de stream e REST de mídia + + Criptografia em stream e REST de mídia - - Entrega via webhooks e atividade + + Entrega via webhooks e activity - + Falhas comuns