The Files vault crypto in Kotlin, pinned to the browser's vectors #77

Merged
paulhitt merged 1 commits from feat/files-engine into main 2026-09-04 20:49:53 +00:00
paulhitt commented 2026-09-04 19:06:35 +00:00 (Migrated from gitlab.com)

Names, key wrapping, chunked content, the KDF and the identity record —
everything needed to open a Files vault written by any other FlameNet
client.

Held to vectors/files-v1.json, produced by the JAVASCRIPT engine in
the browser. Not to the Swift engine. Checking Kotlin against Swift would
establish only that the two agree, and two implementations agreeing on
the wrong answer is how somebody's files stop opening when they change
phones. The file is copied byte for byte from the iOS repository rather
than regenerated, because a vector file regenerated by the implementation
under test proves nothing.

Two vectors were recomputed from scratch in Python before any Kotlin ran
against them — struct.pack('>hQ') for the label encoding and
hmac.new(...).digest() for the name digests. The café case matches
under NFC and NOT under NFD, which is what pins the normalisation form
rather than assuming it: a platform normalising differently does not
error, it silently fails to find a file filed on another device.

Each guard was proven by breaking the code and watching the right test go
red: normalisation removed → the composed/decomposed digest test; the
wrap salt halves swapped → the browser's label-key grant; the end-flag or
the chunk index dropped from the authenticated data → the browser's file
stops decrypting at all.

The truncation and reordering tests each go on to show the surviving
bytes are still authentic under their ORIGINAL index and end-flag. Without
that second half "it threw" is all they would establish, and any bug at
all makes a mangled blob throw.

Two notes on the platform:

  • PBKDF2 is hand-written on Mac, as Bip39.pbkdf2HmacSha512 already is.
    The JCA path takes a CharArray and lets the provider decide the
    encoding; the reference implementations take bytes. A passphrase outside
    ASCII would otherwise stretch differently under a different provider and
    the record simply would not open, with nothing to say why.
  • The public half of an identity is recovered by agreeing against the
    X25519 base point, because the JCA will not derive one from an XDH
    private key. That it returns 32 bytes proves nothing, so it is pinned to
    the vector file's own identityPublicKey.

43 tests. FileContent rather than Content: the messenger's Content is
a message payload and the two are not interchangeable.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

Names, key wrapping, chunked content, the KDF and the identity record — everything needed to open a Files vault written by any other FlameNet client. Held to `vectors/files-v1.json`, produced by the JAVASCRIPT engine in the browser. Not to the Swift engine. Checking Kotlin against Swift would establish only that the two agree, and two implementations agreeing on the wrong answer is how somebody's files stop opening when they change phones. The file is copied byte for byte from the iOS repository rather than regenerated, because a vector file regenerated by the implementation under test proves nothing. Two vectors were recomputed from scratch in Python before any Kotlin ran against them — `struct.pack('>hQ')` for the label encoding and `hmac.new(...).digest()` for the name digests. The `café` case matches under NFC and NOT under NFD, which is what pins the normalisation form rather than assuming it: a platform normalising differently does not error, it silently fails to find a file filed on another device. Each guard was proven by breaking the code and watching the right test go red: normalisation removed → the composed/decomposed digest test; the wrap salt halves swapped → the browser's label-key grant; the end-flag or the chunk index dropped from the authenticated data → the browser's file stops decrypting at all. The truncation and reordering tests each go on to show the surviving bytes are still authentic under their ORIGINAL index and end-flag. Without that second half "it threw" is all they would establish, and any bug at all makes a mangled blob throw. Two notes on the platform: - PBKDF2 is hand-written on `Mac`, as `Bip39.pbkdf2HmacSha512` already is. The JCA path takes a `CharArray` and lets the provider decide the encoding; the reference implementations take bytes. A passphrase outside ASCII would otherwise stretch differently under a different provider and the record simply would not open, with nothing to say why. - The public half of an identity is recovered by agreeing against the X25519 base point, because the JCA will not derive one from an XDH private key. That it returns 32 bytes proves nothing, so it is pinned to the vector file's own `identityPublicKey`. 43 tests. `FileContent` rather than `Content`: the messenger's `Content` is a message payload and the two are not interchangeable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
paulhitt commented 2026-09-04 19:06:35 +00:00 (Migrated from gitlab.com)

assigned to @paulhitt

assigned to @paulhitt
paulhitt commented 2026-09-04 20:49:51 +00:00 (Migrated from gitlab.com)

deleted the feat/files-label-lattice branch. This merge request now targets the main branch

deleted the `feat/files-label-lattice` branch. This merge request now targets the `main` branch
paulhitt commented 2026-09-04 20:49:53 +00:00 (Migrated from gitlab.com)

mentioned in commit 54f9a1e0fa

mentioned in commit 54f9a1e0fa6ea68324af29f1cb2817a1788c5da2
paulhitt (Migrated from gitlab.com) merged commit 54f9a1e0fa into main 2026-09-04 20:49:53 +00:00
paulhitt commented 2026-09-09 19:01:28 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
Sign in to join this conversation.