Portal-Passwort: Reveal/Send prueft Konsistenz gegen Login-Hash

Pentester-Hinweis: bcrypt-Hash (Login) und verschluesseltes Reveal-Feld
koennen out-of-sync sein -> Support liest ein Passwort vor, das beim
Login scheitert.

Analyse: alle aktuellen Schreibpfade sind konsistent (beide Felder
zusammen, oder encrypted=null, oder Rehash desselben Passworts) - der
Code erzeugt keinen Desync. Ursache = Altlast/manueller DB-Eingriff.

Fix (defensiv, unabhaengig von der Ursache):
- getCustomerPortalPassword liefert {status: ok|none|desync} und prueft
  den entschluesselten Klartext per bcrypt.compare gegen den Login-Hash.
- Bei desync (oder Entschluesselungsfehler) geben WEDER Reveal NOCH
  Send-Credentials das Passwort aus -> 409 'Dateninkonsistenz, bitte
  neu setzen'. Reveal-Read wird mit Status auditiert.
- Neues Diagnose-Script scripts/check-portal-password-sync.ts scannt
  alle Portal-Kunden auf Desync (nur Diagnose, aendert nichts) - fuer
  Prod, da der Pentester keinen FS-Zugriff hat.

Verifiziert: desync -> nicht ausgegeben; konsistent -> ok; kein PW ->
none. Scan laeuft (0 Desync auf Dev).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-11 22:46:53 +02:00
co-authored by Claude Opus 4.8
parent 15ac003dad
commit 79f6f3e629
4 changed files with 133 additions and 12 deletions
@@ -0,0 +1,61 @@
/**
* Diagnose: findet Portal-Kunden, deren gespeichertes (verschlüsseltes)
* Passwort NICHT zum Login-Hash (bcrypt) passt. Solche „Desyncs" führen dazu,
* dass der Reveal/Send-Credentials ein Passwort ausgibt, das beim Login
* scheitert. Seit dem Reveal-Konsistenzcheck werden sie nicht mehr ausgegeben
* dieses Script findet bestehende Altlasten (z.B. aus manuellen DB-Eingriffen
* oder alten Code-Versionen), damit man sie gezielt neu setzen kann.
*
* Aufruf (im Backend-Container/-Verzeichnis):
* npx tsx scripts/check-portal-password-sync.ts
* Nur Diagnose ändert NICHTS.
*/
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { decrypt } from '../src/utils/encryption.js';
const prisma = new PrismaClient();
async function main() {
const customers = await prisma.customer.findMany({
where: { portalPasswordEncrypted: { not: null }, portalPasswordHash: { not: null } },
select: {
id: true,
customerNumber: true,
portalEmail: true,
portalPasswordEncrypted: true,
portalPasswordHash: true,
},
});
let desync = 0;
for (const c of customers) {
let decrypted: string;
try {
decrypted = decrypt(c.portalPasswordEncrypted!);
} catch {
desync++;
console.log(`DESYNC (Entschlüsselung fehlgeschlagen): Kunde #${c.id} ${c.customerNumber}`);
continue;
}
const matches = await bcrypt.compare(decrypted, c.portalPasswordHash!);
if (!matches) {
desync++;
console.log(`DESYNC: Kunde #${c.id} ${c.customerNumber} (${c.portalEmail ?? '-'}) gespeichertes Passwort passt nicht zum Login-Hash`);
}
}
console.log(`\nGeprüft: ${customers.length} Portal-Kunden mit beiden Feldern. Desync: ${desync}.`);
if (desync > 0) {
console.log('→ Für diese Kunden bitte ein neues Portal-Passwort setzen/generieren.');
} else {
console.log('→ Alles konsistent.');
}
await prisma.$disconnect();
}
main().catch((e) => {
console.error(e);
process.exit(1);
});