ZK Client Key Delivery
ZK client key delivery is DarkAuth’s extension for applications that need user-held encryption material in the browser. It is optional and configured per client. Standard OIDC clients can ignore it completely.
Problem it solves
Section titled “Problem it solves”Some applications encrypt user data client-side and need a key after login. A normal token endpoint response is not a good place for that key because the authorization server backend would have to store or return it. DarkAuth’s ZK flow keeps the JWE ciphertext in browser memory and the URL fragment instead.
Current v2 clients receive a Client App Key, or CAK. The CAK is derived in the DarkAuth user UI from the user’s account root material and the requesting client context. The OAuth client does not receive the account root key.
This flow requires authentication and key unlock. A user can be signed in but still key locked after federated OIDC login, auth-only passkey login, or any other login path that does not expose local key-unlock material to the browser.
End-to-end flow
Section titled “End-to-end flow”- The app generates an ephemeral P-256 ECDH key pair.
- The app encodes the public JWK as
zk_puband includes it in the authorization request. - DarkAuth accepts
zk_pubonly if the client is configured forfragment-jwe. - The user authenticates with OPAQUE in the DarkAuth user UI.
- Browser code unwraps the user’s account root key locally.
- Browser code derives the CAK for the requesting
client_idand organization context. - Browser code encrypts the CAK to
zk_pubas compact JWE. - Browser code sends only
zk_key_hashto/authorize/finalize. - DarkAuth issues an authorization code and stores the hash binding.
- The browser redirects to the app with
#darkauth_key_jwe=...in the fragment. - The app exchanges the authorization code at
/token. - The app verifies
base64url(SHA-256(darkauth_key_jwe)) === zk_key_hash. - The app verifies the JWE payload metadata before accepting the key.
v2 token fields
Section titled “v2 token fields”For v2 clients, the token response includes:
zk_key_hashzk_key_kind: "client_app_key"zk_key_version: "v2"
The fragment parameter is darkauth_key_jwe.
The compact JWE payload contains the CAK plus binding metadata: typ, version, sub, client_id, aud, optional org_id, request_id, state_hash, redirect_uri_hash, key_id, key_kind, iat, and exp.
The authorization response model also exposes the configured key delivery version and delivered key kind so the user UI can choose the v2 or legacy finalize behavior.
Migrating v1 clients
Section titled “Migrating v1 clients”Existing fragment-jwe clients are migrated as explicit legacy clients with:
key_delivery_version: "v1-drk"delivered_key_kind: "root_key"
Legacy clients keep the previous field names:
- Fragment parameter:
drk_jwe - Finalize field:
drk_hash - Token response field:
zk_drk_hash
Do not silently switch an existing encrypted app from v1 to v2. Existing ciphertext may have been created under keys derived from the delivered DRK. To migrate an app:
- Ship an app version that can read data with the legacy key and write with the new client app key.
- Update the DarkAuth client registration to
key_delivery_version: "v2"anddelivered_key_kind: "client_app_key". - Re-encrypt or rewrap app data under keys derived from the v2 CAK.
- Keep legacy read support until all active data has been migrated.
Legacy v1 flow
Section titled “Legacy v1 flow”The v1 flow is retained only for explicitly legacy clients:
- Browser code encrypts the legacy DRK to
zk_pubas compact JWE. - Browser code sends only
drk_hashto/authorize/finalize. - DarkAuth issues an authorization code and stores the hash binding.
- The browser redirects to the app with
#drk_jwe=...in the fragment. - The app exchanges the authorization code at
/token. - The app verifies
base64url(SHA-256(drk_jwe)) === zk_drk_hash. - The app decrypts the JWE with its ephemeral private key.
What the server sees
Section titled “What the server sees”The server sees the authorization request, zk_pub, zk_pub_kid, session key state, client key-delivery configuration, and either zk_key_hash or legacy drk_hash. It does not receive the plaintext account root key, plaintext CAK, or the fragment JWE in the designed flow. The token endpoint returns the hash so the client can verify the fragment.
Application responsibilities
Section titled “Application responsibilities”A ZK-enabled app must:
- Generate a new ephemeral key pair for each authorization request.
- Keep the private key in memory only until callback handling completes.
- Preserve the URL fragment long enough to read
darkauth_key_jweor legacydrk_jwe. - Verify the token response hash before decrypting.
- Verify JWE payload metadata before using a v2 CAK.
- Remove key fragments from the URL after processing.
- Clear the private key and delivered key on logout.
- Decide whether reloads should trigger a fresh authorization request.
The app must also handle a user returning from DarkAuth without a delivered key when the session could not be unlocked. Treat that as an authentication or recovery problem, not as permission to continue with stale local key material.
Security boundary
Section titled “Security boundary”This flow is designed for honest hosted-web operation. It reduces what the DarkAuth backend and database can learn. It does not protect against malicious JavaScript running in the DarkAuth origin or app origin, compromised browsers, compromised devices, or an application that intentionally exfiltrates the delivered CAK or legacy DRK.
Use it when your app has a real client-side encryption model and users benefit from avoiding backend key custody.