// Utility: read cookie value function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); } // Convert base64 string to ArrayBuffer function base64ToArrayBuffer(b64) { const binary = atob(b64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; } // ArrayBuffer to base64 function arrayBufferToBase64(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } async function deriveKey(password, saltBytes, iterations = 100000, length = 32) { const encoder = new TextEncoder(); const keyMaterial = await window.crypto.subtle.importKey( 'raw', encoder.encode(password), { name: 'PBKDF2' }, false, ['deriveBits'] ); const derivedBits = await window.crypto.subtle.deriveBits( { name: 'PBKDF2', salt: saltBytes, iterations, hash: 'SHA-256' }, keyMaterial, length * 8 ); return new Uint8Array(derivedBits); } async function hmacSha256(keyBytes, messageBytes) { const key = await window.crypto.subtle.importKey( 'raw', keyBytes, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'] ); const signature = await window.crypto.subtle.sign('HMAC', key, messageBytes); return new Uint8Array(signature); } document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const errorEl = document.getElementById('error'); errorEl.textContent = ''; const username = document.getElementById('username').value.trim(); const password = document.getElementById('password').value; if (!username || !password) { errorEl.textContent = '请输入账户与密码'; return; } const btn = document.getElementById('loginBtn'); btn.disabled = true; try { // Step 1: get challenge (nonce + salt) const csrftoken = getCookie('csrftoken'); const chalResp = await fetch('/accounts/challenge/', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ username }) }); if (!chalResp.ok) { throw new Error('获取挑战失败'); } const chal = await chalResp.json(); const nonceBytes = new Uint8Array(base64ToArrayBuffer(chal.nonce)); const saltBytes = new Uint8Array(base64ToArrayBuffer(chal.salt)); // Step 2: derive secret and compute HMAC const derived = await deriveKey(password, saltBytes, 100000, 32); const hmac = await hmacSha256(derived, nonceBytes); const hmacB64 = arrayBufferToBase64(hmac); // Step 3: submit login with username and hmac const submitResp = await fetch('/accounts/login/submit/', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ username, hmac: hmacB64 }) }); const submitJson = await submitResp.json(); if (!submitResp.ok || !submitJson.ok) { throw new Error(submitJson.message || '登录失败'); } // Redirect to home with user_id window.location.href = submitJson.redirect_url; } catch (err) { console.error(err); errorEl.textContent = err.message || '发生错误'; } finally { btn.disabled = false; } });