import init, { Share, reconstruct_secret } from '/public/wasm/sss_recovery_bg.wasm.js'; import { mnemonicToEntropy } from '/public/utils/bip39.ts'; 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); } }); }