feat(security): Zugangsdaten verschluesselt speichern (#021)
AES-256-GCM mit Schluessel aus APP_SECRET fuer SMTP-Passwort und Webhook-Geheimnis. Bestehende Klartextwerte werden weiter gelesen und beim naechsten Speichern umgeschluesselt. 156 Tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { verschluesseln } from "@/lib/geheimnisse";
|
||||
import { protokolliere } from "@/server/audit";
|
||||
import type { EmailKonfiguration, WebhookKonfiguration } from "@/server/notifications/kanaele";
|
||||
import { testNachricht } from "@/server/notifications/nachrichten";
|
||||
@@ -45,8 +46,9 @@ export async function emailKanalSpeichern(
|
||||
port,
|
||||
secure: port === 465,
|
||||
benutzer: formular.get("benutzer")?.toString().trim() ?? "",
|
||||
// Leeres Feld heißt „unverändert“ – so muss das Passwort nicht erneut eingegeben werden.
|
||||
passwort: passwortEingabe || (bisher?.passwort ?? ""),
|
||||
// Leeres Feld heißt „unverändert“; das gespeicherte Passwort bleibt dann wie es ist.
|
||||
// Neue Eingaben werden verschlüsselt abgelegt (#021).
|
||||
passwort: passwortEingabe ? verschluesseln(passwortEingabe) : (bisher?.passwort ?? ""),
|
||||
absender: formular.get("absender")?.toString().trim() ?? "",
|
||||
empfaenger,
|
||||
};
|
||||
@@ -90,7 +92,7 @@ export async function webhookKanalSpeichern(
|
||||
|
||||
const konfiguration: WebhookKonfiguration = {
|
||||
url,
|
||||
geheimnis: geheimnisEingabe || bisher?.geheimnis,
|
||||
geheimnis: geheimnisEingabe ? verschluesseln(geheimnisEingabe) : bisher?.geheimnis,
|
||||
};
|
||||
|
||||
const daten = {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createHmac } from "node:crypto";
|
||||
import nodemailer from "nodemailer";
|
||||
import type { NotificationKanal } from "@/generated/prisma/enums";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { entschluesseln } from "@/lib/geheimnisse";
|
||||
|
||||
/**
|
||||
* Versandwege für Benachrichtigungen: SMTP (getestet gegen Microsoft 365) und ein generischer Webhook,
|
||||
@@ -109,6 +110,19 @@ export interface KanalEintrag {
|
||||
konfiguration: EmailKonfiguration | WebhookKonfiguration;
|
||||
}
|
||||
|
||||
/** Holt die Zugangsdaten aus der Datenbank und entschlüsselt sie für den Versand. */
|
||||
export function konfigurationLesen(
|
||||
typ: NotificationKanal,
|
||||
gespeichert: unknown,
|
||||
): EmailKonfiguration | WebhookKonfiguration {
|
||||
if (typ === "EMAIL") {
|
||||
const eintrag = gespeichert as EmailKonfiguration;
|
||||
return { ...eintrag, passwort: entschluesseln(eintrag.passwort ?? "") };
|
||||
}
|
||||
const eintrag = gespeichert as WebhookKonfiguration;
|
||||
return { ...eintrag, geheimnis: eintrag.geheimnis ? entschluesseln(eintrag.geheimnis) : undefined };
|
||||
}
|
||||
|
||||
export async function aktiveKanaele(): Promise<KanalEintrag[]> {
|
||||
const kanaele = await prisma.benachrichtigungskanal.findMany({ where: { aktiv: true } });
|
||||
return kanaele.map((kanal) => ({
|
||||
@@ -116,7 +130,7 @@ export async function aktiveKanaele(): Promise<KanalEintrag[]> {
|
||||
typ: kanal.typ,
|
||||
name: kanal.name,
|
||||
aktiv: kanal.aktiv,
|
||||
konfiguration: kanal.konfiguration as unknown as EmailKonfiguration | WebhookKonfiguration,
|
||||
konfiguration: konfigurationLesen(kanal.typ, kanal.konfiguration),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { kennzahlen } from "@/server/queries/hosts";
|
||||
import { aktiveKanaele, ueberKanalSenden, type Nachricht } from "./kanaele";
|
||||
import { aktiveKanaele, konfigurationLesen, ueberKanalSenden, type Nachricht } from "./kanaele";
|
||||
import { alertNachricht, zusammenfassungNachricht, type AlertNachrichtDaten } from "./nachrichten";
|
||||
|
||||
/**
|
||||
@@ -149,7 +149,7 @@ export async function ueberEinenKanalSenden(kanalId: string, nachricht: Nachrich
|
||||
typ: eintrag.typ,
|
||||
name: eintrag.name,
|
||||
aktiv: eintrag.aktiv,
|
||||
konfiguration: eintrag.konfiguration as never,
|
||||
konfiguration: konfigurationLesen(eintrag.typ, eintrag.konfiguration),
|
||||
};
|
||||
})());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user