diff --git a/accounts/crypto.py b/accounts/crypto.py index eed1f27..3fe11c7 100644 --- a/accounts/crypto.py +++ b/accounts/crypto.py @@ -90,4 +90,26 @@ def verify_password(password_plain: str, salt_b64: str, hash_b64: str) -> bool: actual = hash_password_with_salt(password_plain, salt) return hmac.compare_digest(actual, expected) except Exception: - return False \ No newline at end of file + return False + +def generate_rsa_private_pem_b64() -> str: + if rsa is None or serialization is None: + raise RuntimeError("cryptography library is required for RSA operations") + priv = rsa.generate_private_key(public_exponent=65537, key_size=2048) + pem = priv.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()) + return base64.b64encode(pem).decode('ascii') + +def public_spki_b64_from_private_pem_b64(private_pem_b64: str) -> str: + if serialization is None: + raise RuntimeError("cryptography library is required for RSA operations") + priv = serialization.load_pem_private_key(base64.b64decode(private_pem_b64), password=None) + pub = priv.public_key() + spki = pub.public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo) + return base64.b64encode(spki).decode('ascii') + +def rsa_oaep_decrypt_b64_with_private_pem(private_pem_b64: str, ciphertext_b64: str) -> bytes: + if serialization is None or padding is None or hashes is None: + raise RuntimeError("cryptography library is required for RSA operations") + priv = serialization.load_pem_private_key(base64.b64decode(private_pem_b64), password=None) + ct = base64.b64decode(ciphertext_b64) + return priv.decrypt(ct, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) \ No newline at end of file diff --git a/accounts/static/accounts/login.js b/accounts/static/accounts/login.js index 76aa71d..e856ed1 100644 --- a/accounts/static/accounts/login.js +++ b/accounts/static/accounts/login.js @@ -77,8 +77,20 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => { 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 }) }); - const setKeyJson = await setKeyResp.json(); - if (!setKeyResp.ok || !setKeyJson.ok) throw new Error('设置会话密钥失败'); + const setKeySnapshot = await (async () => { + const clone = setKeyResp.clone(); + const txt = await clone.text(); + let parsed = null; + try { parsed = await setKeyResp.json(); } catch (_) {} + return { txt, parsed }; + })(); + if (!setKeySnapshot.parsed) { + const msg = (setKeySnapshot.txt || '').trim(); + const mapped = msg.toLowerCase().includes('decrypt error') ? '会话密钥解密失败,请刷新页面后重试' : (msg || '设置会话密钥失败'); + throw new Error(mapped); + } + const setKeyJson = setKeySnapshot.parsed; + if (!setKeyResp.ok || !setKeyJson.ok) throw new Error(setKeyJson.message || '设置会话密钥失败'); const aesKey = await importAesKey(aesKeyRaw); const iv = new Uint8Array(12); window.crypto.getRandomValues(iv); @@ -92,7 +104,19 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => { 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(); + const submitSnapshot = await (async () => { + const clone = submitResp.clone(); + const txt = await clone.text(); + let parsed = null; + try { parsed = await submitResp.json(); } catch (_) {} + return { txt, parsed }; + })(); + if (!submitSnapshot.parsed) { + const msg = (submitSnapshot.txt || '').trim(); + const mapped = msg.toLowerCase().includes('decrypt error') ? '解密失败,请刷新页面后重试' : (msg || '服务器响应异常'); + throw new Error(mapped); + } + const submitJson = submitSnapshot.parsed; if (!submitResp.ok || !submitJson.ok) { if (submitJson && submitJson.captcha_required) { needCaptcha = true; await loadCaptcha(); } throw new Error(submitJson.message || '登录失败'); diff --git a/accounts/views.py b/accounts/views.py index 5dbdfa3..b98592f 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -12,7 +12,7 @@ from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie from django.conf import settings from .es_client import get_user_by_username -from .crypto import get_public_key_spki_b64, rsa_oaep_decrypt_b64, aes_gcm_decrypt_b64, verify_password +from .crypto import get_public_key_spki_b64, rsa_oaep_decrypt_b64, aes_gcm_decrypt_b64, verify_password, generate_rsa_private_pem_b64, public_spki_b64_from_private_pem_b64, rsa_oaep_decrypt_b64_with_private_pem from elastic.es_connect import get_registration_code, get_user_by_username as es_get_user_by_username, get_all_users as es_get_all_users, write_user_data @@ -25,7 +25,11 @@ def login_page(request): @require_http_methods(["GET"]) @ensure_csrf_cookie def pubkey(request): - pk_b64 = get_public_key_spki_b64() + pem_b64 = request.session.get("rsa_private_pem_b64") + if not pem_b64: + pem_b64 = generate_rsa_private_pem_b64() + request.session["rsa_private_pem_b64"] = pem_b64 + pk_b64 = public_spki_b64_from_private_pem_b64(pem_b64) return JsonResponse({"public_key_spki": pk_b64}) @require_http_methods(["GET"]) @@ -56,7 +60,10 @@ def set_session_key(request): if not enc_key_b64: return HttpResponseBadRequest("Missing fields") try: - key_bytes = rsa_oaep_decrypt_b64(enc_key_b64) + pem_b64 = request.session.get("rsa_private_pem_b64") + if not pem_b64: + return HttpResponseBadRequest("Decrypt error") + key_bytes = rsa_oaep_decrypt_b64_with_private_pem(pem_b64, enc_key_b64) except Exception: return HttpResponseBadRequest("Decrypt error") request.session["session_enc_key_b64"] = base64.b64encode(key_bytes).decode("ascii") @@ -110,6 +117,8 @@ def secure_login_submit(request): request.session["permission"] = 1 if "session_enc_key_b64" in request.session: del request.session["session_enc_key_b64"] + if "rsa_private_pem_b64" in request.session: + del request.session["rsa_private_pem_b64"] if "login_failed_once" in request.session: del request.session["login_failed_once"] if "captcha_code" in request.session: