// Comparaison de secrets en temps constant, pour éviter les attaques par // mesure de timing sur des comparaisons naïves (`a === b`) qui court- // circuitent dès le premier caractère différent. // // On hache d'abord les deux valeurs en SHA-256 (Web Crypto native, pas de // dépendance tierce) : ça a deux effets utiles ici - // 1. la comparaison finale porte sur deux buffers de longueur FIXE // (32 octets), donc la longueur du secret fourni par l'appelant ne // fuite pas non plus (pas de court-circuit "longueur différente" sur // l'entrée brute) ; // 2. on peut comparer octet par octet sans early-return (accumulation par // OR bit à bit) pour un temps d'exécution indépendant du contenu. async function sha256(input: string): Promise { const bytes = new TextEncoder().encode(input); const digest = await crypto.subtle.digest("SHA-256", bytes); return new Uint8Array(digest); } function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean { // a et b sont toujours des digests SHA-256 ici (32 octets), donc cette // comparaison de longueur ne dépend jamais des entrées utilisateur. if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) { diff |= a[i] ^ b[i]; } return diff === 0; } /** Vrai si `provided` correspond à `expected`, en temps constant. */ export async function timingSafeEqual( provided: string | null | undefined, expected: string | null | undefined ): Promise { if (!provided || !expected) return false; const [a, b] = await Promise.all([sha256(provided), sha256(expected)]); return constantTimeEqual(a, b); }