diff --git a/accounts/urls.py b/accounts/urls.py index 02435c4..db38b26 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -13,5 +13,6 @@ urlpatterns = [ path('background/upload/', views.upload_background, name='upload_background'), path('background/delete/', views.delete_background, name='delete_background'), path('focus-station/', views.save_focus_station, name='save_focus_station'), + path('check-password/', views.check_password, name='check_password'), path('change-password/', views.change_password, name='change_password'), ] diff --git a/accounts/views.py b/accounts/views.py index 03d59c1..70b9920 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -182,6 +182,19 @@ def save_focus_station(request): return JsonResponse({'ok': True}) +@login_required +@csrf_exempt +@require_http_methods(['POST']) +def check_password(request): + try: + body = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return JsonResponse({'ok': False}, status=400) + pw = body.get('password', '') + user = authenticate(request, username=request.user.username, password=pw) + return JsonResponse({'ok': user is not None}) + + @login_required @require_http_methods(['POST']) def change_password(request): diff --git a/templates/accounts/settings.html b/templates/accounts/settings.html index 953a84d..442ef8f 100644 --- a/templates/accounts/settings.html +++ b/templates/accounts/settings.html @@ -175,26 +175,31 @@ document.getElementById('pw-change-form')?.addEventListener('submit', async func const username = '{{ request.user.username }}'; const storageKey = `diora_enc_key_${userId}`; - statusEl.textContent = 'Schlüssel ableiten…'; + statusEl.textContent = 'Passwort prüfen…'; try { - const oldKey = await _pbkdf2Key(oldPw, username); + // Validate old password server-side before touching any keys + const checkResp = await fetch('/accounts/check-password/', { + method: 'POST', + headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, + body: JSON.stringify({password: oldPw}), + }).then(r => r.json()); + if (!checkResp.ok) { + statusEl.textContent = 'Fehler: Altes Passwort ist falsch.'; + return; + } - // Verify old key matches localStorage + // Use the key currently in localStorage as the old key (source of truth for decryption) const stored = localStorage.getItem(storageKey); + let oldKey; if (stored) { - const storedBytes = _base64ToBytes(stored); - const derivedBytes = new Uint8Array(await crypto.subtle.exportKey('raw', oldKey)); - if (!storedBytes.every((b, i) => b === derivedBytes[i])) { - statusEl.textContent = 'Fehler: Das alte Passwort stimmt nicht mit dem gespeicherten Schlüssel überein. Bitte altes Passwort prüfen.'; - return; - } + oldKey = await crypto.subtle.importKey('raw', _base64ToBytes(stored), {name: 'AES-GCM'}, false, ['decrypt']); } const newKey = await _pbkdf2Key(newPw, username); - // Re-encrypt all books + // Re-encrypt all books (only if we have a key to decrypt with) statusEl.textContent = 'Bücherliste laden…'; - const books = await fetch('/books/').then(r => r.json()); + const books = oldKey ? await fetch('/books/').then(r => r.json()) : []; for (let i = 0; i < books.length; i++) { const book = books[i];