From 637884b72f71d7ac0196377b71f67da153bc4b25 Mon Sep 17 00:00:00 2001 From: Ardis Lu Date: Sun, 20 Jul 2025 20:47:34 -0700 Subject: [PATCH] Remove challenge Challenge does not make sense in this context because both passkey creation and retrieval are entirely client-side. The signed challenge is also not verified, and so challenge is actually unused and could be replaced by any junk value in the output content without affecting the decryption. --- script.js | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/script.js b/script.js index a5ccb54..ac9b13f 100644 --- a/script.js +++ b/script.js @@ -1,18 +1,17 @@ /** * Create a new passkey and get a pseudo-random value produced from the passkey PRF extension. * @see {@link https://www.w3.org/TR/webauthn-3/#prf-extension} - * @param {ArrayBuffer|ArrayBufferView} challenge A cryptographic challenge for the passkey. * @returns {Promise} A 32 byte long `ArrayBuffer` containing a pseudo-random * value produced from the passkey PRF extension. */ -async function createPrf(challenge) { +async function createPrf() { return navigator.credentials.create({ publicKey: { rp: { name: '' }, user: { id: new ArrayBuffer(0), name: '', displayName: '' }, pubKeyCredParams: [{ type: 'public-key', alg: -7 }], extensions: { prf: { eval: { first: new ArrayBuffer(0) } } }, - challenge + challenge: new ArrayBuffer(0) } }).then(c => c.getClientExtensionResults().prf.results.first); } @@ -21,15 +20,14 @@ async function createPrf(challenge) { * 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} - * @param {ArrayBuffer|ArrayBufferView} challenge A cryptographic challenge for the passkey. * @returns {Promise} A 32 byte long `ArrayBuffer` containing a pseudo-random * value produced from the passkey PRF extension. */ -async function getPrf(challenge) { +async function getPrf() { return navigator.credentials.get({ publicKey: { extensions: { prf: { eval: { first: new ArrayBuffer(0) } } }, - challenge + challenge: new ArrayBuffer(0) } }).then(c => c.getClientExtensionResults().prf.results.first); } @@ -68,15 +66,14 @@ async function getKey(rand, salt, info) { /** * Encrypt a `string` by creating a new passkey then using the passkey's PRF extension to generate a - * HKDF key and encrypt the `string`. The `challenge`, `salt`, and `iv` values are embedded (in plaintext) + * HKDF key and encrypt the `string`. The `salt` and `iv` values are embedded (in plaintext) * alongside the ciphertext. * @param {string} plaintext An unencrypted value to encrypt using a new passkey. - * @returns {Promise} The encrypted plaintext (i.e., ciphertext) with `challenge`, `salt`, and - * `iv` values in base64. + * @returns {Promise} The encrypted plaintext (i.e., ciphertext) with `salt` and `iv` values + * in base64. */ export async function encrypt(plaintext) { // Generate all required random values - const challenge = crypto.getRandomValues(new Uint8Array(64)); // For passkey const salt = crypto.getRandomValues(new Uint8Array(64)); // For HKDF const iv = crypto.getRandomValues(new Uint8Array(12)); // For AES-GCM @@ -85,7 +82,7 @@ export async function encrypt(plaintext) { const encodedPlaintext = encoder.encode(plaintext); const info = encoder.encode('https://github.com/ardislu/static-encrypt-passkey'); - const prf = await createPrf(challenge); + const prf = await createPrf(); const key = await getKey(prf, salt, info); const ciphertext = new Uint8Array(await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, @@ -93,15 +90,15 @@ export async function encrypt(plaintext) { encodedPlaintext )); - const buffer = Uint8Array.from([...challenge, ...salt, ...iv, ...ciphertext]); + const buffer = Uint8Array.from([...salt, ...iv, ...ciphertext]); const content = btoa(String.fromCharCode(...buffer)); return content; } /** - * Decrypt a `string` that has been encrypted with a passkey. Assuming the required `challenge`, `salt`, and - * `iv` values are embedded alongside the ciphertext. + * Decrypt a `string` that has been encrypted with a passkey. Assuming the required `salt` and `iv` + * values are embedded alongside the ciphertext. * @param {string} content A base64-encoded ciphertext and required cryptographic values. * @returns {Promise} The decrypted plaintext. * @throws {OperationError} Decryption operation failed. @@ -109,15 +106,14 @@ export async function encrypt(plaintext) { export async function decrypt(content) { // Extract random values const buffer = Uint8Array.from(atob(content), c => c.charCodeAt(0)); - const challenge = buffer.slice(0, 64); // For passkey - const salt = buffer.slice(64, 128); // For HKDF - const iv = buffer.slice(128, 140); // For AES-GCM - const ciphertext = buffer.slice(140); + const salt = buffer.slice(0, 64); // For HKDF + const iv = buffer.slice(64, 76); // For AES-GCM + const ciphertext = buffer.slice(76); // Prepare non-crypto inputs const info = new TextEncoder().encode('https://github.com/ardislu/static-encrypt-passkey'); - const prf = await getPrf(challenge); + const prf = await getPrf(); const key = await getKey(prf, salt, info); const encodedPlaintext = new Uint8Array(await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, -- 2.47.3