修改登录逻辑,使用RSA-OAEP 包裹每会话独立 AES-GCM 密钥 + 加密提交凭据
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import hmac
|
||||
|
||||
from django.http import JsonResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import render, redirect
|
||||
@@ -10,7 +9,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 salt_for_username, hmac_sha256
|
||||
from .crypto import get_public_key_spki_b64, rsa_oaep_decrypt_b64, aes_gcm_decrypt_b64, verify_password
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@@ -19,90 +18,72 @@ def login_page(request):
|
||||
return render(request, "accounts/login.html")
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@csrf_protect
|
||||
def challenge(request):
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest("Invalid JSON")
|
||||
|
||||
username = payload.get("username", "").strip()
|
||||
if not username:
|
||||
return HttpResponseBadRequest("Username required")
|
||||
|
||||
# Generate nonce and compute per-username salt
|
||||
nonce = os.urandom(16)
|
||||
salt = salt_for_username(username)
|
||||
|
||||
# Persist challenge in session to prevent replay with mismatched user
|
||||
request.session["challenge_nonce"] = base64.b64encode(nonce).decode("ascii")
|
||||
request.session["challenge_username"] = username
|
||||
|
||||
return JsonResponse({
|
||||
"nonce": base64.b64encode(nonce).decode("ascii"),
|
||||
"salt": base64.b64encode(salt).decode("ascii"),
|
||||
})
|
||||
@require_http_methods(["GET"])
|
||||
@ensure_csrf_cookie
|
||||
def pubkey(request):
|
||||
pk_b64 = get_public_key_spki_b64()
|
||||
return JsonResponse({"public_key_spki": pk_b64})
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@csrf_protect
|
||||
def login_submit(request):
|
||||
def set_session_key(request):
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest("Invalid JSON")
|
||||
|
||||
username = payload.get("username", "").strip()
|
||||
client_hmac_b64 = payload.get("hmac", "")
|
||||
if not username or not client_hmac_b64:
|
||||
enc_key_b64 = payload.get("encrypted_key", "")
|
||||
if not enc_key_b64:
|
||||
return HttpResponseBadRequest("Missing fields")
|
||||
try:
|
||||
key_bytes = rsa_oaep_decrypt_b64(enc_key_b64)
|
||||
except Exception:
|
||||
return HttpResponseBadRequest("Decrypt error")
|
||||
request.session["session_enc_key_b64"] = base64.b64encode(key_bytes).decode("ascii")
|
||||
return JsonResponse({"ok": True})
|
||||
|
||||
# Validate challenge stored in session
|
||||
session_username = request.session.get("challenge_username")
|
||||
nonce_b64 = request.session.get("challenge_nonce")
|
||||
if not session_username or not nonce_b64 or session_username != username:
|
||||
return HttpResponseBadRequest("Challenge not found or mismatched user")
|
||||
|
||||
# Lookup user in ES (placeholder)
|
||||
@require_http_methods(["POST"])
|
||||
@csrf_protect
|
||||
def secure_login_submit(request):
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest("Invalid JSON")
|
||||
iv_b64 = payload.get("iv", "")
|
||||
ct_b64 = payload.get("ciphertext", "")
|
||||
if not iv_b64 or not ct_b64:
|
||||
return HttpResponseBadRequest("Missing fields")
|
||||
key_b64 = request.session.get("session_enc_key_b64")
|
||||
if not key_b64:
|
||||
return HttpResponseBadRequest("Session key missing")
|
||||
try:
|
||||
key_bytes = base64.b64decode(key_b64)
|
||||
pt = aes_gcm_decrypt_b64(key_bytes, iv_b64, ct_b64)
|
||||
obj = json.loads(pt.decode("utf-8"))
|
||||
except Exception:
|
||||
return HttpResponseBadRequest("Decrypt error")
|
||||
username = (obj.get("username") or "").strip()
|
||||
password = (obj.get("password") or "")
|
||||
if not username or not password:
|
||||
return HttpResponseBadRequest("Missing credentials")
|
||||
user = get_user_by_username(username)
|
||||
if not user:
|
||||
return JsonResponse({"ok": False, "message": "User not found"}, status=401)
|
||||
|
||||
# Server-side HMAC verification
|
||||
try:
|
||||
nonce = base64.b64decode(nonce_b64)
|
||||
stored_derived_b64 = user.get("password", "")
|
||||
stored_derived = base64.b64decode(stored_derived_b64)
|
||||
server_hmac_b64 = base64.b64encode(hmac_sha256(stored_derived, nonce)).decode("ascii")
|
||||
except Exception:
|
||||
return HttpResponseBadRequest("Verification error")
|
||||
|
||||
if not hmac.compare_digest(server_hmac_b64, client_hmac_b64):
|
||||
if not verify_password(password, user.get("password_salt") or "", user.get("password_hash") or ""):
|
||||
return JsonResponse({"ok": False, "message": "Invalid credentials"}, status=401)
|
||||
|
||||
# Successful login: rotate session key and set user session
|
||||
try:
|
||||
request.session.cycle_key()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
request.session["user_id"] = user["user_id"]
|
||||
request.session["username"] = user["username"]
|
||||
try:
|
||||
request.session["permission"] = int(user["permission"]) if user.get("permission") is not None else 1
|
||||
except Exception:
|
||||
request.session["permission"] = 1
|
||||
|
||||
# Clear challenge to prevent reuse
|
||||
for k in ("challenge_username", "challenge_nonce"):
|
||||
if k in request.session:
|
||||
del request.session[k]
|
||||
|
||||
return JsonResponse({
|
||||
"ok": True,
|
||||
"redirect_url": f"/main/home/?user_id={user['user_id']}",
|
||||
})
|
||||
if "session_enc_key_b64" in request.session:
|
||||
del request.session["session_enc_key_b64"]
|
||||
return JsonResponse({"ok": True, "redirect_url": f"/main/home/?user_id={user['user_id']}"})
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
|
||||
Reference in New Issue
Block a user