Skip to content

chat-xdk: add encrypt_file_with_key and decrypt_file_with_key: group avatars encrypt as X clients do, and encrypt_stream calls migrate one for one - #75

Merged
cursor[bot] merged 19 commits into
mainfrom
cursor/file-with-key-a72f
Oct 28, 2026
Merged

cursor[bot] merged 19 commits into
mainfrom
cursor/file-with-key-a72f

Conversation

@santiagomed

@santiagomed santiagomed commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Adds a pair of calls in every binding for a file under a key the app already holds, rather than a file a message carries: a group avatar under the conversation key, as X clients encrypt it, or an app's own files at rest. They write the v0 secretstream file format that 0.5.x encrypt_stream wrote, so files open across both releases, X Android's avatars included.

0.5.x:

encrypted = chat.encrypt_stream(data, key)

Now:

encrypted = chat.encrypt_file_with_key(EncryptFileWithKeyParams(key=key, file=data))

Rust:

// 0.5.x
let encrypted = chat.encrypt_stream(&data, &key)?;
// now
let encrypted = chat.encrypt_file_with_key(&EncryptFileWithKeyParams::new(data, key.to_bytes()))?;

Covers Rust, Python, JS/TS, Go, JVM, .NET and the C ABI, with tests (round trips, released 0.5.0/0.5.2 encrypt_stream fixtures, and a plain-libsodium check), API.md, why.md, every 0.7 migration guide and the CHANGELOG. It also corrects the docs on avatars during key rotation: X clients re-encrypt the avatar on every rotation, so apps should too.

Adds a pair of calls in every binding for a file under a key the app already holds, rather than a file a message carries: a group avatar under the conversation key, as X clients encrypt it, or an app's own files at rest. They write the v0 secretstream file format that 0.5.x encrypt_stream wrote, so files open across both releases, X Android's avatars included.

0.5.x:

encrypted = chat.encrypt_stream(data, key)

Now:

encrypted = chat.encrypt_file_with_key(EncryptFileWithKeyParams(key=key, file=data))

Rust:

// 0.5.x
let encrypted = chat.encrypt_stream(&data, &key)?;
// now
let encrypted = chat.encrypt_file_with_key(&EncryptFileWithKeyParams::new(data, key.to_bytes()))?;

Covers Rust, Python, JS/TS, Go, JVM, .NET and the C ABI, with tests (round trips, released 0.5.0/0.5.2 encrypt_stream fixtures, and a plain-libsodium check), API.md, why.md, every 0.7 migration guide and the CHANGELOG. It also corrects the docs on avatars during key rotation: X clients re-encrypt the avatar on every rotation, so apps should too.

The migration guides, their index and why.md name 0.7.1, the release that carries these calls, in their titles, snippet markers and symbol indexes, and every guide's received-avatar snippet decrypts the image under the change's key version before showing it. decrypt_file_with_key's doc comment, API.md and CRYPTO.md say that an empty file is its 24-byte header alone, which nothing authenticates, so any 24 bytes decrypt to an empty file: a file that can't be empty, such as an avatar, should be rejected when it decrypts to nothing. A test pins that behavior.

Open in Web Open in Cursor 

santiagomed and others added 14 commits October 28, 2026 05:51
… a key the caller holds

The file is encrypted under the raw 32-byte key itself with
crypto::encryption::encrypt_file / decrypt_file, in the secretstream format
files take: a 24-byte header, 1024-byte frames, the last tagged final. A key
that isn't 32 bytes is invalid input; a bad, truncated or extended file is a
decryption error.

The 0.5.x compat fixtures gain an avatar-shaped file and an empty file that
the released 0.5.0 and 0.5.2 encrypted with encrypt_stream; decrypt_file_with_key
opens them and the existing text file byte for byte.
…encrypt a group's avatar on every key rotation
@CLAassistant

CLAassistant commented Oct 28, 2026 •

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 3 committers have signed the CLA.

✅ santiagomed
❌ cursoragent
❌ github-actions[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

santiagomed and others added 3 commits October 28, 2026 06:50
…the docs say so

An empty file is its secretstream header alone, which nothing
authenticates, so a file cut to exactly its header, or another key's empty
file, decrypts to an empty file instead of failing. decrypt_file_with_key's
doc comment and API.md promised a decryption error for every truncated or
other-key input. They and CRYPTO.md's truncation section now describe the
exception and tell callers whose files can't be empty (avatars, a message's
media) to reject an empty result. The format and its behavior are unchanged:
0.5.x encrypt_stream writes an empty file as the bare header too. A test
pins the behavior so a change to it updates the docs.

Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…ing its source files

AGENTS.md keeps comments to this code's intent, not lineage: the core and
Python test comments keep what X Android does (avatars under the raw
conversation key in this secretstream format) and drop the x-android
source file and function names.

Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


1 out of 2 committers have signed the CLA.
✅ (santiagomed)[https://github.com/santiagomed]
❌ @cursoragent
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

cursoragent and others added 2 commits October 28, 2026 07:31
…shows it

The six AvatarChanged snippets showed the avatar from its URL, then
decrypted `downloaded` into an unused `image`, and only four of them
checked that the change names a key version: Python indexed the key map
with None and the JVM passed a null key. Each now decrypts the downloaded
image under the key for the change's version, when it names one, and shows
the decrypted image. The snippet checker's show_avatar stand-ins take the
image bytes.

Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…oughout

The guides install 0.7.1 and call encrypt_file_with_key, which 0.7.0
doesn't have, but their titles, symbol indexes, `[new]` tags and snippet
markers named 0.7.0, and "About the snippets" said CI compiles each 0.7.0
half against 0.7.0. They now name 0.7.1, the release the packages will
carry, as the guides named 0.7.0 when it shipped; the snippet checker reads
any release marker as code migrated to. The claim about which apps read
0.7.1's sends names a 0.7 release, since 0.7.0 reads them too, the index
says the CHANGELOG lists changes by release, and why.md explains the
changes from 0.5.x to 0.7.1. The doc-snippets job's comment names the 0.7.1
halves.

Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
@cursor
cursor Bot merged commit ccf6719 into main Oct 28, 2026
22 of 24 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants