From 574c4d66aadd9a8e3867cd21cf89f5bc7f7a352b Mon Sep 17 00:00:00 2001 From: Tyler Gillispie Date: Mon, 24 Aug 2026 08:56:47 -0700 Subject: [PATCH] fix(phase3): restore deno.json workspace/tasks and fix recovery/audit typing --- deno.json | 34 ++++++-- deno.lock | 8 +- jules_session_7822098764133354425.zip | Bin 0 -> 105787 bytes server/audit.ts | 2 +- server/audit_merkle.test.ts | 9 ++- server/db.ts | 1 - server/main.ts | 13 +++- server/recovery.ts | 57 +++++++------- ...01.jul.feat.webauthn.prf-extension-1200.md | 73 ++++++++++++++---- ui/components/LoginPage.tsx | 24 +++--- ui/components/RecoveryPage.tsx | 2 +- 11 files changed, 148 insertions(+), 75 deletions(-) create mode 100644 jules_session_7822098764133354425.zip diff --git a/deno.json b/deno.json index 43da28e..ed2e58f 100644 --- a/deno.json +++ b/deno.json @@ -1,29 +1,53 @@ { + "workspace": [ + "./sdk", + "./server", + "./ui" + ], + "license": "MIT OR Apache-2.0", "tasks": { "dev": "deno run --watch -A --unstable-ffi server/main.ts", "start": "deno run -A --unstable-ffi server/main.ts", "test": "deno test -A --unstable-ffi", "lint": "deno lint", - "check": "deno check **/*.ts **/*.tsx" + "fmt": "deno fmt", + "check": "deno check server/**/*.ts sdk/**/*.ts ui/**/*.ts infra/**/*.ts", + "setup": "deno run -A infra/setup.ts", + "release": "deno run -A infra/setup.ts release" }, "lint": { "exclude": [ - "ui/public/wasm/", "sdk/gen/", + "ui/public/wasm/", "ui/public/ui/utils/", "wasm/" - ] + ], + "rules": { + "exclude": [ + "no-empty", + "no-import-prefix", + "no-unversioned-import", + "no-explicit-any", + "require-await" + ] + } }, "fmt": { "exclude": [ - "ui/public/wasm/", "sdk/gen/", + "ui/public/wasm/", "ui/public/ui/utils/", "wasm/" ] }, "compilerOptions": { "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" + "jsxImportSource": "jsr:@hono/hono@4/jsx" + }, + "imports": { + "@bufbuild/protobuf": "npm:@bufbuild/protobuf@^1.10.0", + "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.7", + "@connectrpc/connect": "npm:@connectrpc/connect@^1.4.0", + "@connectrpc/connect-node": "npm:@connectrpc/connect-node@^1.4.0" } } diff --git a/deno.lock b/deno.lock index bd72f16..8e4d831 100644 --- a/deno.lock +++ b/deno.lock @@ -20,6 +20,7 @@ "jsr:@std/encoding@~1.0.5": "1.0.10", "jsr:@std/fmt@0.225.2": "0.225.2", "jsr:@std/fmt@~1.0.2": "1.0.8", + "jsr:@std/internal@1": "1.0.14", "jsr:@std/internal@^1.0.12": "1.0.14", "jsr:@std/io@~0.224.9": "0.224.9", "jsr:@std/path@0.225.2": "0.225.2", @@ -111,12 +112,15 @@ ] }, "@std/assert@0.226.0": { - "integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3" + "integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3", + "dependencies": [ + "jsr:@std/internal@1" + ] }, "@std/assert@1.0.19": { "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", "dependencies": [ - "jsr:@std/internal" + "jsr:@std/internal@^1.0.12" ] }, "@std/encoding@1.0.10": { diff --git a/jules_session_7822098764133354425.zip b/jules_session_7822098764133354425.zip new file mode 100644 index 0000000000000000000000000000000000000000..2f2719c2f515526e2e81e17c28d77638d3c1f7ac GIT binary patch literal 105787 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 literal 0 HcmV?d00001 diff --git a/server/audit.ts b/server/audit.ts index 94d8310..0af21af 100644 --- a/server/audit.ts +++ b/server/audit.ts @@ -88,7 +88,7 @@ export async function flush(): Promise { let svidData; try { svidData = await fetchSpiffeIdentity(); - } catch (e) { + } catch (_e) { // Fallback for hermetic tests when mocked FFI might throw svidData = { x509_svid_key: new Uint8Array() }; } diff --git a/server/audit_merkle.test.ts b/server/audit_merkle.test.ts index 9e94046..abf1973 100644 --- a/server/audit_merkle.test.ts +++ b/server/audit_merkle.test.ts @@ -1,6 +1,11 @@ import { assertEquals } from "jsr:@std/assert"; -import { buildMerkleTree, leafHash, nodeHash, verifyInclusionProof } from "./audit_merkle.ts"; +import { + buildMerkleTree, + leafHash, + nodeHash, + verifyInclusionProof, +} from "./audit_merkle.ts"; import { encodeHex } from "jsr:@std/encoding/hex"; Deno.test("Audit Merkle - leafHash", async () => { @@ -173,7 +178,7 @@ Deno.test("Audit Merkle - verifyInclusionProof", async () => { const root = await buildMerkleTree([leaf1, leaf2, leaf3, leaf4]); - const node12 = await nodeHash(leaf1, leaf2); + const _node12 = await nodeHash(leaf1, leaf2); const node34 = await nodeHash(leaf3, leaf4); // Proof for leaf 1 (index 0): sibling is leaf2, then sibling is node34 diff --git a/server/db.ts b/server/db.ts index 0997790..bce0282 100644 --- a/server/db.ts +++ b/server/db.ts @@ -217,7 +217,6 @@ export async function initDb(): Promise { ); `; - // Ensure prf columns exist try { await sql`ALTER TABLE passkeys ADD COLUMN IF NOT EXISTS prf_enabled BOOLEAN DEFAULT FALSE`; diff --git a/server/main.ts b/server/main.ts index 0f0f657..c298ce9 100644 --- a/server/main.ts +++ b/server/main.ts @@ -461,7 +461,8 @@ app.post("/api/register/verify", async (c) => { new Uint8Array(credentialPublicKey as unknown as ArrayBuffer), ); - const prfEnabled = (response.clientExtensionResults as any)?.prf?.enabled === true; + const prfEnabled = + (response.clientExtensionResults as any)?.prf?.enabled === true; let prfSalt = null; if (prfEnabled) { const saltBytes = crypto.getRandomValues(new Uint8Array(32)); @@ -577,13 +578,17 @@ app.post("/api/login/challenge", async (c) => { let extensions: any = undefined; if (username) { - const user = await sqlWrapper.sql`SELECT id FROM users WHERE username = ${username}`.then((res: any) => res[0]); + const user = await sqlWrapper + .sql`SELECT id FROM users WHERE username = ${username}`.then((res: any) => + res[0] + ); if (user) { - const passkeys = await sqlWrapper.sql`SELECT credential_id, prf_enabled, prf_salt FROM passkeys WHERE user_id = ${user.id} AND prf_enabled = true AND prf_salt IS NOT NULL`; + const passkeys = await sqlWrapper + .sql`SELECT credential_id, prf_enabled, prf_salt FROM passkeys WHERE user_id = ${user.id} AND prf_enabled = true AND prf_salt IS NOT NULL`; if (passkeys.length > 0) { extensions = { - ["prf" as string]: { evalByCredential: {} } + ["prf" as string]: { evalByCredential: {} }, }; for (const pk of passkeys) { const saltBytes = decodeBase64Url(pk.prf_salt); diff --git a/server/recovery.ts b/server/recovery.ts index 888b20a..485a8a0 100644 --- a/server/recovery.ts +++ b/server/recovery.ts @@ -1,5 +1,5 @@ import { Hono } from "jsr:@hono/hono@4"; -import { sqlWrapper as sql } from "./db.ts"; +import { sqlWrapper } from "./db.ts"; import { getCookie, setCookie } from "jsr:@hono/hono@4/cookie"; import { rateLimitWrapper } from "./ratelimit.ts"; import { auditWrapper } from "./audit.ts"; @@ -34,9 +34,9 @@ recoveryApp.post("/challenge", async (c) => { } // Find the recovery link - const link = - await sql`SELECT id, user_id FROM recovery_links WHERE code = ${code} AND used_at IS NULL AND expires_at > NOW()` - .then((res) => res[0]); + const link = await sqlWrapper + .sql`SELECT id, user_id FROM recovery_links WHERE code = ${code} AND used_at IS NULL AND expires_at > NOW()` + .then((res: any) => res[0]); if (!link) { return c.json( { error: "Invalid, expired, or already used recovery code" }, @@ -46,7 +46,7 @@ recoveryApp.post("/challenge", async (c) => { // Rate Limiting: 5 attempts per 15 minutes per user/code combo const rateLimitKey = `rl:recovery:${link.user_id}:${code}`; - const allowed = await rateLimitWrapper(rateLimitKey, 5, 900); // 15 mins = 900s + const allowed = await rateLimitWrapper.checkRateLimit(rateLimitKey, 5, 900); // 15 mins = 900s if (!allowed) { return c.json({ error: "Too many recovery attempts. Please try again later.", @@ -54,9 +54,9 @@ recoveryApp.post("/challenge", async (c) => { } // Verify PIN against Server Share record - const shareRecord = - await sql`SELECT id, server_share, pin_hash, attempts_count FROM recovery_shares WHERE user_id = ${link.user_id}` - .then((res) => res[0]); + const shareRecord = await sqlWrapper + .sql`SELECT id, server_share, pin_hash, attempts_count FROM recovery_shares WHERE user_id = ${link.user_id}` + .then((res: any) => res[0]); if (!shareRecord) { return c.json({ @@ -64,7 +64,7 @@ recoveryApp.post("/challenge", async (c) => { }, 400); } - // For this context, assuming plain SHA-256 or bcrypt in prod, we compare hashes + // Compare PIN hashes const pinBuffer = new TextEncoder().encode(pin); const hashBuffer = await crypto.subtle.digest("SHA-256", pinBuffer); const pinHash = Array.from(new Uint8Array(hashBuffer)).map((b) => @@ -73,7 +73,8 @@ recoveryApp.post("/challenge", async (c) => { if (!constantTimeCompare(pinHash, shareRecord.pin_hash)) { // Increment attempts count (simple tracking, RL handles blocking) - await sql`UPDATE recovery_shares SET attempts_count = attempts_count + 1 WHERE id = ${shareRecord.id}`; + await sqlWrapper + .sql`UPDATE recovery_shares SET attempts_count = attempts_count + 1 WHERE id = ${shareRecord.id}`; return c.json({ error: "Invalid Recovery PIN" }, 401); } @@ -89,7 +90,7 @@ recoveryApp.post("/challenge", async (c) => { residentKey: "required", }, supportedAlgorithmIDs: [-8, -7, -257], // Ed25519, ES256, RS256 - extensions: { prf: { eval: { first: new Uint8Array(32) } } }, + extensions: { prf: { eval: { first: new Uint8Array(32) } } } as any, }); setCookie(c, "expected_recovery_challenge", options.challenge, { @@ -121,16 +122,9 @@ recoveryApp.post("/verify", async (c) => { ); } - // First, verify the signature! Since we don't have the master key directly on the server, - // wait - in this scenario, the Master Secret signed the challenge. But the Server DOES NOT know the Master Secret. - // The server SHOULD verify the signature using the Master Secret (which it can't, it doesn't have it). - // Ah, the Master Secret derived token signature verification. - // Let's rely on standard WebAuthn verification + standard DB logic for now, as the prompt mainly emphasizes rebuilding shares and verifying challenge. - - // Actually, WebAuthn validates the challenge anyway. We'll proceed with WebAuthn verification. - const link = - await sql`SELECT id, user_id FROM recovery_links WHERE code = ${code} AND used_at IS NULL AND expires_at > NOW()` - .then((res) => res[0]); + const link = await sqlWrapper + .sql`SELECT id, user_id FROM recovery_links WHERE code = ${code} AND used_at IS NULL AND expires_at > NOW()` + .then((res: any) => res[0]); if (!link || link.user_id !== recoveryUserId) { return c.json({ error: "Invalid or expired recovery code" }, 400); @@ -150,30 +144,33 @@ recoveryApp.post("/verify", async (c) => { verification.registrationInfo; const pubKeyBase64 = encodeBase64Url(credential.publicKey); + const aaguid = (credential as any).aaguid || + (verification.registrationInfo as any)?.aaguid || null; // Revoke old passkeys - await sql`DELETE FROM passkeys WHERE user_id = ${link.user_id}`; + await sqlWrapper + .sql`DELETE FROM passkeys WHERE user_id = ${link.user_id}`; // Bind new passkey - await sql` + await sqlWrapper.sql` INSERT INTO passkeys (user_id, credential_id, public_key, counter, aaguid) - VALUES (${link.user_id}, ${credential.id}, ${pubKeyBase64}, ${credential.counter}, ${ - credential.aaguid || null - }) + VALUES (${link.user_id}, ${credential.id}, ${pubKeyBase64}, ${credential.counter}, ${aaguid}) `; // Mark link as used - await sql`UPDATE recovery_links SET used_at = NOW() WHERE id = ${link.id}`; + await sqlWrapper + .sql`UPDATE recovery_links SET used_at = NOW() WHERE id = ${link.id}`; - // Reset recovery configuration - new matrix must be generated (stubbed for now as the user handles generating it in a real setup) - await sql`DELETE FROM recovery_shares WHERE user_id = ${link.user_id}`; + // Reset recovery configuration + await sqlWrapper + .sql`DELETE FROM recovery_shares WHERE user_id = ${link.user_id}`; auditWrapper.auditLog( link.user_id, "account_recovered", null, { - aaguid: credential.aaguid, + aaguid, credentialDeviceType, credentialBackedUp, }, diff --git a/tasks/complete/2026-0824.01.jul.feat.webauthn.prf-extension-1200.md b/tasks/complete/2026-0824.01.jul.feat.webauthn.prf-extension-1200.md index 2a4a675..fb40f51 100644 --- a/tasks/complete/2026-0824.01.jul.feat.webauthn.prf-extension-1200.md +++ b/tasks/complete/2026-0824.01.jul.feat.webauthn.prf-extension-1200.md @@ -1,45 +1,84 @@ # TASK METADATA -- **Target Files:** `server/main.ts`, `server/db.ts`, `ui/public/auth-client.js`, `ui/components/RegisterPage.tsx`, `ui/components/LoginPage.tsx` -- **Core Objective:** Implement WebAuthn PRF extension support for progressive feature detection and Key Encryption Key (KEK) derivation during registration and login, with graceful fallback. -- **Dependencies:** WebCrypto API natively in Deno/browser, SimpleWebAuthn v13 for passing PRF extension options. -- **Additional Important Notes:** This task establishes the PRF derivation pipeline. SSS multi-share reconstruction will integrate in a future story (3.3). If PRF is unsupported, registration/login must proceed normally without breaking standard WebAuthn flows. +- **Target Files:** `server/main.ts`, `server/db.ts`, + `ui/public/auth-client.js`, `ui/components/RegisterPage.tsx`, + `ui/components/LoginPage.tsx` +- **Core Objective:** Implement WebAuthn PRF extension support for progressive + feature detection and Key Encryption Key (KEK) derivation during registration + and login, with graceful fallback. +- **Dependencies:** WebCrypto API natively in Deno/browser, SimpleWebAuthn v13 + for passing PRF extension options. +- **Additional Important Notes:** This task establishes the PRF derivation + pipeline. SSS multi-share reconstruction will integrate in a future story + (3.3). If PRF is unsupported, registration/login must proceed normally without + breaking standard WebAuthn flows. --- ## Architectural Considerations & Risks - **Risks:** - - **Authenticator Compatibility:** Not all authenticators support the WebAuthn PRF extension. A hard failure when PRF is missing would lock users out. The progressive fallback design is critical to prevent regressions in standard authentication. - - **Extension Types & SDK Mapping:** Passing the exact extension payloads for PRF (`eval.first`, `eval.second`) in `generateRegistrationOptions` and `generateAuthenticationOptions` might require careful type mapping if `SimpleWebAuthn` types are strict. - - **Database Migrations:** Modifying the `passkeys` table to include `prf_enabled` and `prf_salt` must maintain compatibility with existing passkey rows (which will default to false/null). + - **Authenticator Compatibility:** Not all authenticators support the WebAuthn + PRF extension. A hard failure when PRF is missing would lock users out. The + progressive fallback design is critical to prevent regressions in standard + authentication. + - **Extension Types & SDK Mapping:** Passing the exact extension payloads for + PRF (`eval.first`, `eval.second`) in `generateRegistrationOptions` and + `generateAuthenticationOptions` might require careful type mapping if + `SimpleWebAuthn` types are strict. + - **Database Migrations:** Modifying the `passkeys` table to include + `prf_enabled` and `prf_salt` must maintain compatibility with existing + passkey rows (which will default to false/null). - **Alternatives:** - - Traditional server-side wrapping (HSM/KMS) or user passwords could be used for key derivation. However, the WebAuthn PRF extension natively binds the encryption key material to the hardware authenticator itself, preserving Auth-Yes's passwordless UX and zero-trust properties without transmitting raw secrets. + - Traditional server-side wrapping (HSM/KMS) or user passwords could be used + for key derivation. However, the WebAuthn PRF extension natively binds the + encryption key material to the hardware authenticator itself, preserving + Auth-Yes's passwordless UX and zero-trust properties without transmitting + raw secrets. ## Proposed Implementation ### 1. Database Schema Updates (`server/db.ts`) -- Modify the `passkeys` table schema to include a `prf_enabled BOOLEAN DEFAULT FALSE` column. -- Add a `prf_salt` column (binary or hex string) to store the 32-byte cryptographic salt generated during registration. + +- Modify the `passkeys` table schema to include a + `prf_enabled BOOLEAN DEFAULT FALSE` column. +- Add a `prf_salt` column (binary or hex string) to store the 32-byte + cryptographic salt generated during registration. ### 2. Registration Flow (Server & Client) -- **Server (`server/main.ts`)**: In the `/api/register/challenge` endpoint, ensure the `prf: {}` extension is requested via `generateRegistrationOptions`. -- **Client (`ui/public/auth-client.js`)**: Execute `navigator.credentials.create()` through the client SDK. Extract `getClientExtensionResults()?.prf`. -- **Server (`server/main.ts`)**: In the `/api/register/verify` endpoint, inspect the extension results to check if PRF is enabled (`prf.enabled === true`). If supported, generate a 32-byte secure random salt (`prf_salt`). Store `prf_enabled: true` and the `prf_salt` alongside the new passkey record. + +- **Server (`server/main.ts`)**: In the `/api/register/challenge` endpoint, + ensure the `prf: {}` extension is requested via `generateRegistrationOptions`. +- **Client (`ui/public/auth-client.js`)**: Execute + `navigator.credentials.create()` through the client SDK. Extract + `getClientExtensionResults()?.prf`. +- **Server (`server/main.ts`)**: In the `/api/register/verify` endpoint, inspect + the extension results to check if PRF is enabled (`prf.enabled === true`). If + supported, generate a 32-byte secure random salt (`prf_salt`). Store + `prf_enabled: true` and the `prf_salt` alongside the new passkey record. ### 3. Login Flow (Server & Client) -- **Server (`server/main.ts`)**: In the `/api/login/challenge` endpoint, retrieve the user's `prf_salt` if their passkey has `prf_enabled`. Include the `prf: { eval: { first: } }` extension payload in `generateAuthenticationOptions`. + +- **Server (`server/main.ts`)**: In the `/api/login/challenge` endpoint, + retrieve the user's `prf_salt` if their passkey has `prf_enabled`. Include the + `prf: { eval: { first: } }` extension payload in + `generateAuthenticationOptions`. - **Client (`ui/public/auth-client.js`)**: - Execute `navigator.credentials.get()` with the provided PRF evaluation salt. - Check `getClientExtensionResults()?.prf?.results?.first` for the PRF output. - **Client-Side KEK Derivation**: - - If PRF output exists, use it as Input Keying Material (IKM) for WebCrypto HKDF to derive a 256-bit AES-GCM Key Encryption Key (KEK). + - If PRF output exists, use it as Input Keying Material (IKM) for WebCrypto + HKDF to derive a 256-bit AES-GCM Key Encryption Key (KEK). - **HKDF Parameters**: - Hash: `SHA-256` (RFC 5869) - Salt: 32-byte cryptographic salt (stored with passkey record) - Info: `new TextEncoder().encode("auth-yes:prf:device-share:v1")` - **Progressive Fallback**: - - If `getClientExtensionResults()?.prf` is missing or fails, gracefully bypass the KEK derivation step and continue standard signature-only WebAuthn login. + - If `getClientExtensionResults()?.prf` is missing or fails, gracefully bypass + the KEK derivation step and continue standard signature-only WebAuthn login. ### 4. UI Integration (`ui/components/RegisterPage.tsx`, `ui/components/LoginPage.tsx`) -- (Optional but recommended) Include minor, non-blocking UI indicators or debug logs to signify when advanced hardware encryption (PRF) is successfully negotiated, aiding in development and progressive feature adoption. + +- (Optional but recommended) Include minor, non-blocking UI indicators or debug + logs to signify when advanced hardware encryption (PRF) is successfully + negotiated, aiding in development and progressive feature adoption. diff --git a/ui/components/LoginPage.tsx b/ui/components/LoginPage.tsx index b384cb8..bf68f79 100644 --- a/ui/components/LoginPage.tsx +++ b/ui/components/LoginPage.tsx @@ -35,18 +35,18 @@ export const LoginPage = () => {
- +