Ease docs/Concepts/Passkey
Passkey: WebAuthn PRF-derived secrets
A passkey can do more than log a user in. The WebAuthn PRF extension turns a passkey into a source of stable, high-entropy secrets. The same passkey plus the same audience always yields the same 32 bytes, on every device the passkey syncs to. ease.passkey exposes that as a building block for client-side key wrapping.
Two surfaces, kept separate
The word "passkey" covers two distinct surfaces in the SDK. Keep them apart.
Passkey auth lives on ease.auth: registerWithPasskey(handle, otp, { name }) and loginWithPasskey(handle). The handle is an email or phone, routed by the tenant's auth method (email_passkey or phone_passkey). This is how the user gets in.
The passkey primitive lives on ease.passkey: a cryptographic building block the user can call once they're in. That is what this page is about.
deriveSecret(audience): derive a deterministic 32-byte secret from the signed-in user's passkey, scoped toaudience. Triggers a WebAuthn gesture on every call; returns aUint8Array.prfAvailable(): whether this browser supports the PRF extension. Returns abooleanand never prompts.
What PRF is
PRF is the WebAuthn PRF extension: a passkey deriving stable, audience-scoped secrets. The same passkey and the same audience always produce the same 32 bytes, on every device the passkey syncs to. The SDK uses it for client-side key wrapping: keystore encryption today, encrypted backups and future signing as they land.
On passkey tenants the libsignal keystore key is itself the passkey's PRF output: 256 bits of hardware entropy, with no user-chosen component. The stored bytes are not brute-forceable offline.
Deriving a secret
deriveSecret triggers a WebAuthn assertion (a user gesture, like a Touch ID prompt) and returns 32 bytes. Use them to wrap (encrypt) a key your app controls; never store the derived secret itself.
// Encrypt something with a passkey-derived key.
const secret = await ease.passkey.deriveSecret('tenant:note-vault');
const key = await crypto.subtle.importKey(
'raw', secret, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'],
);
// …encrypt your data with `key`. Re-derive the same `secret`
// on any device to decrypt — no key escrow, no server.Because the secret is re-derivable from the passkey, a synced passkey (iCloud Keychain, Google Password Manager) carries the wrapping key to every device the user owns, without Ease or your backend ever holding it.
Audiences
The audience argument names the purpose of a secret. Two distinct audiences always produce two unrelated secrets. The SDK domain-separates them with HKDF before the passkey is ever called, so collision resistance does not depend on how you spell the string.
Prefix your own audiences with tenant: (tenant:note-vault, tenant:backup). The prefix is a naming convention, not a security boundary (domain separation holds either way), but it keeps your audiences clear of the names the SDK reserves for itself. One reserved name is enforced today: keystore, which the SDK uses to encrypt its own message keystore. deriveSecret("keystore") throws passkey_audience_reserved. Stay under the tenant: prefix and you never have to track the reserved list.
When PRF isn't available
PRF is supported on Chrome 117+, Safari 18+, and a current Firefox. That is broad, but not universal. Check before you surface a PRF-dependent feature:
if (await ease.passkey.prfAvailable()) {
// show the encrypted-backup option
}deriveSecret throws passkey_prf_unavailable on a browser without PRF. Catch it and fall back (skip the feature, prompt a different device). A false from prfAvailable() is conservative: it means "not confirmed", not necessarily "absent". The definitive check is an actual ceremony.
Auth still works without PRF. A browser without PRF can still complete passkey login. The user is signed in, only PRF-derived features degrade. The one hard requirement is passkey registration on web: the SDK needs PRF to encrypt the message keystore, so registerWithPasskey fails fast with passkey_prf_unavailable rather than producing an account that can never message.
Native SDKs ship passkey auth without PRF
The PRF primitive is web-only. On Apple and Android the operating system keystore already protects key material, so those SDKs ship passkey auth without exposing PRF. ease.passkey is a web surface; native applications wrap keys through the platform keystore instead.