馃 distributed transcription service thistle.dunkirk.sh
1/** 2 * Client-side password hashing using PBKDF2. 3 * Uses aggressive iteration count to waste client CPU instead of server CPU. 4 * Server will apply lightweight Argon2 on top for storage. 5 */ 6 7const ITERATIONS = 1_000_000; // ~1-2 seconds on modern devices 8 9/** 10 * PBKDF2 implementation with fallback for non-secure contexts. 11 */ 12async function pbkdf2( 13 password: Uint8Array, 14 salt: Uint8Array, 15 iterations: number, 16): Promise<Uint8Array> { 17 if (crypto.subtle) { 18 // Use native crypto.subtle when available (secure contexts) 19 const keyMaterial = await crypto.subtle.importKey( 20 "raw", 21 password, 22 { name: "PBKDF2" }, 23 false, 24 ["deriveBits"], 25 ); 26 27 const hashBuffer = await crypto.subtle.deriveBits( 28 { 29 name: "PBKDF2", 30 salt, 31 iterations, 32 hash: "SHA-256", 33 }, 34 keyMaterial, 35 256, 36 ); 37 38 return new Uint8Array(hashBuffer); 39 } 40 41 // Fallback: lazy-load pure JS implementation for non-secure contexts 42 const { pbkdf2Fallback } = await import("./crypto-fallback"); 43 return pbkdf2Fallback(password, salt, iterations); 44} 45 46/** 47 * Hash password client-side using PBKDF2. 48 * @param password - Plaintext password 49 * @param email - Email address (used as salt) 50 * @returns Hex-encoded hash 51 */ 52export async function hashPasswordClient( 53 password: string, 54 email: string, 55): Promise<string> { 56 const encoder = new TextEncoder(); 57 58 // Use email as salt (deterministic, unique per user) 59 const passwordBytes = encoder.encode(password); 60 const salt = encoder.encode(email.toLowerCase()); 61 62 // Derive 256 bits using PBKDF2 (with fallback for non-secure contexts) 63 const hashBuffer = await pbkdf2(passwordBytes, salt, ITERATIONS); 64 65 // Convert to hex string 66 const hashArray = Array.from(hashBuffer); 67 return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); 68}