import init, { reconstruct_secret, Share, } from "/public/wasm/sss_recovery_bg.wasm.js"; import { mnemonicToEntropy } from "/public/utils/bip39.js"; const methodSelect = document.getElementById("recovery-method"); const voucherSection = document.getElementById("voucher-section"); if (methodSelect && voucherSection) { methodSelect.addEventListener("change", (e) => { if (e.target.value === "voucher") { voucherSection.style.display = "block"; } else { voucherSection.style.display = "none"; } }); } const urlParams = new URLSearchParams(globalThis.location.search); const code = urlParams.get("code"); if (!code) { const errMsg = document.getElementById("error-message"); if (errMsg) { errMsg.textContent = "No recovery code found in URL. Please use the emergency recovery link provided by an admin."; errMsg.style.display = "block"; } const form = document.getElementById("recovery-form"); if (form) form.style.display = "none"; } else { const rcInput = document.getElementById("recovery-code"); if (rcInput) rcInput.value = code; } async function getDeviceShare() { throw new Error( "Device Share PRF not available on this browser. Please use the 12-Word Voucher.", ); } const form = document.getElementById("recovery-form"); if (form) { form.addEventListener("submit", async (e) => { e.preventDefault(); const btn = document.getElementById("reconstructBtn"); const errorDiv = document.getElementById("error-message"); btn.disabled = true; btn.textContent = "Reconstructing Secret..."; errorDiv.style.display = "none"; let share1Data, share2Data; let share1X = 1, share2X = 2; try { await init("/public/wasm/sss_recovery_bg.wasm"); const pin = document.getElementById("recovery-pin").value; const method = methodSelect.value; if (method === "device") { share1Data = await getDeviceShare(); share1X = 1; } else { const mnemonic = document.getElementById("recovery-voucher").value; share1Data = await mnemonicToEntropy(mnemonic); share1X = 3; } const challengeRes = await fetch("/api/recovery/challenge", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code, pin }), }); if (!challengeRes.ok) { const data = await challengeRes.json(); throw new Error(data.error || "Failed to get server share"); } const challengeData = await challengeRes.json(); const { options, serverShareHex } = challengeData; share2Data = new Uint8Array( serverShareHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)), ); share2X = 2; const s1 = new Share(share1X, share1Data); const s2 = new Share(share2X, share2Data); const masterSecret = reconstruct_secret(s1, s2); const cryptoKey = await crypto.subtle.importKey( "raw", masterSecret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const enc = new TextEncoder(); const signatureBuffer = await crypto.subtle.sign( "HMAC", cryptoKey, enc.encode(options.challenge), ); const signatureHex = Array.from(new Uint8Array(signatureBuffer)).map( (b) => b.toString(16).padStart(2, "0"), ).join(""); masterSecret.fill(0); share1Data.fill(0); share2Data.fill(0); const { startRegistration } = SimpleWebAuthnBrowser; const attResp = await startRegistration({ optionsJSON: options }); const verifyRes = await fetch("/api/recovery/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code, response: attResp, signature: signatureHex, }), }); if (!verifyRes.ok) { const data = await verifyRes.json(); throw new Error(data.error || "Failed to verify passkey"); } document.getElementById("recovery-form").style.display = "none"; document.getElementById("success-message").style.display = "block"; setTimeout(() => { globalThis.location.href = "/login"; }, 2000); } catch (err) { errorDiv.textContent = err.message || "An error occurred during recovery."; errorDiv.style.display = "block"; btn.disabled = false; btn.textContent = "Reconstruct & Bind New Passkey"; if (share1Data && share1Data.fill) share1Data.fill(0); if (share2Data && share2Data.fill) share2Data.fill(0); } }); }