Auto-decrypt appointments after added keys + removing arbitrary key expiry + slow down login to give browser time to write cookies

This commit is contained in:
Karl Ludwig Weise
2026-09-08 08:58:59 +02:00
parent fe05c55ffb
commit 575845acad
5 changed files with 45 additions and 21 deletions
-14
View File
@@ -153,9 +153,7 @@ export class UnifiedAppointmentCrypto {
// Staff-specific properties
private staffKeyPair: StaffKeyPair | null = null;
private staffId: string | null = null;
private tenantId: string | null = null;
private staffAuthenticated: boolean = false;
private keyExpiry: number | null = null;
// Shared crypto utilities
private kyberCrypto: KyberCrypto = new KyberCrypto();
@@ -786,9 +784,7 @@ export class UnifiedAppointmentCrypto {
};
this.staffId = staffId;
this.tenantId = tenantId;
this.staffAuthenticated = true;
this.keyExpiry = Date.now() + 10 * 60 * 1000; // 10 minutes
console.log("✅ Staff authentication successful with PRF");
} catch (error) {
@@ -820,10 +816,6 @@ export class UnifiedAppointmentCrypto {
throw new Error("Staff not authenticated");
}
if (this.keyExpiry && Date.now() > this.keyExpiry) {
throw new Error("Staff session expired - please authenticate again");
}
try {
// Parse the staffKeyShare which now contains: encapsulatedSecret || iv || encryptedTunnelKey
const staffKeyShareBytes = this.hexToUint8Array(encryptedData.staffKeyShare);
@@ -958,9 +950,7 @@ export class UnifiedAppointmentCrypto {
logoutStaff(): void {
this.staffKeyPair = null;
this.staffId = null;
this.tenantId = null;
this.staffAuthenticated = false;
this.keyExpiry = null;
}
/**
@@ -1727,10 +1717,6 @@ export class UnifiedAppointmentCrypto {
throw new Error("Staff not authenticated");
}
if (this.keyExpiry && Date.now() > this.keyExpiry) {
throw new Error("Staff session expired - please authenticate again");
}
try {
// Parse the staffKeyShare which now contains: encapsulatedSecret || iv || encryptedTunnelKey
const staffKeyShareBytes = this.hexToUint8Array(staffKeyShare);
+30 -1
View File
@@ -1,9 +1,10 @@
import { writable } from "svelte/store";
import { get, writable } from "svelte/store";
import { UnifiedAppointmentCrypto } from "$lib/client/appointment-crypto";
import { auth } from "./auth";
interface StaffCryptoState {
crypto: UnifiedAppointmentCrypto | null;
isAuthenticating: boolean;
isAuthenticated: boolean;
error: string | null;
}
@@ -11,6 +12,7 @@ interface StaffCryptoState {
const createStaffCryptoStore = () => {
const store = writable<StaffCryptoState>({
crypto: null,
isAuthenticating: false,
isAuthenticated: false,
error: null,
});
@@ -37,11 +39,18 @@ const createStaffCryptoStore = () => {
*/
async authenticate(staffId: string, tenantId: string): Promise<boolean> {
try {
store.set({
...get(store),
isAuthenticating: true,
error: null,
});
const crypto = new UnifiedAppointmentCrypto();
await crypto.authenticateStaff(staffId, tenantId);
store.set({
crypto,
isAuthenticating: false,
isAuthenticated: true,
error: null,
});
@@ -51,6 +60,7 @@ const createStaffCryptoStore = () => {
const errorMessage = error instanceof Error ? error.message : "Authentication failed";
store.set({
crypto: null,
isAuthenticating: false,
isAuthenticated: false,
error: errorMessage,
});
@@ -59,6 +69,24 @@ const createStaffCryptoStore = () => {
}
},
async authenticateAndWait(staffId: string, tenantId: string): Promise<boolean> {
const isAuthenticating = get(store).isAuthenticating;
if (isAuthenticating) {
// Wait for the ongoing authentication to complete
return new Promise((resolve) => {
const unsubscribe = store.subscribe((state) => {
if (!state.isAuthenticating) {
unsubscribe();
resolve(state.isAuthenticated);
}
});
});
} else {
// Start a new authentication process
return await this.authenticate(staffId, tenantId);
}
},
/**
* Clear the staff crypto state and auth data
*/
@@ -66,6 +94,7 @@ const createStaffCryptoStore = () => {
auth.clearPasskeyAuthData();
store.set({
crypto: null,
isAuthenticating: false,
isAuthenticated: false,
error: null,
});
@@ -4,7 +4,6 @@
import { staffCrypto } from "$lib/stores/staff-crypto";
import { type TCalendarItem } from "$lib/types/calendar";
import Loader2Icon from "@lucide/svelte/icons/loader-2";
import { onMount } from "svelte";
import { calendarStore } from "$lib/stores/calendar";
import { Button } from "$lib/components/ui/button";
import { m } from "$i18n/messages";
@@ -22,8 +21,14 @@
let decrypted = $state<AppointmentData | undefined>();
let error = $state<string | undefined>();
onMount(() => {
decrypt();
$effect(() => {
if (!decrypted) {
if ($staffCrypto.isAuthenticated) {
decrypt();
} else {
error = "Keys missing";
}
}
});
const errors = {
@@ -31,6 +36,8 @@
};
const decrypt = async (retry = true) => {
error = undefined;
if (!item.appointment) {
console.error("Unable to decrypt appointment data - no appointment data in item.", item.id);
error = "Missing data";
@@ -64,8 +71,7 @@
const user = $auth.user;
if (retry && user && user.tenantId) {
console.warn("Staff crypto not initialized, retrying decryption...");
// TODO: This should only be done once, not in every instance
await staffCrypto.authenticate(user.id, user.tenantId);
await staffCrypto.authenticateAndWait(user.id, user.tenantId);
decrypt(false);
return;
}
@@ -74,7 +74,6 @@
};
onMount(() => {
console.log("item.start", item.start);
if (mode.agentId && item.availableAgents) {
const availableAgents = item.availableAgents.map((it) => it.id);
if (availableAgents.includes(mode.agentId)) {
@@ -43,6 +43,10 @@
onResult: async (event) => {
if (event.result.type === "success") {
auth.setUser(event.result.data?.user);
// Wait for cookies to be set before navigating to dashboard
await new Promise((resolve) => setTimeout(resolve, 100));
await goto(resolve(ROUTES.DASHBOARD.MAIN));
} else {
if ($formData.type === "passkey" && $formData.id === "") {