Extract Counter against replay attacks

This commit is contained in:
Hendrik Belitz
2025-08-22 16:07:20 +02:00
parent 91fb1fa115
commit 3c206fcba7
4 changed files with 39 additions and 3 deletions
+24
View File
@@ -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
}
}
}
+5 -1
View File
@@ -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"
});
+5 -1
View File
@@ -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"
});
+5 -1
View File
@@ -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"
});