Skip to content

Key Management

The only long-term secret is a 32-byte master seed. Host identities, bag keys, and export signatures are derived from it inside the enclave. After unlock, that seed lives in process memory (and in the sync Worker). WebAuthn is used to bind or store the seed at rest — not to keep it out of the browser while the app is running.

Credentials are scoped to the page hostname (rpId). Every bind, unseal, and largeBlob I/O requires user verification (platform biometric / PIN, or security-key PIN + touch).

Mechanisms

PRF binding (daily unlock)

WebAuthn prf (CTAP2 hmac-secret) produces 32 bytes of IKM. The enclave domain-separates those bytes (diplomatic.bind.v1) and AEAD-seals under XSalsa20-Poly1305. The ciphertext plus salt and credential id sit in protocol IndexedDB. Without a successful prf evaluation, the blob is useless. Pairing uses a different KDF (diplomatic.qrpair.v1); see Pairing.

PRF credential create (inside Enclave) does not default authenticatorAttachment. Omitting it lets the UA offer platform passkeys, roaming keys, and third-party providers. Pass platform to restrict (iCloud Keychain, Google Password Manager, Windows Hello). The platform vendor may sync that passkey — and therefore the ability to evaluate PRF — with the user’s account. That is accepted: the OS or browser already sees the unlocked seed in memory.

Create must report prf.enabled. Eval must return 32 bytes. Otherwise seal/unseal fails closed (WebAuthnError / MissingBody). There is no passphrase-seal fallback yet.

App API: Enclave.sealWithPasskey / unsealWithPasskey, PrfSeedStore. Probe: prfCapable() (browser advertises the extension; not a guarantee the binding key has hmac-secret).

A device keeps a local keyring: a flat list of typed bindings (type: "prf" now; shard later). Each row is one binding key: seal, cred id, nick, enrollment time, last used, attachment / transports / AAGUID / enrolled OS. There is no synced primary — daily unlock is whichever cred asserts. bindAndSave appends or upserts by cred id (create unless a cred id is passed). Unlock puts every stored cred id in allowCredentials. list() returns public rows (no ciphertext). Rename and remove are local; remove does not delete the binding-key cred. Cap is KEYRING_MAX (8).

The keyring is on-device only. A later optional sync would union by cred id and is add-only on the host (bags cannot be guaranteed deleted). Do not describe remove as revoke.

largeBlob (offline backup)

WebAuthn largeBlob stores an IdentityBundle (32-byte seed, then host rows) on the binding key. The write is not a PRF binding. UV and rpId are the gate. This is not a confidentiality boundary against the OS or against anyone who can complete UV on a stolen key.

Create requires largeBlob.support = "required". Attachment is not defaulted — pass cross-platform for a security key. Platform passkeys generally do not implement largeBlob.

App API: Enclave.persistToLargeBlob / fromLargeBlob / clearLargeBlob. Probe: largeBlobCapable().

Pairing (QR)

In-person seed transfer to a device that does not share a passkey. See Pairing for the request/response flow, threat model, and why the X25519 scalar is ours (getRandomValues) rather than subtle.generateKey.

App API: Enclave.pairRequest / PairRequest, enclave.pairAccept, then sealWithPasskey on the enrollee.

Raw import

Enclave.fromBytes (web hex paste, CLI DIP_SEED). No WebAuthn. Session-only unless the app then binds with PRF or writes largeBlob.

Platform support

Tables are current as of August 2026. Always test the target browser; prf.enabled / largeBlob.supported are the source of truth.

Platform passkeys (PRF)

EnvironmentPRFNotes
macOS 15+ Safari 18+, ChromeyesiCloud Keychain
iOS / iPadOS 18+ SafariyesiCloud Keychain
Android ChromeyesGoogle Password Manager
GrapheneOS VanadiumUSB key / GPM onlyNo GPM PRF without Play. extension:prf / extension:largeBlob are Chromium client flags. Discoverable get() + largeBlob.read is aborted by Android Credential Manager (no USB picker) — restore picks the key first, then reads. A security-key provider may still be required (Play FIDO or hw-fido2-provider).
Windows 11 + Chrome/Edge 147+, Firefox 148+yesWindows Hello

Platform passkeys do not provide largeBlob. Use a roaming key for IdentityBundle backup.

Roaming binding keys (YubiKey and similar)

The token must implement the CTAP extension and the OS/browser must pass extension data to it.

EnvironmentPRFlargeBlob
Windows 11 Chrome, Edge, Firefox (USB)yesyes
macOS Chrome (USB)yesyes
macOS Safarinono
Android Chrome USByesyes
Android Chrome NFCnono
iOS / iPadOS (any browser)nono

iOS/iPadOS does not pass WebAuthn extension I/O to external keys. Chrome on iOS is WebKit, so the same limit applies. On those devices, use the platform passkey for PRF; keep the YubiKey for desktop backup.

Which YubiKeys

FeatureHardware
hmac-secret / PRFYubiKey 5 Series and Bio (firmware 5.2+); Security Key Series (FIDO2)
largeBlobFirmware 5.5+ (1 KiB); 5.7+ (4 KiB). Newer Security Key Series (5.7+) included.

Firmware is printed in Yubico Authenticator. Keys cannot be upgraded. Prefer 5.7+ if the bundle will carry many host rows.

Other CTAP2.1 keys with hmac-secret and/or largeBlob work the same way when the platform forwards extensions. We do not require attestation. Keyring labels (Apple / GPM / YubiKey / …) are inferred from AAGUID, attachment, transports, and enrollment OS — display only.

Choosing a path

GoalUse
Unlock this browser next visitPRF binding → protocol IDB
Move identity to another device in personQR pair (pairRequest / pairAccept), then PRF binding on the new device
Survive a wiped profile / new machine without cloud passkeyslargeBlob on a YubiKey (desktop), then optional PRF bind
CLI / testsRaw seed

A typical app (see LIFE): platform PRF for daily unlock; optional cross-platform largeBlob write as a YubiKey backup; QR pair for a second device. On iPhone, only the platform PRF path is available after the pair.