Files
certain/backend/app/services/geheimnis.py
T
GuidoandClaude Opus 5 fc6ee2f345 feat: Postfaecher einrichten und Zahlungs-Mails abholen (Etappe B, Teil 2)
- Verschluesselung fuer Zugangsdaten: Ein IMAP-Passwort laesst sich nicht
  hashen, Certain muss es dem Mailserver vorlegen. Es liegt deshalb mit
  Fernet verschluesselt in der Datenbank, der Schluessel wird per HKDF aus
  session_secret abgeleitet und nirgends gespeichert. Wird das Geheimnis
  getauscht, meldet Certain das verstaendlich statt mit einem Stacktrace.
- IMAP-Abruf ueber imaplib/email (Standardbibliothek). Ordnerliste,
  Verbindungstest mit klarer Fehlermeldung, Absender-Zuordnung auf Konten,
  danach als gelesen markieren oder verschieben.
- Wiedererkennung ueber die Message-ID statt der UID – die vergibt der
  Server beim Verschieben neu.
- Nichts wird stillschweigend gebucht: Eine Mail wird zum Eingangs-Eintrag.
  Fehlt das Zielkonto oder ist ein Feld unsicher, gilt sie als unklar.
- Hintergrund-Thread beim Start, nach dem Muster des EZB-Kursabrufs: ein
  langsames Postfach darf den Dienst nie blockieren.

Getestet gegen einen IMAP-Doppelgaenger, damit die Suite ohne Netz laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 19:58:06 +02:00

46 lines
1.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Verschlüsselung für Zugangsdaten, die Certain im Klartext braucht.
Ein IMAP-Passwort lässt sich nicht hashen: Certain muss es dem Mailserver
vorlegen können. Es liegt deshalb verschlüsselt in der Datenbank – wer die
Datei in die Hände bekommt, hat damit noch nichts.
Der Schlüssel wird aus ``session_secret`` abgeleitet und nirgends gespeichert.
Das hat eine Folge, die im Formular auch so stehen muss: Wird das
Server-Geheimnis getauscht, sind die abgelegten Passwörter unlesbar und die
Postfächer müssen neu eingerichtet werden. Das ist der gewollte Preis dafür,
dass ein Datenbank-Diebstahl allein nicht reicht.
"""
from functools import lru_cache
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from ..config import get_settings
# Trennt diesen Verwendungszweck von jedem anderen, der dasselbe Geheimnis nutzt.
_INFO = b"certain-finance/mail-credentials/v1"
@lru_cache
def _schluessel() -> Fernet:
roh = HKDF(
algorithm=hashes.SHA256(), length=32, salt=None, info=_INFO
).derive(get_settings().session_secret.encode("utf-8"))
import base64
return Fernet(base64.urlsafe_b64encode(roh))
def verschluesseln(klartext: str) -> str:
return _schluessel().encrypt(klartext.encode("utf-8")).decode("ascii")
def entschluesseln(geheim: str) -> str | None:
"""Klartext zurück, oder None wenn der Schlüssel nicht mehr passt."""
try:
return _schluessel().decrypt(geheim.encode("ascii")).decode("utf-8")
except (InvalidToken, ValueError):
return None