Skip to content
shieldfivePublic

About

Open-source command-line client for ShieldFive: encrypt a folder locally, then push or sync it to your EU-hosted, post-quantum vault. Read the code; it encrypts before upload.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

@shieldfive/cli

Command-line client for ShieldFive. Encrypt a folder on your machine, then push or continuously sync it to your EU-hosted vault. File contents and names are encrypted locally before they leave your machine (contents with a post-quantum hybrid key). Read the code and verify that for yourself.

CI License Status

The point of this tool being open source is that you do not have to trust a marketing claim. The plaintext of your files and their names is never sent to a ShieldFive server:

  • File contents — AES-256-GCM chunks under a per-file key that combines a random envelope key (wrapped by your vault root key) with an ML-KEM-1024 + X25519 hybrid encapsulation to your vault identity.
  • File names — AES-256-GCM under a key derived from your vault root key (not the post-quantum layer), plus a keyed HMAC of the lowercased name so the server can spot duplicates without reading it.

What the server does see, by design: the plaintext size of each file, part hashes (SHA-1 of ciphertext), the wrapped keys, an upload proof, timestamps and your account. Sign-in sends a secret derived from your password, never the password itself. 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 file bytes are sent over the network. Every file body it sends 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.

Honest scope

  • The underlying cryptography (@shieldfive/crypto) has not undergone an external audit yet. Treat this as early software.
  • Neither upload path has been exercised against production since 2026-09-03. The direct (≤ 5 MiB) path was validated end to end against the backend as it stood at 0.1.0, but the server then moved that path to a presigned S3 PUT and stopped issuing an upload token. 0.2.0 hard-required that token, so it could not upload any file of 5 MiB or less at all. This release rewrites the direct path to the presigned PUT. It is covered by the offline mock test in test/upload.flow.test.mjs and sends byte-for-byte the request the web app sends, but it has not been run live.
  • 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 sync is append-only: it uploads new and changed files; it does not yet mirror local deletions or renames into the vault.
  • sf login and the agent have not been run against production. They are covered by offline tests, including one that runs the agent as its own process and reaches it from separate sf processes against a local stub of the auth and verify endpoints. Sign-in, unlock and session refresh against the live backend are not covered.

Requirements

  • Node.js >= 20
  • A ShieldFive account with a vault (create one in the web app first)

Install

npm install -g @shieldfive/cli    # provides the `sf` command

Or run it without installing:

npx @shieldfive/cli --help

To 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        # 107 tests

Commands

sf login [--idle=<hours>]                    sign in once; an agent holds the session in memory (locks after 8 h idle)
sf status                                    is an agent running, and for which account
sf logout [--everywhere]                     revoke the session and stop the agent; --everywhere signs out every device
sf push <folder> [--include-dotfiles]        encrypt and upload every file once (skips dotfiles unless asked; never follows symlinks)
sf sync <folder> [--watch] [--interval=N]    upload new/changed files; --watch keeps a poll loop (N seconds, default 5)
sf verify <file>...                          report whether each file is safely stored in your vault
sf encrypt <folder>                          encrypt each file locally, upload nothing (no account needed)
sf inspect <folder>                          read-only recursive migration inventory; upload nothing

sf push and sf sync pick the direct or multipart upload path automatically from the file size. Both read only regular files directly inside the folder: symlinks are skipped (and files are opened with O_NOFOLLOW), and names starting with . are skipped unless you pass sf push --include-dotfiles. An empty (0-byte) file is uploaded only when the server says it accepts empty files; otherwise it is reported as skipped and the exit code is 2. If an upload fails after its session was created, the pending session is cancelled so it does not hold quota. sf sync writes its manifest (.shieldfive-sync.json) with mode 0600.

sf inspect reports JSON counts of files, directories, bytes and skipped entries. It includes nested/hidden files and empty folders, reads metadata only, and does not follow symbolic links. Exit code 2 means entries were skipped. It does not upload anything or prove that a migration has completed. Current push/sync remain top-level only; do not use their completion as proof of a whole-tree copy.

With an agent running, sf sync records uploads in the agent's ledger (see What the agent can and cannot do) and sf verify works. Without one, 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 verify needs the agent.

Running the live commands

With sf login

sf login
sf push ./my-folder
sf sync ./my-folder --watch
sf verify ./my-folder/tax-return-2025.pdf
sf logout

sf login prompts for your email, your password and, if it differs, your vault password. It never reads them from the environment and there is no flag for them. It then starts an agent in the background and exits; every later command talks to that agent and needs nothing exported.

If SF_EMAIL and SF_PASSWORD are also set, sf push and sf sync sign in with those instead and tell you an agent is running, so a script written for one account is never sent to another. sf push exits 2 if a file changed while it was being uploaded: the vault then holds the version that was read, not the file as it is now.

Without an agent

sf push and sf sync still work from environment variables, exactly as before. 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 --watch

The 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.

Two-factor authentication

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.

With sf login you are asked once. The agent refreshes the stepped-up session on its own, so sf sync --watch keeps working on a two-factor account until the agent locks or you log out.

What the agent can and cannot do

The agent keeps two things in memory: your signed-in session and your unlocked vault key. Neither is written to disk. It listens on a Unix socket in ~/.shieldfive/run, a directory only your user can enter, and refuses to start if that directory is a symlink, owned by someone else or open to other users. Only one agent runs at a time.

It accepts six requests: status, upload, sync, verify, lock and logout. It cannot list what is in your vault, download a file, decrypt anything, or hand out the key, because there is no request for any of those. A process that reaches the socket can put files into your vault and ask whether files are backed up. It cannot read your vault through the agent.

Uploads are recorded in ~/.shieldfive/ledger/<account>.jsonl: the path, size, time, vault file id, and an HMAC of the bytes that were actually encrypted, under a key derived from your vault key. A plain hash would let anyone holding the ledger confirm whether you have a particular known document; the HMAC does not, without your vault key. Each record also carries a tag under that key, so a line added to the ledger by anything other than the agent is ignored, and records made on another machine do not count on this one.

sf verify reports a file as backed up only when the bytes on disk now match an upload in the ledger and the server confirms, at that moment, that the upload is stored, complete, intact and not in your Bin. It never decides by name or size. The other answers:

Answer Meaning
NOT confirmed these bytes were uploaded, but the server cannot confirm they are still safely stored
changed since upload this path was uploaded, but the file has changed since
no upload record this agent never uploaded these bytes; they may still be in your vault from another device
unreadable the local file could not be read

no upload record does not mean the file is missing from your vault. Do not delete anything on the strength of that answer.

The agent locks itself after 8 hours without an upload, sync or verify (sf login --idle=<hours> to change it; sf status does not count). Locking and sf logout both overwrite the key in memory first and then sign the session out on the server. Sessions on ShieldFive do not expire on their own, so a session that is forgotten rather than signed out would stay valid. If the server cannot be reached when you log out, sf logout says the session is still valid and exits 1; run sf login and then sf logout --everywhere once you are online.

Limits:

  • Windows is not supported. Use SF_EMAIL and SF_PASSWORD there.
  • Node cannot see which process connected to the socket, so directory permissions are the only gate. This is also how ssh-agent works on macOS.
  • Overwriting the key in memory is best-effort. A garbage-collected runtime can copy key material during crypto operations.
  • The ledger is per device. A file uploaded from another machine shows as no upload record here.

The full design, including the threat model, is in docs/agent-design.md.

How the upload works (for auditors)

