]> git.ardis.lu - envpass/commitdiff
Move `exp` from query param into script injection
authorArdis Lu <ardisglu@gmail.com>
Mon, 28 Sep 2026 00:12:48 +0000 (17:12 -0700)
committerArdis Lu <ardisglu@gmail.com>
Mon, 28 Sep 2026 00:12:48 +0000 (17:12 -0700)
src/getPrf.html
src/getPrf.js

index 14b915bc72af136592ebee7608b4e31d712b57f9..90db0996f960412c6af82a35a33e1f1b95f79d13 100644 (file)
   <p>Create or select a passkey, or <button type="button" class="link">click here</button> if no prompt appeared. This window will expire in 2 minutes.</p>
 
   <script type="module">
-    const { challenge, exp } = Object.fromEntries(new URLSearchParams(location.search));
+    const { challenge } = Object.fromEntries(new URLSearchParams(location.search));
     history.replaceState(null, '', location.pathname);
 
     const p = document.querySelector('p');
 
-    const expiration = Number(exp) - Date.now();
+    const expiration = Number(envpass.exp) - Date.now();
     const timeoutId = setTimeout(() => p.textContent = 'This window has expired.', expiration);
 
     async function createPrf(signal) {
index 68c4874458146933fa78f73ca680906507d6d266..a507f8f2011e08394fe0cd93067240ecda964181 100644 (file)
@@ -69,9 +69,8 @@ export async function getPrf(options = {}) {
   const { promise: prfPromise, resolve: prfResolve } = /** @type {PromiseWithResolvers<Bytes32>}*/(Promise.withResolvers());
   const { promise: abortPromise, resolve: abortResolve } = /** @type {PromiseWithResolvers<null>}*/(Promise.withResolvers());
 
-  const html = await getHTML();
-
-  // Generate hashes of <style> and <script> contents for the CSP header
+  // Fetch and validate HTML template
+  let html = await getHTML();
   const encoder = new TextEncoder();
   const style = html.match(/<style.*>([\s\S]*)<\/style>/i)?.[1];
   const script = html.match(/<script.*>([\s\S]*)<\/script>/i)?.[1];
@@ -81,8 +80,6 @@ export async function getPrf(options = {}) {
   if (script === undefined) {
     throw new Error('Could not find <script> tag in HTML template file.');
   }
-  const styleHash = new Uint8Array(await crypto.subtle.digest('SHA-512', encoder.encode(style))).toBase64();
-  const scriptHash = new Uint8Array(await crypto.subtle.digest('SHA-512', encoder.encode(script))).toBase64();
 
   // Setup local HTTP server
   const server = http.createServer();
@@ -103,6 +100,15 @@ export async function getPrf(options = {}) {
   // Challenge is used to verify the client POST-ing to the server was instantiated from this code
   const challenge = crypto.getRandomValues(new Uint8Array(32)).toHex();
 
+  // Setup injection in the HTML template, including hashes of <style> and <script> contents for the CSP header
+  const injectedScript = `
+    const envpass = {
+      exp: ${timeoutExp}
+    }; ${script}`;
+  const styleHash = new Uint8Array(await crypto.subtle.digest('SHA-512', encoder.encode(style))).toBase64();
+  const scriptHash = new Uint8Array(await crypto.subtle.digest('SHA-512', encoder.encode(injectedScript))).toBase64();
+  html = html.replace(/(<script.*)>[\s\S]*<\/script>/i, `$1 integrity="sha512-${scriptHash}">${injectedScript}</script>`);
+
   server.on('request', (request, response) => {
     const { port } = /** @type {AddressInfo} */(server.address());
     // Return minimal client-side code to get a passkey prf value
@@ -192,7 +198,7 @@ export async function getPrf(options = {}) {
   server.listen(port, 'localhost');
   server.on('listening', () => {
     const { port } = /** @type {AddressInfo} */(server.address());
-    const url = `http://localhost:${port}?challenge=${challenge}&exp=${timeoutExp}`;
+    const url = `http://localhost:${port}?challenge=${challenge}`;
     urlResolve(url);
     if (onListening !== null) { onListening(url); }
   });
This page took 0.045408 seconds and 4 git commands to generate.