Command-line client for ShieldFive. Encrypt a folder on your machine, then push or continuously sync it to your EU-hosted, post-quantum vault. Everything is encrypted locally before it leaves your machine. Read the code and verify that for yourself.
The point of this tool being open source is that you do not have to trust a marketing claim. The plaintext of your files is never sent to a ShieldFive server. The only bytes that leave your machine are AES-GCM ciphertext and a short cryptographic proof. Two files carry the whole story:
src/uploadCrypto.mjs— the encryption applied to every chunk and to the filename.src/upload.mjs— the only place bytes are sent over the network. Every request body is ciphertext.
If you want to see encryption happen without an account at all, run
sf encrypt <folder> — it encrypts each file locally and prints the ciphertext
size, uploading nothing.
- The underlying cryptography (
@shieldfive/crypto) has not undergone an external audit yet. Treat this as early software. - The direct upload path (files up to one 5 MiB chunk) has been run end to end against the production backend: files uploaded by this CLI appear in the web app with their real names and decrypt correctly.
- The multipart path (files larger than 5 MiB) is covered by a byte-for-byte review against the server contract and a mock end-to-end test that decrypts every part, but has not yet been exercised against production with a real large file.
sf syncis append-only: it uploads new and changed files; it does not yet mirror local deletions or renames into the vault.
- Node.js >= 20
- A ShieldFive account with a vault (create one in the web app first)
npm install -g @shieldfive/cli # provides the `sf` commandOr run it without installing:
npx @shieldfive/cli --helpTo read and build from source (the whole point — verify what it does before you trust it):
git clone https://github.com/shieldfive/cli.git
cd cli
npm install # @shieldfive/crypto (+ libsodium), @supabase/supabase-js, @noble/*
npm test # 27/27sf encrypt <folder> encrypt each file locally, upload nothing (no account needed)
sf push <folder> sign in, unlock, encrypt, upload every file once
sf sync <folder> [--watch] [--interval=N] upload new/changed files; --watch keeps a poll loop (N seconds, default 5)
sf push and sf sync pick the direct or multipart upload path automatically
from the file size. sf sync writes a .shieldfive-sync.json manifest in the
target folder that records what has already been uploaded (by size and mtime),
so unchanged files are skipped and an interrupted run does not re-upload
everything.
sf push and sf sync need only your ShieldFive account. Your password is
typed, never passed on the command line:
export SF_EMAIL=you@example.com
read -rs SF_PASSWORD; echo; export SF_PASSWORD
export SF_VAULT_PASSWORD="$SF_PASSWORD" # only if your vault password differs
sf push ./my-folder
sf sync ./my-folder --watchThe backend endpoints default to ShieldFive, so there is nothing else to
configure. SF_API_BASE_URL, SF_SUPABASE_URL, and SF_SUPABASE_ANON_KEY are
baked in with ShieldFive's public values (the Supabase anon key is a role=anon
JWT gated by row-level security, the same value any browser receives) and only
need overriding if you point the CLI at a development backend.
If your account has an authenticator app (TOTP) enabled, the CLI performs the AAL2 step-up automatically: after your password it prompts
Two-factor code (6 digits):
Enter the current code from your authenticator. In a non-interactive context
(scripts, CI) set SF_TOTP_CODE to the current code instead of being prompted.
Only TOTP is supported; SMS and other factors are not.
The upload uses chunked AES-GCM for file content and issues a
server-verified HMAC proof. (The sf encrypt demo uses the post-quantum hybrid
suite; the sync/push path uses AES-GCM, which is what the vault's upload
protocol and server-side proof expect.)
- Filename — encrypted with AES-GCM under a key derived from your vault root
key via Argon2id (
encryptMetadataV4). The server stores ciphertext; it never sees the name. A keyed HMAC of the lowercased name (hashMetadataV4) lets the server deduplicate without learning the name. - File content — each chunk is AES-GCM encrypted under a per-file content key (itself wrapped by your root key). The nonce is a 4-byte random prefix followed by an 8-byte big-endian chunk counter, so every chunk has a distinct nonce.
- Upload proof —
HMAC-SHA256(proofKey, prefix || ciphertext)over the first chunk, whereprefix = [version=1][cipherVersion=1][chunkSize u32 BE][noncePrefix 4]. The server issuesproofKeywhen it creates the session and verifies the proof when finalizing. It ties the stored ciphertext to the session without the server ever seeing plaintext. - Multipart (> 5 MiB) — the file is streamed one 5 MiB chunk at a time (never loaded whole into memory), each chunk uploaded as a Backblaze part. The ciphertext hash the server checks is SHA-1 over the concatenated raw part digests; the server finishes the large file with the ordered part list.
The mock end-to-end test in test/upload.flow.test.mjs
captures every byte this client would send, decrypts each part with the key
recovered from the session envelope, and asserts the result equals the original
file — a machine-checked demonstration that only ciphertext is uploaded.
src/cli.mjs— thesfentry point (argument parsing, command dispatch)src/auth.mjs— Supabase sign-in to a Bearer tokensrc/vault.mjs/src/unlock.mjs— fetch the wrapped vault key, unwrap the root key from your password (Argon2id / PBKDF2)src/uploadCrypto.mjs— filename and chunk encryption, upload proof, multipart ciphertext hashsrc/upload.mjs— create session, encrypt, upload (direct + multipart), finalize; the streaming chunk readersrc/sync.mjs—sf sync: manifest, change detection, reconcile pass, watch loop (reusesupload.mjs)src/sfCrypto.mjs— the post-quantum hybrid encrypt/decrypt used by thesf encryptdemotest/*.test.mjs— round-trip guarantees plus the mock end-to-end upload and sync tests
Please report vulnerabilities privately — see SECURITY.md. Do not open a public issue for security reports.