Accounts: fix password change key migration
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 15s

The old key comparison was wrong — the localStorage key may have been
randomly generated rather than PBKDF2-derived. Fix:
- Add /accounts/check-password/ to validate the old password server-side
  before touching any keys
- Use the localStorage key directly as the old decryption key (it is
  always the correct source of truth, regardless of how it was generated)
- Derive the new key from the new password via PBKDF2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-05-25 16:46:03 +02:00
parent 1426c69a73
commit 817323ad19
3 changed files with 30 additions and 11 deletions

View file

@ -13,5 +13,6 @@ urlpatterns = [
path('background/upload/', views.upload_background, name='upload_background'), path('background/upload/', views.upload_background, name='upload_background'),
path('background/delete/', views.delete_background, name='delete_background'), path('background/delete/', views.delete_background, name='delete_background'),
path('focus-station/', views.save_focus_station, name='save_focus_station'), 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'), path('change-password/', views.change_password, name='change_password'),
] ]

View file

@ -182,6 +182,19 @@ def save_focus_station(request):
return JsonResponse({'ok': True}) 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 @login_required
@require_http_methods(['POST']) @require_http_methods(['POST'])
def change_password(request): def change_password(request):

View file

@ -175,26 +175,31 @@ document.getElementById('pw-change-form')?.addEventListener('submit', async func
const username = '{{ request.user.username }}'; const username = '{{ request.user.username }}';
const storageKey = `diora_enc_key_${userId}`; const storageKey = `diora_enc_key_${userId}`;
statusEl.textContent = 'Schlüssel ableiten…'; statusEl.textContent = 'Passwort prüfen…';
try { try {
const oldKey = await _pbkdf2Key(oldPw, username); // Validate old password server-side before touching any keys
const checkResp = await fetch('/accounts/check-password/', {
// Verify old key matches localStorage method: 'POST',
const stored = localStorage.getItem(storageKey); headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
if (stored) { body: JSON.stringify({password: oldPw}),
const storedBytes = _base64ToBytes(stored); }).then(r => r.json());
const derivedBytes = new Uint8Array(await crypto.subtle.exportKey('raw', oldKey)); if (!checkResp.ok) {
if (!storedBytes.every((b, i) => b === derivedBytes[i])) { statusEl.textContent = 'Fehler: Altes Passwort ist falsch.';
statusEl.textContent = 'Fehler: Das alte Passwort stimmt nicht mit dem gespeicherten Schlüssel überein. Bitte altes Passwort prüfen.';
return; return;
} }
// Use the key currently in localStorage as the old key (source of truth for decryption)
const stored = localStorage.getItem(storageKey);
let oldKey;
if (stored) {
oldKey = await crypto.subtle.importKey('raw', _base64ToBytes(stored), {name: 'AES-GCM'}, false, ['decrypt']);
} }
const newKey = await _pbkdf2Key(newPw, username); 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…'; 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++) { for (let i = 0; i < books.length; i++) {
const book = books[i]; const book = books[i];