Skip to content
xdevplatformPublic

About

X Chat encryption SDK

Resources

Code of conduct

Security policy

Stars

27 stars

Watchers

0 watching

Forks

Repository files navigation

chat-xdk

Encryption SDK for X Chat (encrypted Direct Messages). A single Rust core (chat-xdk-core) handles key management, message encryption, signing, and the Thrift wire protocol, exposed through idiomatic bindings for six languages.

Bindings

Language Import Backed by Guide
Rust chat-xdk-core — docs/API.md
Python chat_xdk PyO3 crates/pyo3/README.md
JavaScript / WASM @xdevplatform/chat-xdk wasm-bindgen crates/wasm/js/README.md
Go github.com/xdevplatform/chat-xdk/go/chatxdk cgo go/chatxdk/README.md
.NET ChatXdk P/Invoke crates/dotnet/README.md
JVM com.x.chatxdk JNA crates/jvm/README.md

Examples

The fastest way in: examples/ has one small, clone-and-run encrypted chat bot per binding. Each imports the real binding, talks to the X Chat API, and shows the full flow (key load → decrypt → reply → encrypt → send). Start there:

cd examples && make test-all      # offline crypto tests for every binding (no creds)

See examples/README.md for setup, getting an access token, and running a bot live.

The core flow

Every binding follows the same shape (names are camelCase in JS, snake_case in Python/Rust, PascalCase in .NET/Go):

  1. Load keys — import a private-key blob, or recover from Juicebox (below).
  2. Set identity — setIdentity({ userId, signingKeyVersion }) once per instance, where signingKeyVersion is the public_key_version returned when your keys were registered; every signed action defaults its sender and signing-key version to these values. Optionally setCacheKeys(true) and setSigningKeys(signingKeys) so the decrypt/encrypt calls below need no per-call key plumbing.
  3. Initial load — decryptEvents({events}): batch-decrypts a backlog, extracting conversation keys from KeyChange events and matching signing keys by user automatically. Per-event errors are collected in the result; only invalid params throw. The events endpoint returns the KeyChange events in meta.conversation_key_events, separate from data — pass both together.
  4. Each new event — decryptEvent({eventB64, conversationKeys}): decrypts one event (webhook / poll). Throws on failure.
  5. Reply — encryptMessage({conversationId, text}) or encryptReply({conversationId, text, replyToEvent}) → POST the result to the X Chat API.
  6. Files — a message with files is built first: start each upload, list its media hash key in the message's attachments, then encryptFile({file, eventB64, mediaHashKey}) with the payload's eventB64 (plus conversationKey and conversationKeyVersion unless the key cache holds them, as for the message). decryptFile opens a received file from its message's event. A file no message carries, such as a group's avatar image under the conversation key, goes through encryptFileWithKey({file, key}) and decryptFileWithKey({encrypted, key}).
Method Use case
decryptEvents Batch (initial load, pagination). Self-extracts keys; collects errors.
decryptEvent Single event (webhook, real-time) with cached keys. Throws on error.

The conversation id is part of the message signature. The SDK signs it in its canonical (colon-separated) form, so you can pass either the colon or the hyphen form used in REST URL paths. Every derived value (sender, signing-key version, conversation key) still takes an explicit per-call override.

Quick start (Python)

from chat_xdk import (
    Chat,
    DecryptEventParams,
    DecryptEventsParams,
    EncryptMessageParams,
    ImportKeysParams,
    SetIdentityParams,
)

chat = Chat()
chat.import_keys(ImportKeysParams(private_key_blob, signing_key_version=key_version))  # or Chat(config).unlock(pin)
chat.set_identity(SetIdentityParams(my_user_id, key_version))
chat.set_cache_keys(True)
chat.set_signing_keys(signing_keys)

result = chat.decrypt_events(DecryptEventsParams(raw_events))
for dm in result["events"]:
    if dm["event"]["type"] == "Message":
        print(dm["event"]["sender_id"], dm["event"]["content"]["text"])

event = chat.decrypt_event(DecryptEventParams(new_event_b64, {}))
payload = chat.encrypt_message(EncryptMessageParams(conversation_id, "hello"))  # POST to the X API

Quick start (JavaScript / WASM)

import { createChat } from "@xdevplatform/chat-xdk";

const chat = await createChat({ juiceboxConfig, getAuthToken });
await chat.unlock("2580");
chat.setIdentity({ userId: myUserId, signingKeyVersion });
chat.setCacheKeys(true);
chat.setSigningKeys(signingKeys);

