Merge commit from fork

Remove email from passkey crypto. Validation now occurs via Bootstrap…
This commit is contained in:
Karl Ludwig Weise
2026-07-08 20:35:40 +02:00
committed by GitHub
3 changed files with 20 additions and 40 deletions
-2
View File
@@ -1014,7 +1014,6 @@ export class UnifiedAppointmentCrypto {
staffId: string,
passkeyId: string,
prfOutput: ArrayBuffer,
email: string,
keyPair: { publicKey: Uint8Array; privateKey: Uint8Array },
): Promise<void> {
// Derive passkey-based shard from PRF output
@@ -1034,7 +1033,6 @@ export class UnifiedAppointmentCrypto {
passkeyId,
publicKey: this.uint8ArrayToBase64(keyPair.publicKey),
privateKeyShare: this.uint8ArrayToBase64(dbShard),
email,
}),
});
@@ -196,14 +196,7 @@
if (tenantId && passkeyId && prfOutput && kyberKeyPair) {
const crypto = new UnifiedAppointmentCrypto();
return await crypto
.storeStaffKeyPair(
tenantId,
$formData.userId,
passkeyId,
prfOutput,
$formData.email,
kyberKeyPair,
)
.storeStaffKeyPair(tenantId, $formData.userId, passkeyId, prfOutput, kyberKeyPair)
.then(() => {
toast.success(m["setupPasskey.successKeyPairSaved"]());
})
@@ -25,6 +25,7 @@ import { centralDb } from "$lib/server/db";
import { user } from "$lib/server/db/central-schema";
import { eq } from "drizzle-orm";
import { normalizeEmail } from "$lib/utils";
import { verifyRegistrationBootstrapToken } from "$lib/server/auth/registration-bootstrap";
const ML_KEM_768_PUBLIC_KEY_BYTES = 1184;
const ML_KEM_768_PRIVATE_KEY_BYTES = 2400;
@@ -84,13 +85,8 @@ registerOpenAPIRoute("/tenants/{id}/staff/{staffId}/crypto", "POST", {
type: "string",
description: "Base64-encoded database shard of private key",
},
email: {
type: "string",
format: "email",
description: "Email address for cookie validation during registration",
},
},
required: ["passkeyId", "publicKey", "privateKeyShare", "email"],
required: ["passkeyId", "publicKey", "privateKeyShare"],
},
},
},
@@ -157,7 +153,6 @@ const requestSchema = z.object({
(value) => hasDecodedByteLength(value, ML_KEM_768_PRIVATE_KEY_BYTES),
`Private key share must decode to ${ML_KEM_768_PRIVATE_KEY_BYTES} bytes (ML-KEM-768)`,
),
email: z.email("Valid email is required").optional(), // Required for cookie validation
});
export const POST: RequestHandler = async ({ params, locals, request, cookies }) => {
@@ -179,42 +174,36 @@ export const POST: RequestHandler = async ({ params, locals, request, cookies })
);
}
const { passkeyId, publicKey, privateKeyShare, email } = validation.data;
const { passkeyId, publicKey, privateKeyShare } = validation.data;
// Authentication: Accept either active session OR registration cookie
const isAuthenticated = locals.user && locals.user.id === staffId;
const registrationEmail = normalizeEmail(cookies.get("webauthn-registration-email"));
const requestEmail = normalizeEmail(email);
const isRegistration =
!!registrationEmail && !!requestEmail && registrationEmail === requestEmail;
if (!isAuthenticated && !isRegistration) {
log.warn("Unauthorized crypto key storage attempt", {
tenantId,
staffId,
requesterId: locals.user?.id,
hasRegistrationCookie: !!registrationEmail,
emailsMatch: registrationEmail === requestEmail,
});
throw new AuthorizationError();
}
// For authenticated users, verify tenant access
if (isAuthenticated) {
checkPermission(locals, tenantId, false);
} else {
const bootstrapPayload = await verifyRegistrationBootstrapToken(
cookies.get("webauthn-registration-bootstrap"),
);
if (!bootstrapPayload) {
throw new AuthorizationError();
}
if (bootstrapPayload.userId !== staffId) {
throw new AuthorizationError();
}
const staffUser = await centralDb
.select({ id: user.id })
.select({ id: user.id, email: user.email, tenantId: user.tenantId })
.from(user)
.where(eq(user.id, staffId))
.limit(1);
if (staffUser.length === 0) {
log.warn("Rejected registration crypto key storage for unknown staff user", {
tenantId,
staffId,
hasRegistrationCookie: !!registrationEmail,
});
if (staffUser.length === 0 || staffUser[0].tenantId !== tenantId) {
throw new AuthorizationError();
}
if (normalizeEmail(staffUser[0].email) !== bootstrapPayload.email) {
throw new AuthorizationError();
}
}