Skip to content

Recovery Key (Legacy)

If you can’t use passkeys (no WebAuthn PRF support, need offline-only mode), you can use a recovery key.

  • Browser doesn’t support WebAuthn PRF
  • Need encryption without any server
  • Want explicit control over the key
  • Migrating from an older version
ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ23-4567-ABCD-EFGH-IJKL-MNOP-Q
  • 52 characters (excluding dashes)
  • Base32-like alphabet (A-Z, 2-7)
  • ~256 bits of entropy
  • Human-readable (no confusable chars like 0/O, 1/I)
import { generateRecoveryKey } from "@ursalock/crypto";
const recoveryKey = generateRecoveryKey();

Uses crypto.getRandomValues() for cryptographic randomness.

import { create } from "zustand";
import { vault, type VaultOptionsLegacy } from "@ursalock/zustand";
const options: VaultOptionsLegacy<MyState> = {
name: "my-store",
recoveryKey: "ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ23-4567-ABCD-EFGH-IJKL-MNOP-Q",
// Optional: add server sync
server: "https://vault.example.com",
getToken: () => authToken,
};
const useStore = create(vault(storeCreator, options));

With recovery keys, the encryption key is derived using Argon2id:

Recovery Key + Salt → Argon2id → AES-256 Key

Parameters (OWASP 2024):

memory: 64 MB
iterations: 3
parallelism: 4
hashLength: 32 bytes

Store in 1Password, Bitwarden, Proton Pass, etc.

╔═══════════════════════════════════════════════════╗
║ ursalock Recovery Key ║
║ ║
║ ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ23-4567 ║
║ ABCD-EFGH-IJKL-MNOP-Q ║
║ ║
║ Store this safely. It's the only way to ║
║ decrypt your data. ║
╚═══════════════════════════════════════════════════╝
  • Store unencrypted in cloud storage
  • Email to yourself
  • Share via unencrypted chat
  • Use a guessable key
  • Reuse across different vaults

If you lose your recovery key:

  1. Your data cannot be recovered
  2. The server cannot help (zero-knowledge)
  3. Start fresh with a new key

This is by design — true E2EE means no backdoors.

If you started with a recovery key and want to switch to passkeys:

// 1. Pull data with old key
const oldStore = create(vault(storeCreator, {
name: "old-vault",
recoveryKey: oldKey,
}));
await oldStore.vault.pull();
const data = oldStore.getState();
// 2. Create new store with passkey
const newStore = createStoreWithPasskey(cipherJwk);
newStore.setState(data);
await newStore.vault.push();
// 3. Clear old vault
await oldStore.vault.clearStorage();
AspectPasskeysRecovery Keys
UXTap to authenticateRemember/store a key
Cross-deviceVia passkey providerManual key entry
Lost keyRe-register passkeyData lost forever
Offline-onlyNeeds server for authWorks fully offline
Browser supportChrome 116+, Safari 17+All browsers