]> git.ardis.lu - static-encrypt-passkey/commitdiff
Remove challenge
authorArdis Lu <ardisglu@gmail.com>
Mon, 21 Jul 2025 03:47:34 +0000 (20:47 -0700)
committerArdis Lu <ardisglu@gmail.com>
Mon, 21 Jul 2025 03:47:34 +0000 (20:47 -0700)
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

index a5ccb5433f7516ad94a4538d7c717bff7e1bc399..ac9b13fc7b87638014d3b25f88a5e9f6f904bc6f 100644 (file)
--- 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<ArrayBufferLike>} challenge A cryptographic challenge for the passkey.
  * @returns {Promise<ArrayBuffer&{byteLength:32}>} 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<ArrayBufferLike>} challenge A cryptographic challenge for the passkey.
  * @returns {Promise<ArrayBuffer&{byteLength:32}>} 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<string>} The encrypted plaintext (i.e., ciphertext) with `challenge`, `salt`, and
- * `iv` values in base64.
+ * @returns {Promise<string>} 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<string>} 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 },
This page took 0.042139 seconds and 4 git commands to generate.