Knowledge Base

Zero-knowledge backups: your recovery keys

Zero-knowledge backups seal your server's backups to encryption keys that only you hold. Once you register a recovery key set for a server, our backup system seals each backup's data-encryption key (DEK) to your whole registered key set as one age file. Any single key you registered opens it. We never see a private key, a passphrase, or an open DEK — so we can never recover your data for you, and neither can anyone who compromises us.

This guide is about the part that matters: getting your data back. Recovery always ends the same way, no matter which key you hold:

age -d -i <your-identity-file> backup.dek.age > dek.bin

age is the small, widely audited file-encryption tool (age-encryption.org), packaged on every major OS. Every recovery option below (a “rung”) is a deterministic mapping from a secret only you hold to an age identity (AGE-SECRET-KEY-1...). Registering a rung means sending us only the derived public half (an age1... recipient). Recovering means re-deriving the identity from your secret and running age -d. The exact byte-level constructions and test vectors are published in the key derivation reference, so you can verify or reimplement every rung with standard tools — no SHC software required.

For convenience, SHC provides a small recovery-key helper, shc-age.py (python3, standard library only, no network access — read it before you run it). The per-rung commands below use it; because the constructions are open (see the reference above), you can equally do everything with standard tools. To turn encryption on and manage keys from your server's Backups tab, see Zero-knowledge backups: turning it on.

The one rule

You can recover if you still hold any one registered key. You lose access only if you lose all of them. Register at least two independent rungs, stored in two different places, and you are safe.

The five rungs at a glance

  • SSH key — your existing ed25519 SSH key; zero new secrets to manage.
  • Passphrase — a six-word passphrase, strengthened by a slow key derivation.
  • Recovery card — 24 words on paper; the words are the key.
  • Bitcoin wallet — an independent secret minted by your wallet with BIP-85.
  • Nostr key — an age key derived one-way from your nsec.

There are also two more key types, covered below: PGP — not a derivation but a self-wrap — and a post-quantum hybrid that pairs an age key with an ML-KEM-1024 key for the longest horizon. You arm an ssh-ed25519 key in one click from your server's Backups tab, and register any other rung there too by pasting its derived age1... recipient into the panel's Add a recovery recipient control (or through the zero-knowledge routes in the API reference) — see turning it on. In every case what you submit is only the derived public key.

SSH key (zero new secrets)

If you have an ed25519 SSH key, you already have a recovery key.

  • What you hold: ~/.ssh/id_ed25519 (it never leaves your machine).
  • Register: the one-line public key (ssh-ed25519 AAAA... you@host) as a recipient of kind ssh-ed25519.
  • Recover:
age -d -i ~/.ssh/id_ed25519 backup.dek.age > dek.bin

That is the whole procedure — plain age, no SHC tooling at all.

This is the rung the Backups-tab Encryption panel arms in one click — see turning it on.

Passphrase

  • What you hold: your passphrase. We recommend the generated six-word form, e.g. fine-unfair-saddle-grid-find-correct.
  • How it maps: passphrase plus a per-service random salt, through argon2id (a deliberately slow, memory-hard function), to an X25519 key, to an age identity. The salt and cost parameters are public — they are returned by your server's data-preferences endpoint — and the passphrase never leaves your machine.
  • Register: the derived age1... recipient, kind password.
  • Recover: re-derive the identity from your passphrase and the salt (the construction is pinned in the key derivation reference), then run age -d as above.
python3 shc-age.py passphrase register --new-salt        # prints the salt + your age1... recipient
python3 shc-age.py passphrase recover --salt <salt-hex> -o pw.key
age -d -i pw.key backup.dek.age > dek.bin

Recovery card (24 words)

  • What you hold: 24 words written on paper. The words are the key — a 32-byte secret encoded on the standard BIP-39 word list, with a checksum that catches a mistyped word. No passphrase, no derivation cost, nothing else needed.
  • Register: mint a fresh card from a good random source on your own machine, write the 24 words down, and register the derived recipient as kind recovery-key.
  • Recover: decode the 24 words back to the key, then age -d.
python3 shc-age.py card generate                         # shows the 24 words + your age1... recipient
python3 shc-age.py card recover -o card.key               # prompts for the 24 words
age -d -i card.key backup.dek.age > dek.bin

Bitcoin wallet (BIP-85)

Your hardware or software wallet can deterministically mint an independent 32-byte secret for SHC backups using BIP-85 (“Deterministic Entropy From BIP32 Keychains”). The derivation happens on your wallet — Coldcard, Sparrow, and others support it — and your bitcoin keys never touch anything.

What you hold: your wallet seed. Our published convention path:

m/83696968'/128169'/32'/0'      (BIP-85 hex application, 32 bytes, index 0)
  • Register: have the wallet display the derived entropy at that path (64 hex characters); it is used directly as the age secret. Register the derived recipient as kind btc. The 24-word BIP-85 mnemonic application is accepted too — note down which path you used.
  • Recover: re-derive the same entropy on the wallet (same path), turn it into the identity, and run age -d.
  • Never paste a bitcoin private key or your seed words anywhere. Only the BIP-85 child entropy is used, and it cannot be reversed to your wallet seed.
python3 shc-age.py btc register                          # paste the BIP-85 entropy (64 hex, or 24 words)
python3 shc-age.py btc recover -o btc.key
age -d -i btc.key backup.dek.age > dek.bin

