Last active
July 31, 2025 07:35
-
-
Save ardislu/98564ae85254854ba78b704fe1679ebc to your computer and use it in GitHub Desktop.
Minimal boilerplate JavaScript code to use the pseudo-random function extension (prf) of the Web Authentication API.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * =================================================== IMPORTANT =================================================== | |
| * | |
| * The passkey creation and retrieval MUST occur in the same "effective domain" or "registrable domain suffix" | |
| * ([more info](https://www.w3.org/TR/webauthn-3/#rp-id)): | |
| * | |
| * > For example, given a Relying Party whose origin is `https://login.example.com:1337`, then the following | |
| * > RP IDs are valid: `login.example.com` (default) and `example.com`, but not `m.login.example.com` and not `com`. | |
| * | |
| * As a result of this restriction, data encrypted with a crypto key that was derived from a passkey's PRF extension | |
| * is tied to a specific domain, and **can only be decrypted on the same domain**. Only perform long-term encryption | |
| * if you expect the domain to be online for a long time as well. | |
| * | |
| * ================================================================================================================= | |
| */ | |
| /** | |
| * Create a new resident passkey and get a pseudo-random value produced from the passkey PRF extension. | |
| * | |
| * If the passkey does not return a PRF value after creation, try to sign in immediately after passkey creation | |
| * and get the PRF value from the sign in flow. | |
| * @see {@link https://www.w3.org/TR/webauthn-3/#prf-extension} | |
| * @returns {Promise<ArrayBuffer&{byteLength:32}>} A 32 byte long `ArrayBuffer` containing a pseudo-random | |
| * value produced from the passkey PRF extension. | |
| */ | |
| async function createPrf() { | |
| return navigator.credentials.create({ | |
| publicKey: { | |
| rp: { name: '' }, | |
| user: { id: new ArrayBuffer(1), name: crypto.randomUUID(), displayName: '' }, // Windows Hello requires non-empty id; Yubikey requires non-empty name | |
| pubKeyCredParams: [{ type: 'public-key', alg: -8 }, { type: 'public-key', alg: -7 }, { type: 'public-key', alg: -257 }], | |
| extensions: { prf: { eval: { first: new ArrayBuffer(0) } } }, | |
| challenge: new ArrayBuffer(0), | |
| authenticatorSelection: { residentKey: 'required' } // Resident key is required because there is no server to store c.rawId | |
| } | |
| }).then(c => c.getClientExtensionResults().prf?.results?.first ?? getPrf()); // Yubikey does not return PRF on creation, workaround is sign in immediately after creation | |
| } | |
| /** | |
| * Request a "sign in" with a passkey created previously, if it exists. This function will return the same | |
| * pseudo-random value produced during the passkey creation function. | |
| * @see {@link https://www.w3.org/TR/webauthn-3/#prf-extension} | |
| * @returns {Promise<ArrayBuffer&{byteLength:32}>} A 32 byte long `ArrayBuffer` containing a pseudo-random | |
| * value produced from the passkey PRF extension. | |
| */ | |
| async function getPrf() { | |
| return navigator.credentials.get({ | |
| publicKey: { | |
| extensions: { prf: { eval: { first: new ArrayBuffer(0) } } }, | |
| challenge: new ArrayBuffer(0) // Challenge is not used in this context | |
| } | |
| }).then(c => c.getClientExtensionResults().prf.results.first); | |
| } | |
| // Usage example: | |
| const prf1 = await createPrf(); // Will prompt user to create a new passkey | |
| const prf2 = await getPrf(); // Will prompt user to "sign in" with passkey created above | |
| // The values are exactly equal: | |
| const a1 = [...new BigUint64Array(prf1)]; | |
| const a2 = [...new BigUint64Array(prf2)]; | |
| console.log(a1.every((n, i) => n === a2[i])); | |
| // true | |
| // You can use the prf value for crypto: | |
| const keyMaterial = await crypto.subtle.importKey( | |
| 'raw', | |
| prf1, | |
| 'HKDF', | |
| false, | |
| ['deriveKey'] | |
| ); | |
| // Then pass to `crypto.subtle.deriveKey` and `crypto.subtle.encrypt`, etc... | |
| // See https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto | |
| // To signal that this file is a JS module which can use top-level await | |
| export { }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment