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>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user