Exporting and decrypting your ShieldFive data
ShieldFive stores your files as ciphertext under EU jurisdiction. The cryptography is open source. If you want a backup that does not depend on ShieldFive continuing to operate, you can pull every encrypted blob plus the wrapped-key bundle out of your account today, and decrypt them later on any machine that runs Node.js. The code does not stop working when the company does.
This document is the recipe. It has two parts.
- Part 1 — pull everything out of your account while ShieldFive is still up. Curl recipe against the existing API.
- Part 2 — decrypt the saved bundle offline using
@shieldfive/cryptoand a small Node script. No ShieldFive servers required.
The recipe is intentionally minimal — there is no one-click "export my account" button in the UI yet. That is a planned follow-up (P1). What is shipped is what is documented here.
What you need before starting
- The email and master password for your ShieldFive account.
- A working
curl,jq, and Node.js 20+ installation. - About one terminal's worth of patience.
- Your recovery key if you have lost your master password. Recovery key uses the same offline path with one flag change.
Where the bundle lives
The recipe writes everything into a working directory ./backup/:
backup/
vault.json # your wrapped vault key bundle
files.json # array of file rows (wrapped CSKs + metadata)
folders.json # flat array of folder rows (wrapped folder keys)
blobs/<file-id>.bin # encrypted blob per file
Everything in this directory except vault.json is already
ciphertext. vault.json contains key material wrapped under a
key derived from your master password — losing your password
makes it useless to anyone else and to you.
Part 1 — pull your data
Step 1.1 — get a Supabase access token
ShieldFive's database is a Supabase project. Sign in against Supabase's auth REST endpoint with your account credentials. The resulting access token (a JWT) is what authenticates every subsequent request.
mkdir -p backup/blobs
cd backup
# Substitute your account credentials.
EMAIL='[email protected]'
PASSWORD='your-account-password'
# These two values are public and are pinned to ShieldFive's
# Supabase project. They are the same values the website itself
# loads on startup.
SUPABASE_URL='https://dskbmjpanehckhqzkclp.supabase.co'
SUPABASE_ANON_KEY='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRza2JtanBhbmVoY2tocXprY2xwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzAxMTc3MzEsImV4cCI6MjA4NTY5MzczMX0.AK7K8tyrAWFWgfaa6vvTveqmKiMrL2nY1m7qS9TII9U'
# Build the request body with jq, not string interpolation, so a password
# containing " \ or $ is encoded safely instead of breaking the JSON.
PAYLOAD=$(jq -n --arg e "$EMAIL" --arg p "$PASSWORD" '{email:$e,password:$p}')
ACCESS_TOKEN=$(curl -sX POST \
"$SUPABASE_URL/auth/v1/token?grant_type=password" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
| jq -r .access_token)
[ -n "$ACCESS_TOKEN" ] || { echo 'sign-in failed'; exit 1; }
EMAIL must be the email address you sign in to ShieldFive with (your
account login) — not a contact, alias, or forwarding address. Sign-in
fails against anything else.
Step 1.2 — pull your vault key bundle
curl -s "https://shieldfive.com/api/vault-key" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
> vault.json
The bundle contains: the root key wrapped under the user-key
(rkWrappedByUk), the user-key derivation parameters (ukSalt,
ukIv, ukKdf, ukIterations or ukArgon2Preset), and the
root key wrapped under the recovery key (rkWrappedByRec,
recIv).
Step 1.3 — list every file
Files are queryable directly against Supabase's PostgREST endpoint. Row-Level Security restricts what you see to rows you own.
# PostgREST is keyset-paginated using Range. 0-999 = first 1000 rows.
curl -s "$SUPABASE_URL/rest/v1/files?select=id,name,path,created_at,size_bytes,folder_id,csk_wrapped,csk_iv,cipher_version,cipher_chunk_size,cipher_nonce_prefix&deleted_at=is.null&order=created_at.asc" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Range: 0-999" \
> files.json
If you have more than 1000 files, repeat with Range: 1000-1999,
Range: 2000-2999, etc., and concatenate the JSON arrays. Most
accounts have far fewer.
Step 1.4 — list every folder
Folder keys form a tree: each fk_wrapped is the folder key
wrapped under the parent folder's key. Root-level folders are
wrapped under your root key. You need the whole flat list so the
offline decryptor can walk the ancestry.
curl -s "$SUPABASE_URL/rest/v1/folders?select=id,name,parent_id,fk_wrapped,fk_iv&deleted_at=is.null" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
> folders.json
Step 1.5 — download every encrypted blob
Blobs live in Backblaze B2. The download flow is two-step: ask
the app for a single-use token URL, then fetch the blob from it.
Both requests must carry Authorization: Bearer $ACCESS_TOKEN —
the second one too, or it returns 401 {"error":"Unauthorized"}.
The token-issue endpoint is rate-limited to ~5 requests/minute per IP
(two overlapping per-IP limits, 10/min and 5/min — the tighter one
binds), on top of your account's bandwidth quota, so loop with care.
First, smoke-test a single file before looping over everything:
# /api/files/download returns { url, encryption }; the url points at
# ShieldFive's single-use download proxy.
FILE_ID=$(jq -r '.[0].id' files.json)
DOWNLOAD_URL=$(curl -s \
"https://shieldfive.com/api/files/download?fileId=$FILE_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r .url)
[ -n "$DOWNLOAD_URL" ] && [ "$DOWNLOAD_URL" != null ] \
|| { echo 'download token failed (auth, rate limit, or wrong fileId)'; exit 1; }
curl -sL "$DOWNLOAD_URL" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-o "blobs/$FILE_ID.bin"
Then loop over every file:
for FILE_ID in $(jq -r '.[].id' files.json); do
echo "Fetching $FILE_ID"
# /api/files/download returns { url, encryption }
TOKEN_RESPONSE=$(curl -s \
"https://shieldfive.com/api/files/download?fileId=$FILE_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN")
DOWNLOAD_URL=$(echo "$TOKEN_RESPONSE" | jq -r .url)
if [ -z "$DOWNLOAD_URL" ] || [ "$DOWNLOAD_URL" = "null" ]; then
echo " download token failed: $TOKEN_RESPONSE"
continue
fi
curl -sL "$DOWNLOAD_URL" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-o "blobs/$FILE_ID.bin"
sleep 13 # stay under the 5 req/min per-IP limit
done
The token URL is single-use and short-lived (seconds to minutes, scaled to file size). Because the fetch is authenticated by your account JWT — not a browser session — it is not bound to a browser or IP: a non-browser client such as curl works. Request a fresh URL per file; a consumed or expired one will not serve again.
You now have a complete bundle. Skip ahead to Part 2 to decrypt it offline, or keep the bundle on cold storage until you need it.
Part 2 — decrypt the bundle offline
The decryptor is a small Node script (decrypt-one.mjs) that
takes the bundle, your master password (or your recovery key),
and a file ID, and writes the plaintext. It uses two open-source
libraries: @shieldfive/crypto and libsodium-wrappers-sumo.
Step 2.1 — set up the decryptor
# Pick any working directory; doesn't have to be the bundle dir.
mkdir -p ~/shieldfive-decrypt && cd ~/shieldfive-decrypt
npm init -y >/dev/null
npm install @shieldfive/[email protected] libsodium-wrappers-sumo
# Save decrypt-one.mjs (appendix at the bottom of this page) here.
@shieldfive/crypto is the same library that runs in your
browser when you upload or download a file. It is published on
npm and pinned in the recipe so you can compare what you run
against the version your files were encrypted with.
Step 2.2 — decrypt one file
# Pick a file ID from files.json. Example: the first file.
FILE_ID=$(jq -r '.[0].id' /path/to/backup/files.json)
# --output is treated as a directory; create it first. (The script also
# creates it with mkdir -p if missing — this line just makes the intent clear.)
mkdir -p ./decrypted
node decrypt-one.mjs \
--bundle /path/to/backup \
--file-id "$FILE_ID" \
--password 'YOUR MASTER PASSWORD' \
--output ./decrypted
The script:
- Loads
vault.json,files.json,folders.json. - Stretches your master password with Argon2id (or PBKDF2 for older accounts) using the stored parameters → derives the user-key.
- Unwraps the root key with the user-key.
- Walks the folder ancestry of the requested file, unwrapping each folder key under its parent's key.
- Unwraps the per-file content key (CSK) under the file's parent-folder key.
- Decrypts the encrypted filename: Argon2id under the parent key at the strength the web client recorded in the name (interactive on mobile, moderate on desktop), then AES-GCM. AAD-bound names are verified against the file's row id.
- For Suite 0x01 (AES-256-GCM-v1) and v0 files: decrypts the blob with the CSK as the content key.
- For Suite 0x03 (PQ-hybrid, the default for new uploads since 2026-05-17): re-derives the ML-KEM-1024 secret key deterministically from the root key, then decrypts the blob using the CSK as the classical envelope key plus the ML-KEM secret.
- Writes the plaintext to
./decrypted/<original-filename>.
Step 2.3 — decrypt every file (loop)
mkdir -p decrypted
for FILE_ID in $(jq -r '.[].id' /path/to/backup/files.json); do
node decrypt-one.mjs \
--bundle /path/to/backup \
--file-id "$FILE_ID" \
--password 'YOUR MASTER PASSWORD' \
--output ./decrypted \
|| echo "failed: $FILE_ID"
done
Step 2.4 — using your recovery key instead of your password
If your password is lost, the recovery key wraps the same root
key under a different key. Pass --recovery-key instead of
--password:
node decrypt-one.mjs \
--bundle /path/to/backup \
--file-id "$FILE_ID" \
--recovery-key 'YOUR BASE64 RECOVERY KEY' \
--output ./decrypted
What this recipe does and does not recover
Recovered:
- Every encrypted blob.
- The wrapped per-file content keys (CSKs) and their IVs.
- Per-file metadata: cipher suite, chunk size, plaintext size.
- The ability to decrypt offline given your master password OR your recovery key, with no ShieldFive server in the loop.
- Original filenames and folder placement, by walking the
folder ancestry stored in
folders.json.
Not recovered:
- Active share links. Share links are server-side state: short-lived slugs, bcrypt-hashed share passwords, share expiration and download counters. If ShieldFive disappears, outstanding share links stop working — the recipients can no longer click through to download. The underlying file is still in your bundle and you can re-share it however you like (email, signal, your own host) once you decrypt it.
- Per-file analytics. Download counters, last-accessed timestamps, and similar dashboard-only data live in the database but are not exported by the recipe. They do not affect file recoverability.
- Account-level state. Subscription, billing history, MFA enrollment, recovery-key acknowledgement timestamps. None of this is needed to decrypt your files.
Trust boundary:
- This recipe runs on your machine. It hits Supabase REST and
ShieldFive's
/api/files/downloadfor ciphertext and key bundles, but the actual key derivation and decryption happen in your process. ShieldFive cannot observe your master password or your plaintext at any point in the recipe. - If ShieldFive is no longer here, the Supabase REST call in Part 1 will fail — Supabase is the storage backend, not the decryption. Part 2 runs entirely offline from a previously saved bundle. The honest reading of the trust claim is: download your bundle while we are still up, and the bundle is forever-decryptable thanks to the wire format being documented and the library being open source.
Verifying what you ran
The decryptor is small (~250 lines, no obfuscation). Read it before you trust it with your password. Two cross-checks:
# 1. Confirm you're running the published library, not a tampered local copy.
npm ls @shieldfive/crypto
# Should print: @shieldfive/[email protected] (or whichever pinned version)
# 2. Compare decrypted vs known-good (only useful while ShieldFive is up).
# Pick a small file. Decrypt with the recipe, download via the web UI,
# then:
shasum -a 256 ./decrypted/<file-name>
shasum -a 256 ./from-web-ui/<file-name>
# The two hashes must match exactly.
Appendix — the offline decryptor
The full source of decrypt-one.mjs lives at
examples/decrypt-one.mjs
in the @shieldfive/crypto repository. Copy it into your decrypt working
directory, install the two npm dependencies listed above, and run
it as shown in Part 2. The same file is also distributed in any
exported audit pack.
Reporting problems with this recipe
If a step here does not match what your tools produce, that is a documentation defect — please open an issue or report it via security contact if you believe the gap has security implications. The recipe is verified end-to-end as part of every release; a mismatch is worth flagging.