Repository navigation
sf push/sync write the web's default format (cipher_version 3, row-bound, PQ); sync --watch refreshes its session - #10
Merged
Conversation
…ow-bound); refresh the session under sync --watch
This was referenced Oct 1, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
From the 2026-09-30 audit (crypto finding 9, plus the mcp-cli-desktop "Claims" and
sf sync --watchfindings):sf push/sf syncwrote the deprecated v0 format (cipher_version1: AES-GCM chunks under a random 4-byte nonce prefix, no header), whichcrypto/spec/format-v0.mdforbids for new writes. It has no truncation detection: a server can drop trailing chunks and the file still decrypts. It has no row binding, so a server can move a file's bytes onto another row. It has no post-quantum layer, while the package was described as a "post-quantum vault".sf sync --watchsigned in once. After the access token expired (about 1 h), every pass failed every changed file while the process kept looping and never exited non-zero.@shieldfive/cryptofloated on^1.0.0-beta.1.Fix
Format. Uploads are now exactly what the web writes by default (
encryptModal.tsx,v1-pq-hybrid):cipher_version3, suite 0x03, v1 wire format, produced by@shieldfive/crypto'sencryptStreamPqHybridV1.file_idis the row id that create-upload-session returned (uuidToBytes(session.fileId)), which binds the object to its row. The MAC'd header carries the plaintext size and chunk count, and the chunk AAD binds index, total and is-final.createIdentity({ masterSecret: rootKey }), exactly as webutils/pqIdentity.tsdoes. It is never fetched, so the server cannot substitute it.csk_wrappedholds the classical envelope key under the root key, as on the web.pqk_fk_wrappedis left for the owner's backfill, also as on the web.header || frame_0, part i =frame_i. The split is computed from the byte format (header length, then each frame's length prefix), not from how the encoder enqueues. Chunk size is the server's v1 chunk (5 MiB,session.chunkSize).buildUploadProofV3from the library, the same function the web and MCP use.cipherVersion: 3, and nocipher_nonce_prefixorchunkSize(the server rejects both for v1).nameEncryptedV6(encryptNameV6, AAD = row id), the same best-effort upgrade the web sends, so the v4 name written at create is replaced by a row-bound one.encryptFileChunk,encryptChunkWithDigest, the v0 proof, the nonce-prefix generator) is removed fromsrc/. Reading v0: the CLI has no download or decrypt path, so there is nothing to keep. Files uploaded by 0.3.0 and earlier still open in the web and mobile apps, which read v0.sf sync --watch.authAndUnlocknow also returns the refresh token, the expiry and arefreshSession(the samerefreshAccessTokenexchange the agent uses).runSyncasks for a token before every upload. It refreshes once the token is within 120 s of expiry. The refresh is single-flight and the rotated refresh token replaces the old one, because Supabase revokes a session whose refresh token is reused. If the refresh fails, sync stops with an error (non-zero exit) instead of failing every file forever. A singlesf syncpass with failed files now exits non-zero.Re-uploads as new root-level copies: not fixed, documented. Replacing or trashing the previous
fileIdmeans moving it into the Bin. That needs the Bin's folder key to re-wrapcsk_wrappedand the name, and this client holds only the root key, so it is not cheap. The sync.mjs header now states the behaviour, and it is listed as a follow-up below.Also:
@shieldfive/cryptois pinned to~1.0.1. The description no longer says "post-quantum vault"; it names the hybrid scheme. README "How the upload works" is rewritten for the new format, and CHANGELOG is updated.Tests (behavioural)
test/upload.flow.test.mjs(rewritten assertions): every byte the client would send is captured and the stored object is opened with@shieldfive/crypto's reference reader (autoDecryptBlob), using only the session fields and the root key (test/v1Reference.mjs, which imports none of the CLI's crypto). The tests assert:file_id== row id,cipherVersion: 3and no v0 fields in the request;nameEncryptedV6opens for this row and not for another one;test/v1Format.test.mjswith shared vectorstest/vectors/v3-upload.json(regenerate withnode scripts/generate-vectors.mjs; decrypt vectors, since encryption is randomised). The tests check:decryptStreamPqHybridV1;test/sync.test.mjs: three new tests:--watchrefreshes once (with the held refresh token) and later uploads use the new token; a refresh failure ends sync with an error before any upload is attempted with the expiring token; a single pass with a failed file rejects.Fails on origin/main, passes here. The new test files were copied onto an origin/main checkout:
upload.flow: 3 of 4 fail (v0 output, no v3 header,file_idis not the row);v1Format: fails (no v3 writer);sync: the refresh test fails, and the refresh-failure test never terminates on main (the watch loop runs forever), which is the bug.On this branch, full
npm test(macOS, Node 20): 133 tests, 133 pass.Rollout
Client-only. The server already accepts
cipher_version3 withfolderId: nullfrom the web. Merge, then the founder releases. No tag pushed, nothing published.Not verified against the live server. All upload tests use mocks written from the server source (
create-upload-session,complete-upload/finalize.ts,uploadProofClient.ts). Before release, run one realsf pushof a small file and a file over 5 MiB on a test account and open both on web and Android.Follow-ups
sf syncstill uploads into the vault root only; there is no folder target.pqk_fk_wrapped, as web uploads are. Folder shares and agent grants can read them only after the owner's backfill.