修改登录逻辑,使用RSA-OAEP 包裹每会话独立 AES-GCM 密钥 + 加密提交凭据
This commit is contained in:
@@ -1,64 +1,39 @@
|
||||
// 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);
|
||||
}
|
||||
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]);
|
||||
}
|
||||
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 importRsaPublicKey(spkiBytes) {
|
||||
return window.crypto.subtle.importKey('spki', spkiBytes, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']);
|
||||
}
|
||||
|
||||
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);
|
||||
async function rsaOaepEncrypt(publicKey, dataBytes) {
|
||||
const encrypted = await window.crypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, dataBytes);
|
||||
return new Uint8Array(encrypted);
|
||||
}
|
||||
|
||||
async function importAesKey(keyBytes) {
|
||||
return window.crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
|
||||
}
|
||||
|
||||
async function aesGcmEncrypt(aesKey, ivBytes, dataBytes) {
|
||||
const ct = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: ivBytes }, aesKey, dataBytes);
|
||||
return new Uint8Array(ct);
|
||||
}
|
||||
|
||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
@@ -68,53 +43,41 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
if (!username || !password) {
|
||||
errorEl.textContent = '请输入账户与密码';
|
||||
return;
|
||||
}
|
||||
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 })
|
||||
const pkResp = await fetch('/accounts/pubkey/', { method: 'GET', credentials: 'same-origin', headers: { 'X-CSRFToken': csrftoken || '' } });
|
||||
if (!pkResp.ok) throw new Error('获取公钥失败');
|
||||
const pkJson = await pkResp.json();
|
||||
const spkiBytes = new Uint8Array(base64ToArrayBuffer(pkJson.public_key_spki));
|
||||
const pubKey = await importRsaPublicKey(spkiBytes);
|
||||
|
||||
const aesKeyRaw = new Uint8Array(32); window.crypto.getRandomValues(aesKeyRaw);
|
||||
const encAesKey = await rsaOaepEncrypt(pubKey, aesKeyRaw);
|
||||
const encAesKeyB64 = arrayBufferToBase64(encAesKey);
|
||||
|
||||
const setKeyResp = await fetch('/accounts/session-key/', {
|
||||
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ encrypted_key: encAesKeyB64 })
|
||||
});
|
||||
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));
|
||||
const setKeyJson = await setKeyResp.json();
|
||||
if (!setKeyResp.ok || !setKeyJson.ok) throw new Error('设置会话密钥失败');
|
||||
|
||||
// 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);
|
||||
const aesKey = await importAesKey(aesKeyRaw);
|
||||
const iv = new Uint8Array(12); window.crypto.getRandomValues(iv);
|
||||
const payload = new TextEncoder().encode(JSON.stringify({ username, password }));
|
||||
const ct = await aesGcmEncrypt(aesKey, iv, payload);
|
||||
const ctB64 = arrayBufferToBase64(ct);
|
||||
const ivB64 = arrayBufferToBase64(iv);
|
||||
|
||||
// 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 submitResp = await fetch('/accounts/login/secure-submit/', {
|
||||
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ iv: ivB64, ciphertext: ctB64 })
|
||||
});
|
||||
const submitJson = await submitResp.json();
|
||||
if (!submitResp.ok || !submitJson.ok) {
|
||||
throw new Error(submitJson.message || '登录失败');
|
||||
}
|
||||
// Redirect to home with user_id
|
||||
if (!submitResp.ok || !submitJson.ok) throw new Error(submitJson.message || '登录失败');
|
||||
window.location.href = submitJson.redirect_url;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
Reference in New Issue
Block a user