Uploads are written in the format the web app writes by default: cipher_version 3, suite 0x03 (post-quantum hybrid) in the v1 wire format (@shieldfive/crypto spec/format-v1.md), using the library's own stream encoder.

  • Filename — encrypted with AES-GCM under a key derived from your vault root key via Argon2id (encryptMetadataV4), then, at finalize, re-sealed with the file's row id as additional data (v6, encryptNameV6), so the server cannot move one file's name onto another row. A keyed HMAC of the lowercased name (hashMetadataV4) lets the server deduplicate without learning the name.
  • File content — a MAC'd header carries the file's file_id (the row id the server assigned, so the stored object is bound to its row), the plaintext size and the chunk count. The content key combines a per-file envelope key (wrapped by your root key in csk_wrapped) with an ML-KEM-1024 + X25519 encapsulation to your vault identity, which is derived on this machine from your root key, as the web app derives it; the server cannot substitute a recipient. Each 5 MiB chunk is AES-256-GCM with its index, the total and an is-final flag in the additional data, so a dropped, reordered or truncated chunk fails to decrypt.
  • Upload proof — base64 of [3][3] || HMAC-SHA256(proofKey, [3][3] || header || frame_0) (buildUploadProofV3 in @shieldfive/crypto, the same function the web and the MCP server use). The server issues proofKey when it creates the session and verifies the proof against the stored object when finalizing.
  • Parts — each storage part is exactly one encrypted chunk frame; part 1 also carries the header.

Versions up to 0.3.0 wrote the legacy v0 format (AES-GCM chunks under a random nonce prefix, no header): no truncation detection, no row binding and no post-quantum layer. Files already uploaded that way still open in the apps; this client no longer writes it.

  • Direct (≤ 5 MiB) — the single encrypted chunk is PUT at a presigned S3 URL the server issues with the session. The URL's SigV4 signature is the only credential and it authorises exactly one object key, so the request carries no Authorization header and no X-Bz-* headers; the client sends no storage id at finalize, because the server reads the real one off the stored object. Because the signature covers host only, nothing on this path binds the body: the server compares the client's ciphertextHash against the client's own partSha1Array[0], so they always agree. Integrity here rests on the upload proof and on the header MAC and chunk tags at download, not on a wire checksum.
  • 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, opens the stored object with @shieldfive/crypto's reference reader from nothing but the session fields and the root key, and asserts the result equals the original file and the header's file_id equals the row id. test/v1Format.test.mjs checks the shared vectors in test/vectors/v3-upload.json (regenerate with node scripts/generate-vectors.mjs) against the library's blob and streaming readers.

Layout

  • src/cli.mjs — the sf entry point (argument parsing, command dispatch)
  • src/auth.mjs — sign-in to a Bearer token through ShieldFive's sign-in route, with two-factor step-up
  • src/loginSecret.mjs — the login secret derived from your password; for accounts on the current scheme the password itself is never sent
  • src/argon2.mjs — Argon2id, native when available, WebAssembly otherwise
  • src/vault.mjs / src/unlock.mjs — fetch the wrapped vault key, unwrap the root key from your password (Argon2id / PBKDF2)
  • src/uploadCrypto.mjs — filename encryption, the suite-0x03 part writer, upload proof, multipart ciphertext hash
  • src/upload.mjs — create session, encrypt, upload (direct + multipart), finalize; the streaming chunk reader
  • src/sync.mjs — sf sync: manifest, change detection, reconcile pass, watch loop (reuses upload.mjs)
  • src/prompt.mjs — terminal prompts; the password prompt echoes nothing
  • src/agent/ — sf login's agent: server.mjs (the process and its six requests), client.mjs, protocol.mjs (the request allowlist), ledger.mjs (HMAC upload ledger and verify classification), paths.mjs (socket directory checks), main.mjs (entry point and session handoff)
  • docs/agent-design.md — design record and threat model for the agent
  • src/sfCrypto.mjs — the post-quantum hybrid encrypt/decrypt used by the sf encrypt demo
  • test/*.test.mjs — round-trip guarantees plus the mock end-to-end upload and sync tests

Security

Please report vulnerabilities privately — see SECURITY.md. Do not open a public issue for security reports.

License

Apache-2.0

About

Open-source command-line client for ShieldFive: encrypt a folder locally, then push or sync it to your EU-hosted, post-quantum vault. Read the code; it encrypts before upload.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages