Keys, backup & recovery

Every Wavelength wallet is a hierarchical-deterministic (HD) wallet rooted in a single seed. The seed’s origin depends on the auth mode (typed password or passkey), and everything else follows from it: what sits on disk, what counts as a backup, and what a user needs to get funds back on a new device. For the lifecycle states and UI phases around unlock, see Wallet lifecycle & auth.

One seed, two ways to derive it

A password wallet gets its seed from a freshly generated (or imported) 24-word mnemonic; the password protects the wallet’s key material at rest. A passkey wallet inverts this: the seed itself is derived from the passkey, so the passkey is simultaneously the unlock credential and the root secret.

%%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, sans-serif","fontSize":"16px","primaryColor":"#2c2c33","primaryTextColor":"#f5f5f7","primaryBorderColor":"#15e0c2","secondaryColor":"#24242a","secondaryTextColor":"#f5f5f7","secondaryBorderColor":"#a78bfa","tertiaryColor":"#1c1c21","tertiaryTextColor":"#b6b6c0","tertiaryBorderColor":"#56c7f2","lineColor":"#8c8c96","textColor":"#f5f5f7","mainBkg":"#2c2c33","nodeBorder":"#303037","clusterBkg":"#1c1c21","clusterBorder":"#303037","titleColor":"#b6b6c0","edgeLabelBackground":"#24242a","edgeLabelTextColor":"#f5f5f7","nodeTextColor":"#f5f5f7","rectBorderRadius":"10px","clusterBorderRadius":"12px"},"flowchart":{"htmlLabels":false,"curve":"basis","padding":20,"nodeSpacing":55,"rankSpacing":60,"diagramPadding":12,"useMaxWidth":false}}}%%
flowchart TD
PK["Passkey assertion<br/>(WebAuthn PRF output)"] --> HKDF["HKDF-SHA256"]
HKDF -->|"info: wavewalletdk:seed:v1"| ENT["16-byte entropy"]
HKDF -->|"info: wavewalletdk:dbpw:v1"| DBP["wallet DB password<br/>(64-char hex)"]
ENT --> AZ["aezeed mnemonic<br/>(24 words)"]
AZ --> SEED["HD wallet seed"]
PW["User password"] --> DBP2["wallet DB password"]
MN["Generated / imported<br/>mnemonic"] --> SEED
SEED --> DB[("wallet database<br/>(OPFS / app sandbox)")]
DBP --> DB
DBP2 --> DB

Both paths converge on the same storage: the seed lives only inside the wallet database, encrypted under the wallet DB password and stored device-locally. On the web that is OPFS, the browser’s origin-private file system. On React Native it is a data directory inside the app sandbox: RuntimeConfig.dataDir, defaulting to the platform path reported by getDefaultDataDir(). Either way the database never leaves the device, and no key material ever reaches the operator or any server.

The passkey ceremony

Passkey wallets are built on the WebAuthn PRF extension: an authenticator holds a hidden pseudo-random function per credential, and evaluating it over a fixed input yields a stable 32-byte secret. The SDK always evaluates the PRF over the same input, SHA-256("wavewalletdk-passkey:v1") (exported from core as PASSKEY_PRF_SALT_HEX), so the same passkey produces the same secret on every device and every session. That determinism is the whole design: the PRF output is the wallet’s root secret.

On the web, registerPasskeyWallet(appName) creates the credential with authenticatorAttachment: 'platform', residentKey: 'required', and userVerification: 'required', meaning a discoverable, biometric-gated passkey in the platform authenticator (Face ID, Touch ID, Windows Hello, or a synced password manager). Some browsers do not return a PRF result from the creation ceremony itself, so registration falls back to an immediate assertion scoped to the just-created credential to read the PRF value reliably.

assertPasskeyPrf(credentialId?) runs the returning-session ceremony. With a stored credentialId the assertion is scoped to that one credential and the OS unlocks it directly; without one, the assertion is discoverable, so a passkey synced from another device can be offered even though this device has never seen the wallet. The credentialId is not a secret; persisting it in localStorage just skips the credential chooser on the next unlock.

On React Native, createNativePasskeyCeremony({ rpId }) supplies the same ceremony through the platform credential APIs: Credential Manager on Android and AuthenticationServices on iOS (iOS support is experimental and needs iOS 18 or newer). Because every platform evaluates the PRF over the same fixed input, the same passkey derives the same wallet everywhere: a wallet created in the browser opens in the app, and vice versa, provided both use the same relying-party domain.

Seed derivation

The wallet runtime (the Go SDK, compiled to WASM on the web and bundled as a native library on React Native) expands the PRF output with HKDF-SHA256 into two domain-separated secrets:

HKDF info Output Role
wavewalletdk:seed:v1 16 bytes aezeed entropy (the HD wallet seed)
wavewalletdk:dbpw:v1 32 bytes, hex-encoded wallet DB password

The entropy is wrapped into an aezeed cipher seed with a pinned version and a birthday pinned to the Bitcoin genesis date, and an empty seed passphrase. Pinning all three means the wallet is a pure function of the PRF output: nothing else needs to be stored or remembered for the keys to be reproducible.

Two invariants protect this contract:

  • The PRF input never changes. The evaluation input is fixed per namespace version. If a caller ever evaluated the PRF over a different input, the same passkey would silently derive a different seed (a different wallet), and the original funds would be unreachable until the correct input is used again.
  • Short PRF outputs are rejected. The SDK refuses PRF outputs under 32 bytes, so a platform bug or empty input cannot collapse the derivation into a low-entropy, attacker-reproducible seed.

The derived DB password plays the same role a typed password plays in a password wallet: it is the private passphrase that encrypts the wallet database’s key material at rest. Because it comes out of HKDF, it carries the full 32 bytes of entropy, far stronger than any human password. And because the derivation is deterministic, every device derives the same password without storing or syncing it, which is why a passkey wallet never shows a password prompt.

For a password wallet, the user’s typed password is used directly as the wallet database passphrase, and the seed comes from a mnemonic generated by the daemon (createWallet({ password })) or imported by the user (createWallet({ password, mnemonic })). An optional seed passphrase (seedPassphrase) can be layered on an imported mnemonic; passkey wallets always use an empty one.

What is stored where

Artifact Where Protected by
HD seed and derived keys Wallet database in OPFS (web) or the app-sandbox data directory (React Native) Encrypted under the wallet DB password (typed password, or HKDF-derived for passkey wallets)
Swap and daemon bookkeeping SQLite databases alongside the wallet database Origin or app-sandbox isolation (no key material)
Passkey private key Platform authenticator / passkey provider Device biometrics or PIN; provider sync encryption
credentialId, wallet-kind markers App-level storage (localStorage on web, by convention) Nothing (not secret)
Mnemonic Wherever the user wrote it down The user

Nothing above leaves the device except the passkey itself, which the platform’s passkey provider (iCloud Keychain, Google Password Manager, 1Password, and similar) may sync end-to-end encrypted across the user’s devices. The operator sees signed protocol messages, never keys.

Backup

A passkey wallet has two independent backup layers:

  1. The synced passkey. If the user’s passkey provider syncs credentials, the wallet is already recoverable on any device signed into that provider: the PRF travels with the credential, and the seed is derived from it on demand. This is the primary, zero-effort layer.
  2. The mnemonic. When a passkey wallet is created (or first imported on a device), the result includes a 24-word aezeed mnemonic. It decodes to the same entropy the passkey derives, so it recovers the same wallet without the passkey, on any domain, in any aezeed-compatible flow. Offer it to the user once, encourage writing it down, and do not persist it anywhere.

A password wallet has exactly one backup layer: the mnemonic. The password cannot be recovered or reset from anywhere, and without the mnemonic a forgotten password means the funds are gone once the local database is lost.

Recovery paths

Scenario Path What comes back
Same device, returning session Passkey assertion (scoped to the stored credentialId) + openWalletFromPasskey() / unlockWallet({ password }) Everything; the local database is intact
New device, synced passkey Discoverable passkey assertion + openWalletFromPasskey(). The wallet state is 'none', so the SDK derives the seed and imports it Keys immediately; the view of funds converges in the background
Lost passkey, has mnemonic createWallet({ password, mnemonic, recoverState: true }); the wallet continues as a password wallet Keys immediately; funds and history via server-assisted recovery
Lost passkey and mnemonic None Nothing; there is no custodial reset

Recovery restores the seed instantly; restoring the view of funds takes a scan. A mnemonic restore can opt into server-assisted recovery with recoverState: true: the daemon walks the seed’s addresses and queries the operator’s indexer to rebuild boarding outputs, VTXOs, and Lightning receive history, tracked in the background through the engine’s 'restoring' phase and snapshot.recovery. See Restore a wallet for the full flow. The passkey import path does not run that scan today, so an app that supports passkey recovery on a fresh device should treat the imported wallet’s view of funds as converging rather than immediate.

Security model

  • Spending requires the device. A passkey assertion demands user verification (biometric or PIN) on an unlocked device. Anyone who can pass that check can derive the seed; there is no second factor beyond the platform’s.
  • The database at rest is only as strong as its passphrase. For passkey wallets that is 32 bytes of HKDF output, effectively unbreakable offline. For password wallets it is the user’s password; encourage real passphrases.
  • The app is part of the trust boundary. Script running on the wallet’s web origin, or code running inside the mobile app’s process, can drive a ceremony (still gated by a user-verification prompt) and can read the PRF output once a legitimate ceremony completes. XSS on the wallet origin or a compromised dependency in the app bundle is therefore equivalent to key compromise; treat content-security policy and dependency hygiene as wallet security, not routine hygiene.
  • The mnemonic is the root of everything. Anyone holding the 24 words holds the wallet, independent of passkeys, devices, or domains.