Nostr key

  • What you hold: your nsec. Software keys only — the derivation needs the raw key, so if your nsec lives in a remote signer or bunker you cannot use this rung; use a recovery card instead.
  • How it maps: a one-way, domain-separated derivation (HKDF-SHA256) turns your 32-byte nsec into a 32-byte age secret. The derived key cannot be reversed to your nsec, and it is useless anywhere else.
  • Register: the derived recipient, kind secp256k1 (the API also accepts nostr as an alias). The derivation runs locally; your nsec is never transmitted.
  • Recover: re-derive from your nsec, then age -d.
python3 shc-age.py nostr register                        # paste nsec1... or 64 hex (processed locally)
python3 shc-age.py nostr recover -o nostr.key
age -d -i nostr.key backup.dek.age > dek.bin

PGP (a wrap, not a derivation)

PGP keys are often hardware-bound (YubiKey, OpenPGP card) and cannot be exported, so there is deliberately no derivation from a PGP key. Instead you mint a dedicated age key and lock it under your PGP key, yourself, with your own gpg:

age-keygen | gpg -ea -r <your-key-id> > envelope.asc

Register the printed age1... recipient as kind pgp, and keep the envelope file with your PGP backups. To recover:

gpg -d envelope.asc > pgp.key
age -d -i pgp.key backup.dek.age > dek.bin

The same wrap and recovery are scripted by the helper: python3 shc-age.py pgp wrap --gpg-recipient <your-key-id> --envelope envelope.asc to mint and wrap the age key, and python3 shc-age.py pgp recover --envelope envelope.asc -o pgp.key to unwrap it for age -d.

Post-quantum hybrid (kind: pq-hybrid)

Every rung above rests on X25519 (elliptic-curve) cryptography. A cryptographically relevant quantum computer — if one is ever built — would break X25519, and an adversary who archives your sealed key files today could open them then (“harvest now, decrypt later”). If your backups must stay sealed on that horizon, register a post-quantum hybrid key: an age key paired with an ML-KEM-1024 key, the NIST post-quantum encryption standard (FIPS 203).

  • What you hold: two private halves — an age identity (mint one with age-keygen, or derive one with any rung above) and the ML-KEM-1024 private key you generate beside it. A hybrid seal opens only with both halves, so store them together and treat the pair as one key when you count your rungs.
  • Register: kind pq-hybrid, via the zero-knowledge routes in the API (the panel dropdown does not list this type yet): submit the age key's X25519 public key as pubkey and the ML-KEM-1024 public key as mlkem_pubkey. As always, only the public halves are sent. Both are bound write-once into your key set, and the recipients endpoint returns a reader_id fingerprint covering the pair.
  • Recover: the X25519 half is a full member of your key set — it opens backup.dek.age exactly like any other rung, with plain age -d. A hybrid-sealed key file additionally demands the ML-KEM half: breaking X25519 alone is not enough, which is the point.

The hybrid seal itself is not an age file. It is a small published construction — the X25519 and ML-KEM-1024 shared secrets combined into one AES-256-GCM seal — pinned byte-for-byte with test vectors in the key derivation reference, so opening one needs standard implementations, not SHC software. Hybrid seals ride alongside the classic age seal rather than replacing it. Because your key set is write-once, the time to include the ML-KEM half is when you choose the set — adding it later means a destructive re-key.

How a restore actually runs

  • Every encrypted backup has exactly one sealed key file (backup.dek.age) — the DEK sealed to your whole registered key set. When you restore, that file and the fingerprints of your registered keys are made available to you.
  • On your machine, re-derive the identity for whichever rung you still hold and run age -d. Compare the key's printed age1... recipient against the one you recorded at registration — if it differs, stop: wrong key or wrong data.
  • Hand the recovered DEK to the restore flow, which decrypts and rebuilds your server. The DEK is used once; the restored machine is yours again.

Your passphrase, private keys, and words never touch our systems at any step.

What if I lose my passphrase?

Use any other rung — every registered key opens every backup made while it was registered. Once you are back in, consider re-keying to a fresh set.

If you lose all registered keys, the backups are unrecoverable — by anyone, including us. That is not a limitation we can waive; it is the direct, deliberate consequence of us never holding your key. There is no “reset password” for zero-knowledge data.

Storing your recovery keys

  • Keep at least two rungs, in two different places, on two different media.
  • Paper (or metal) beats files for the 24-word card. A safe, a lockbox, a bank box.
  • Do not store two rungs where one fire or one compromise takes both.
  • Do not email keys to yourself, paste them into chat, or photograph them to a cloud-synced phone.
  • Record the age1... recipient shown at registration alongside (not with) each key — it lets you verify you hold the right key without exposing it.
  • Test a recovery once, early, on a non-critical backup. Registration only counts if you have proven you can recover.

Re-keying: starting fresh (and what it destroys)

If you believe a key was exposed, or you want a new set, use the re-key action. It installs a brand-new key generation, and every backup made under the previous generation becomes permanently unrecoverable. It is deliberately hard to trigger: you must type DESTROY-MY-BACKUPS and complete a two-step confirmation. Only re-key when you truly intend to abandon the old backups.

Recovery readiness checklist

  • I registered at least two independent rungs.
  • They are stored in two different safe places.
  • I recorded the recipients shown at registration.
  • I did a test recovery with each rung I depend on.
  • I understand that losing every key means the data is gone — and that this guarantee is the point.

For the exact constructions, parameters, and test vectors — enough to verify or reimplement everything yourself — see the key derivation reference. For endpoints and payloads, see the API reference. For what backups are included with your plan, see Backups and snapshots.

Please rate this article to help us improve our Knowledge Base.

0 0