From 3c206fcba714fca088aec98b9c8955416299519a Mon Sep 17 00:00:00 2001 From: Hendrik Belitz Date: Fri, 22 Aug 2025 16:07:20 +0200 Subject: [PATCH] Extract Counter against replay attacks --- src/lib/server/auth/webauthn-service.ts | 24 ++++++++++++++++++++++++ src/routes/api/admin/init/+server.ts | 6 +++++- src/routes/api/auth/passkeys/+server.ts | 6 +++++- src/routes/api/auth/register/+server.ts | 6 +++++- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/lib/server/auth/webauthn-service.ts b/src/lib/server/auth/webauthn-service.ts index ef0269d..b0ed1e6 100644 --- a/src/lib/server/auth/webauthn-service.ts +++ b/src/lib/server/auth/webauthn-service.ts @@ -214,4 +214,28 @@ export class WebAuthnService { static async getUserPasskeys(userId: string) { return await centralDb.select().from(userPasskey).where(eq(userPasskey.userId, userId)); } + + /** + * Extract counter from WebAuthn credential response + * Used during registration to get the initial counter value + */ + static extractCounterFromCredential(credential: { + response: { + authenticatorData: string; + }; + }): number { + try { + // Parse authenticator data + const authenticatorDataBuffer = Buffer.from(credential.response.authenticatorData, "base64"); + + // Extract counter from authenticator data (bytes 33-36) + const counter = authenticatorDataBuffer.readUInt32BE(33); + + logger.debug("Counter extracted from credential", { counter }); + return counter; + } catch (error) { + logger.error("Failed to extract counter from credential", { error: String(error) }); + return 0; // Fallback to 0 if extraction fails + } + } } diff --git a/src/routes/api/admin/init/+server.ts b/src/routes/api/admin/init/+server.ts index db1f765..4a292a0 100644 --- a/src/routes/api/admin/init/+server.ts +++ b/src/routes/api/admin/init/+server.ts @@ -1,5 +1,6 @@ import { json } from "@sveltejs/kit"; import { UserService } from "$lib/server/services/user-service"; +import { WebAuthnService } from "$lib/server/auth/webauthn-service"; import { ValidationError } from "$lib/server/utils/errors"; import type { RequestHandler } from "./$types"; import { registerOpenAPIRoute } from "$lib/server/openapi"; @@ -154,10 +155,13 @@ export const POST: RequestHandler = async ({ request, cookies, url }) => { // Clear the registration cookie after validation (challenge cookie is cleared by login route) cookies.delete("webauthn-registration-email", { path: "/" }); + // Extract counter from WebAuthn credential + const counter = WebAuthnService.extractCounterFromCredential(body.passkey); + await UserService.addPasskey(admin.id, { id: body.passkey.id, publicKey: body.passkey.publicKey, - counter: body.passkey.counter || 0, + counter, deviceName: body.passkey.deviceName || "Unknown Device" }); diff --git a/src/routes/api/auth/passkeys/+server.ts b/src/routes/api/auth/passkeys/+server.ts index c78dbd3..37432b6 100644 --- a/src/routes/api/auth/passkeys/+server.ts +++ b/src/routes/api/auth/passkeys/+server.ts @@ -1,5 +1,6 @@ import { json } from "@sveltejs/kit"; import { UserService } from "$lib/server/services/user-service"; +import { WebAuthnService } from "$lib/server/auth/webauthn-service"; import { NotFoundError, ValidationError } from "$lib/server/utils/errors"; import type { RequestHandler } from "./$types"; import { registerOpenAPIRoute } from "$lib/server/openapi"; @@ -115,12 +116,15 @@ export const POST: RequestHandler = async ({ request }) => { deviceName: body.passkey.deviceName }); + // Extract counter from WebAuthn credential + const counter = WebAuthnService.extractCounterFromCredential(body.passkey); + // Add the passkey using the UserService await UserService.addAdditionalPasskey(body.userId, { id: body.passkey.id, userId: body.userId, publicKey: body.passkey.publicKey, - counter: body.passkey.counter || 0, + counter, deviceName: body.passkey.deviceName || "Unknown Device" }); diff --git a/src/routes/api/auth/register/+server.ts b/src/routes/api/auth/register/+server.ts index f06f8f3..68cc352 100644 --- a/src/routes/api/auth/register/+server.ts +++ b/src/routes/api/auth/register/+server.ts @@ -1,6 +1,7 @@ import { json } from "@sveltejs/kit"; import { UserService } from "$lib/server/services/user-service"; import { InviteService } from "$lib/server/services/invite-service"; +import { WebAuthnService } from "$lib/server/auth/webauthn-service"; import { ValidationError } from "$lib/server/utils/errors"; import type { RequestHandler } from "./$types"; import { registerOpenAPIRoute } from "$lib/server/openapi"; @@ -201,10 +202,13 @@ export const POST: RequestHandler = async ({ request, cookies, url }) => { // Clear the registration cookie after validation (challenge cookie is cleared by login route) cookies.delete("webauthn-registration-email", { path: "/" }); + // Extract counter from WebAuthn credential + const counter = WebAuthnService.extractCounterFromCredential(body.passkey); + await UserService.addPasskey(user.id, { id: body.passkey.id, publicKey: body.passkey.publicKey, - counter: body.passkey.counter || 0, + counter, deviceName: body.passkey.deviceName || "Unknown Device" });