diff --git a/src/services/auth.ts b/src/services/auth.ts index 3391a1a..c487acb 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -6,6 +6,7 @@ import { StorageService } from './storage'; // The client already does heavy PBKDF2 (600k iterations). // This second layer only needs to be non-trivial, not expensive. const SERVER_HASH_ITERATIONS = 100_000; +const SERVER_HASH_PREFIX = '$s$'; const AUTH_CONTEXT_CACHE_TTL_MS = 15 * 1000; interface CachedUserEntry { @@ -133,7 +134,7 @@ export class AuthService { // Second-layer hash: PBKDF2-SHA256(clientHash, email-salt, iterations). // Ensures database contents alone cannot be used to authenticate (pass-the-hash defense). - // Result is prefixed with "$s$" to distinguish from legacy raw client hashes. + // Result is prefixed to distinguish server-hashed credentials from invalid legacy rows. async hashPasswordServer(clientHash: string, email: string): Promise { const keyMaterial = await crypto.subtle.importKey( 'raw', @@ -151,19 +152,16 @@ export class AuthService { const bytes = new Uint8Array(bits); let binary = ''; for (const b of bytes) binary += String.fromCharCode(b); - return '$s$' + btoa(binary); + return SERVER_HASH_PREFIX + btoa(binary); } // Verify password: hash the input the same way, then constant-time compare. - async verifyPassword(inputHash: string, storedHash: string, email?: string): Promise { - // New server-hashed passwords are prefixed with "$s$". - // Legacy accounts (created before the upgrade) store raw client hashes without prefix. - if (email && storedHash.startsWith('$s$')) { - const serverHash = await this.hashPasswordServer(inputHash, email); - return this.constantTimeEquals(serverHash, storedHash); + async verifyPassword(inputHash: string, storedHash: string, email: string): Promise { + if (!storedHash.startsWith(SERVER_HASH_PREFIX)) { + return false; } - // Legacy path: direct constant-time comparison of raw client hashes. - return this.constantTimeEquals(inputHash, storedHash); + const serverHash = await this.hashPasswordServer(inputHash, email); + return this.constantTimeEquals(serverHash, storedHash); } private constantTimeEquals(a: string, b: string): boolean {