const result = chat.decryptEvents({ events: rawEvents });
for (const dm of result.events) {
  if (dm.event.type === "message") console.log(dm.event.senderId, dm.event.content.text);
}
const event = chat.decryptEvent({ eventB64: newEventB64 });
const payload = chat.encryptMessage({ conversationId, text: "hello" });

Full per-language API (params + return types) is in docs/API.md; runnable code for all six is in examples/.

Key storage

Private keys can be held two ways:

  • Local blob — exportKeys() / importKeys() move raw private-key bytes yourself, in every binding. Simplest; the bytes are unencrypted identity material — never commit them or log them, and zero them once stored.
  • Juicebox (production) — generate_keypairs() creates the keys, then setup(pin) stores the generated keys in Juicebox distributed PIN-protected storage; unlock(pin) recovers them on any device, so nothing secret touches disk. The Juicebox config comes from the X API public-keys response. See the optional path in examples/python.

Registering your public key with the X API is a separate, one-time provisioning step in both modes.

Network access

The SDK makes one kind of request itself: an anonymous GET https://api.x.com/2/chat/features, which reads the X API's feature switches. They pick the format the SDK sends in. Each chat reads them once, when it is created, and keeps the answer for its lifetime; chats created while a request is in flight share it.

The request carries no credentials and never delays or fails an SDK call. Until it answers, and for the chat's lifetime if it fails or is blocked, every switch stays off and sends stay in v0, the format 0.5.x sends. The Rust crate and the C ABI never make the request: a Rust app or a C caller makes it and hands the chat the answer (set_feature_switches, chat_xdk_set_feature_switches), and a chat that gets none sends v0.

Cryptography

Purpose Algorithm
Conversation key exchange ECIES — ECDH P-256 + AES-128-GCM
Message content XSalsa20-Poly1305 (256-bit key, random 192-bit nonce)
Media files crypto_secretstream_xchacha20poly1305
Message authorship ECDSA P-256 over the encrypted content
Key derivation HKDF-SHA256

Private key types are zeroized on drop (zeroize) and redact to [REDACTED] in debug output; all randomness comes from the OS CSPRNG. Details and the full encrypt/decrypt walkthroughs are in docs/CRYPTO.md.

Architecture

flowchart TD
    subgraph bindings["Language bindings"]
        py["Python (PyO3)"]
        js["JS / WASM"]
        go["Go (cgo)"]
        net[".NET / JVM (FFI)"]
        rs["Rust"]
    end
    bindings --> core["chat-xdk-core<br/>(ChatCore: keys, crypto, Thrift)"]
    core --> jb["Juicebox SDK<br/>(optional key storage)"]
    core --> api["X Chat API (your code)"]
Loading

ChatCore is the platform-agnostic engine; Chat adds the Juicebox lifecycle. Bindings are thin FFI wrappers — the crypto lives in one place. Module structure and data flows: docs/ARCHITECTURE.md.

Project structure

chat-xdk/
├── crates/
│   ├── core/     # chat-xdk-core: ChatCore engine, crypto, keys, Thrift types
│   ├── pyo3/     # Python binding (chat_xdk)
│   ├── wasm/     # JS/WASM binding (+ js/ wrapper and tests)
│   ├── ffi/      # C ABI (chat_xdk.h): Go's static archive, .NET's and the JVM's shared library
│   ├── dotnet/   # .NET binding (P/Invoke)
│   └── jvm/      # JVM binding (JNA)
├── go/chatxdk/   # Go binding (cgo + prebuilt static archives)
├── examples/     # one clone-and-run bot per binding (+ Makefile, CI)
├── docs/         # API.md, ARCHITECTURE.md, CRYPTO.md
└── tests/        # cross-binding fixtures (sdk_vectors.json)

Building

make build              # Rust workspace
make test               # Rust tests

make wheel-dev          # Python: maturin develop
make wasm               # WASM: wasm-pack build --target web
make test-go            # Go: build staticlib + go test
make dotnet-test        # .NET: native cdylib + xUnit
make jvm-test           # JVM: native cdylib + mvn test (JDK 17+)

The repo depends on the Juicebox SDK via a path dependency at ../juicebox-sdk (CI checks it out automatically). For JS/WASM with Juicebox key storage you also build juicebox-sdk's wasm crate — see crates/wasm/js/README.md.

Documentation

License

MIT — see LICENSE.

Third-party open-source components and their notices are listed in THIRD_PARTY_NOTICES.md.

About

X Chat encryption SDK

Resources

Code of conduct

Security policy

Stars

27 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages