const encoder = new TextEncoder(); const iterations = 310000; export async function hashPassword(password, salt = crypto.getRandomValues(new Uint8Array(16))) { const key = await crypto.subtle.importKey( "raw", encoder.encode(password), "PBKDF2", false, ["deriveBits"] ); const bits = await crypto.subtle.deriveBits( { name: "PBKDF2", hash: "SHA-256", salt, iterations }, key, 256 ); return `pbkdf2_sha256$${iterations}$${base64url(salt)}$${base64url(new Uint8Array(bits))}`; } export async function verifyPassword(password, storedHash) { const [scheme, storedIterations, saltText, hashText] = String(storedHash || "").split("$"); if (scheme !== "pbkdf2_sha256" || Number(storedIterations) !== iterations) return false; const candidate = await hashPassword(password, fromBase64url(saltText)); return timingSafeEqual(candidate, storedHash); } export async function hashSessionToken(token) { const digest = await crypto.subtle.digest("SHA-256", encoder.encode(token)); return base64url(new Uint8Array(digest)); } export function createSessionToken() { const bytes = crypto.getRandomValues(new Uint8Array(32)); return base64url(bytes); } function timingSafeEqual(a, b) { const left = encoder.encode(a); const right = encoder.encode(b); if (left.length !== right.length) return false; let result = 0; for (let index = 0; index < left.length; index += 1) { result |= left[index] ^ right[index]; } return result === 0; } function base64url(bytes) { const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join(""); return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", ""); } function fromBase64url(value) { const padded = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(value.length / 4) * 4, "="); const binary = atob(padded); return Uint8Array.from(binary, (char) => char.charCodeAt(0)); }