Geheimnisse in Windows-Apps speichern – Klartextkonfiguration mit DPAPI vermeiden

· · Windows-Entwicklung, Sicherheit, DPAPI, C# / .NET, Win32

Im vorigen Artikel „Checkliste für die minimale Sicherheit bei der Windows-App-Entwicklung“ wurde die minimale Grundlinie festgehalten: „Geheiminformationen nicht im Quellcode oder in Klartext-Konfiguration ablegen“ und „unter Win32 / .NET DPAPI / ProtectedData verwenden“.

Diesmal wird ein Teil davon genauer betrachtet: „DPAPI verwenden, um zumindest besser als Klartext zu sein“.

Die Zielgruppe sind Windows-Apps wie diese:

  • WPF-/WinForms-/WinUI-Desktopanwendungen
  • Windows-Clients in C# / .NET
  • Apps, die geneigt sind, Zielanmeldeinformationen oder API-Tokens in einer lokalen Konfigurationsdatei zu speichern

Behandelt wird hier ein realistisches Design dafür, „ein Geheimnis, das zwangsläufig lokal gespeichert werden muss, wenigstens nicht unverändert im Klartext von appsettings.json liegen zu lassen“. Es geht nicht um „perfekten Schutz, der jeden Angreifer schlägt“. Übertreibt man das, wird aus dem Sicherheitsthema plötzlich eine Gruselgeschichte.

1. Zunächst das Fazit

In der Praxis ist diese Reihenfolge des Denkens am klarsten.

  1. Dem Client von vornherein kein langlebiges Geheimnis mitgeben
    • Windows-Authentifizierung, integrierte Authentifizierung, interaktive Benutzeranmeldung und serverseitige Geheimnisverwaltung bevorzugen
  2. Ist lokale Speicherung wirklich unvermeidlich, nicht im Klartext ablegen
    • Unter Windows zuerst DPAPI / ProtectedData als erste Wahl ansetzen
  3. Bei einer gewöhnlichen Desktopanwendung DataProtectionScope.CurrentUser als Grundeinstellung verwenden
    • LocalMachine hat einen ziemlich eingeschränkten Einsatzbereich
  4. DPAPI schützt nicht bis zu dem Punkt, an dem das Gerät vollständig kompromittiert ist
    • Code, der mit denselben Benutzerrechten läuft, kann grundsätzlich alles entschlüsseln, was auch dieser Benutzer entschlüsseln kann

Und der wichtigste Punkt dieses Artikels ist dieser:

„Der geheime Schlüssel muss ohnehin irgendwo gespeichert werden – ist das sicherheitstechnisch dann nicht dasselbe, ob Klartext oder DPAPI?“

Das ist zur Hälfte richtig, und die Schlussfolgerung ist falsch.

  • Eigene AES-Verschlüsselung mit dem Schlüssel in derselben App oder derselben Konfiguration kommt Klartext ziemlich nahe
  • Aber DPAPI verlagert die Schlüsselverwaltung an das Betriebssystem und bindet die entschlüsselungsfähige Partei an „diesen Windows-Benutzer“ oder „diesen Computer“
  • Dadurch ändert sich die Widerstandsfähigkeit gegenüber Vorfällen wie dem alleinigen Durchsickern der Konfigurationsdatei, dem Mitnehmen auf einen anderen PC, Fehlversand, Backup-Leck oder versehentlicher Aufnahme ins Repository erheblich

Mit anderen Worten: Betrachtet man nur die abstrakte Aussage „der Schlüssel liegt irgendwo“, sehen sie gleich aus – aber „wer ihn wie leicht in welchem Kontext nutzen kann“ ist völlig unterschiedlich.

Einen Schlüssel unter die Fußmatte zu legen und einen Schlüssel erst nach Identitätsprüfung am Empfang auszuhändigen als „dasselbe“ zu bezeichnen, ist etwas grob.

2. Warum Klartext-Konfiguration gefährlich ist

Der Grund, warum Klartextspeicherung gefährlich ist, ist viel bodenständiger als Kryptografietheorie. In der Praxis sickern Geheimnisse meist über solche Wege durch:

  • Die Konfigurationsdatei wird unverändert in Git eingecheckt
  • Die Konfigurationsdatei landet komplett in einem Diagnose-ZIP
  • Bei einer Support-Anfrage wird die Konfigurationsdatei angehängt
  • Über Backups oder Dateifreigaben können Dritte sie lesen
  • Verbindungszeichenfolgen und Tokens erscheinen unverändert in Protokollen
  • Ausgeschiedene Mitarbeiter oder andere Benutzer können die Datei auf demselben Gerät lesen

Das größte Problem bei Klartext ist, dass in dem Moment, in dem er gelesen werden kann, das Geheimnis erledigt ist.

  • Datei geöffnet – vorbei
  • Kopiert – vorbei
  • Als E-Mail-Anhang verschickt – vorbei
  • Im Repository verblieben – man kümmert sich quasi dauerhaft darum

Der Angreifer muss nicht einmal besonders raffiniert sein. „Lässt sich mit einem Texteditor öffnen“ ist für sich genommen bereits ziemlich schwach.

3. Die Antwort auf „Der geheime Schlüssel wird doch ohnehin irgendwo gespeichert, ist das nicht dasselbe?“

Diese Frage ist berechtigt. Und beantwortet man sie unbedacht, wird ein Sicherheitsartikel sofort vage.

Die Antwort lautet: im Sinne von „irgendwo wird ein Schlüssel benötigt“ ja, im Sinne von „deshalb ist es dasselbe“ nein.

3.1. Was ist gleich, was ist unterschiedlich?

Tatsächlich braucht Verschlüsselung letztlich irgendeine Root of Trust. Geheimnisse sprudeln nicht kostenlos irgendwo aus dem Universum. Das ist eine strenge Welt.

Der sicherheitstechnische Unterschied entscheidet sich jedoch an diesen drei Punkten:

  • Hält die App den Schlüssel direkt?
  • An welche Partei ist der Schlüssel gebunden?
  • Lässt sich entschlüsseln, wenn nur die Datei gestohlen wird?

Grob tabellarisch zusammengefasst sieht dieser Unterschied so aus:

Methode Konfigurationsdatei wird gelesen Nur die Datei wird auf einen anderen PC mitgenommen Wird von einem anderen Benutzer auf demselben PC gelesen Code, der mit denselben Benutzerrechten läuft
Klartext Sickert sofort durch Sickert unverändert durch Sickert unverändert durch Kann selbstverständlich lesen
Eigene Verschlüsselung + Schlüssel in derselben Konfiguration/Binärdatei Sickert weitgehend durch Sickert weitgehend durch Sickert weitgehend durch Kann selbstverständlich entschlüsseln
DPAPI + CurrentUser Allein aus der Datei nicht sofort lesbar Normalerweise schwer zu entschlüsseln Normalerweise schwer zu entschlüsseln Kann entschlüsseln
DPAPI + LocalMachine Allein aus der Datei nicht sofort lesbar Außerhalb dieses PCs normalerweise schwer zu entschlüsseln Auf demselben PC breit entschlüsselbar Kann entschlüsseln

Wichtig ist hier, dass DPAPI „die Datei lesen können“ von „das Geheimnis nutzen können“ trennt.

Für die Entschlüsselung Benötigtes ── verbleibt auf Seiten des Betriebssystems, ist nicht im Chiffrat enthaltengeschützt mit CurrentUsergeschützt mit LocalMachineHauptschlüssel des Benutzersbei Schutz mit CurrentUserHauptschlüssel des Computersbei Schutz mit LocalMachineChiffratlandet in Konfigurationsdatei / DB-Spalte / Diagnose-ZIP= das, was mitgenommen werden kannCode, der als dieser Benutzer läuft→ kann entschlüsselnAnderer Benutzer auf demselben PC→ kann nicht entschlüsselnCode auf demselben PC→ kann auch als anderer Benutzer entschlüsselnNur das Chiffrat auf einen anderen PC kopieren→ kann nicht entschlüsselnMit dem gesamten Roamingprofil umziehen→ Schlüsselmaterial wandert mit, daher entschlüsselbar (Abschnitt 6.5)

Abbildung 1: Nur das Chiffrat befindet sich auf der Seite dessen, was mitgenommen werden kann; der für die Entschlüsselung nötige Hauptschlüssel bleibt auf Seiten des Betriebssystems. Allerdings reicht LocalMachine über den ganzen PC, sodass sich andere Benutzer auf demselben PC damit nicht aussperren lassen

Bei Klartext sind diese beiden Dinge identisch. Kann man die Datei lesen, kann man auch das Geheimnis lesen.

Bei DPAPI dagegen muss man, zumindest mit CurrentUser,

  • als dieser Windows-Benutzer,
  • in diesem Windows-Kontext,
  • über den Schutzmechanismus des Betriebssystems

entschlüsseln.

Dieser Unterschied ist am Ort eines tatsächlichen Vorfalls ziemlich groß.

3.2. „Aber derselbe Benutzer kann es doch trotzdem entschlüsseln?“ – Stimmt

Das ist ein Punkt, den man nicht schönreden sollte.

Code, der mit denselben Benutzerrechten ausgeführt wird, kann grundsätzlich alles entschlüsseln, was auch dieser Benutzer entschlüsseln kann.

Das heißt, DPAPI ist nicht in erster Linie auf Situationen wie diese ausgelegt:

  • Das Gerät ist bereits mit Malware kompromittiert
  • Der Angreifer kann Code als dieser Benutzer ausführen
  • Das Gerät ist auf Administratorebene vollständig übernommen worden

In dieser Situation kann, da die App selbst entschlüsseln kann, auch der Code des Angreifers entschlüsseln. „Aber es ist doch verschlüsselt“ ist hier keine große Beruhigung.

DPAPI wirkt vor allem auf der Seite „Dateidiebstahl, Fehlplatzierung, Offline-Mitnahme, Zugriff durch einen anderen Benutzer“.

Verwechselt man das, passieren beide Fehler gleichzeitig:

  • man unterschätzt, was sich schützen lässt, und verzichtet auf den Einsatz
  • man überschätzt, was sich nicht schützen lässt, und fühlt sich unbegründet sicher

Beides ist unauffällig gefährlich.

3.3. Worin liegt also der eigentliche Nutzen?

Der Nutzen von DPAPI lässt sich in einem Satz zusammenfassen:

„Das Geheimnis selbst lässt sich von der Lesbarkeit der Konfigurationsdatei entkoppeln.“

Bei Vorfällen wie diesen zeigt sich der Unterschied zwischen Klartext und DPAPI zum Beispiel:

  • Ein Benutzer hat die Konfigurationsdatei versehentlich an den Support geschickt
  • Die Konfigurationsdatei landete in einem Diagnose-ZIP
  • Aus einem Backup ist nur die Konfigurationsdatei durchgesickert
  • Sie wurde in einen Freigabeordner kopiert
  • Ein Entwickler sah nur das Chiffrat und konnte den Inhalt nicht lesen

Das sind ziemlich reale Vorteile. Man kann den Radius alltäglicher Vorfälle verkleinern, ohne den Angreifer zu einem Filmhelden zu stilisieren.

4. Warum DPAPI genau richtig ist

Beim Umgang mit lokal gespeicherten Geheimnissen unter Windows liegen die praktischen Vorzüge von DPAPI in Folgendem.

4.1. Schlüsselverwaltung lässt sich an das Betriebssystem delegieren

Selbst einen AES-Schlüssel erzeugen, speichern, mit Berechtigungen versehen, rotieren, die Auswirkungen eines Lecks durchdenken und dazu noch eine Manipulationserkennung einbauen. Das ist schwerer, als es aussieht. Und macht man es unbedacht, landet der Schlüssel meist am selben Ort.

Mit DPAPI lässt sich das Problem „wie erzeuge und wo lege ich den Verschlüsselungsschlüssel ab“ aus der App-Implementierung herauslösen.

In diesem Sinn trifft man den Kern eher, wenn man DPAPI nicht als „API zur Wahl eines Verschlüsselungsalgorithmus“, sondern als „API zur Delegation der Schlüsselverwaltung an das Betriebssystem“ betrachtet.

4.2. Die entschlüsselungsfähige Partei lässt sich an einen Windows-Benutzer oder den Computer binden

Bei einer gewöhnlichen Desktopanwendung reicht in vielen Fällen die Wahl von CurrentUser.

Die Entschlüsselung setzt voraus, dass

  • dieser Benutzer angemeldet ist,
  • die Verarbeitung im Kontext dieses Benutzers läuft.

Dadurch erhält man die Eigenschaft, dass das reine Kopieren des Chiffrats auf einen anderen PC dieses nicht ohne Weiteres nutzbar macht.

4.3. Manipulationserkennung lässt sich leicht mit einbeziehen

Bei selbstgebauter Verschlüsselung passiert häufig, dass man mit „mit AES verschlüsselt, fertig“ die Manipulationserkennung vergisst.

DPAPI besitzt auch einen Integritätsschutz für die verschlüsselten Daten, sodass sich das Erkennen einer eigenmächtigen Änderung des Chiffrats leicht auf den betriebssystemseitigen Mechanismus abstützen lässt – ein praktischer Vorteil.

4.4. Unkompliziert aus C# / .NET nutzbar

In C# lässt sich System.Security.Cryptography.ProtectedData direkt verwenden. Dass keine zusätzliche Bibliothek nötig ist, hilft bei reinen Windows-Apps ziemlich.

5. Was DPAPI schützt und was nicht

Hier lohnt es sich, klar zu trennen.

5.1. Was sich leichter schützen lässt

DPAPI wirkt zumindest in Situationen wie diesen:

  • Klartext-Leck der Konfigurationsdatei
  • Mitnahme der Datei auf einen anderen PC
  • Zugriff durch einen anderen Benutzer auf demselben PC (unter der Annahme von CurrentUser)
  • Leck über Backup oder Anhang
  • Der Zustand „aus Versehen lesbar“ in Entwicklung und Wartung

5.2. Was nicht oder nur schwach geschützt ist

Andererseits sollte man in diesen Situationen nicht zu viel Vertrauen setzen:

  • Angriffscode, der mit denselben Benutzerrechten läuft
  • Vollständige Kompromittierung des Geräts selbst
  • Übernahme mit Administratorrechten
  • Der Klartext im Arbeitsspeicher, nachdem die App entschlüsselt hat
  • Ein langlebiges Geheimnis, das an alle Clients gleich verteilt wird

Der letzte Punkt, „ein für alle Clients gemeinsames langlebiges Geheimnis“, ist besonders wichtig.

Zum Beispiel Designs wie:

  • allen Kunden denselben API-Schlüssel einbetten
  • auf allen Geräten dasselbe gemeinsame Passwort halten
  • einen fest vorgegebenen Entschlüsselungsschlüssel verteilen, der ausschließlich auf dem Client liegt

neigen dazu, sich vom Zeitpunkt an, an dem er von einem einzigen Gerät extrahiert wird, auf das Gesamtsystem auszuwirken. Die Logik ist einfach: Kann die App auf einem einzigen Gerät entschlüsseln, lässt sich das Geheimnis dort herausholen.

DPAPI ist wirksam, um „diesen Speicherort besser als Klartext zu machen“, aber es rechtfertigt nicht, ein Geheimnis überhaupt erst auf dem Client abzulegen, das dort nicht liegen sollte.

Geheimnisse dieser Art sollte man nicht durch eine andere Speichermethode absichern, sondern eher in diese Richtung auslagern:

  • serverseitig ablegen
  • der Client hält nur ein Token
  • benutzerbezogene Anmeldeinformationen verwenden
  • befristete Tokens verwenden

6. Die Abgrenzung zwischen CurrentUser und LocalMachine

Dieser Punkt ist ziemlich wichtig. Wählt man unbedacht, ändert sich die Bedeutung.

6.1. Die Grundeinstellung ist CurrentUser

Bei einer gewöhnlichen Windows-Desktopanwendung geht man zunächst grundsätzlich von CurrentUser aus.

Geeignete Beispiele:

  • Benutzerorientierte WPF-/WinForms-/WinUI-Desktopanwendungen
  • Apps mit benutzerbezogenen Einstellungen oder Anmeldeinformationen
  • Apps, die ihre Einstellungen unter %LocalAppData% oder %AppData% ablegen

In diesem Fall lässt sich der Wert leicht als „Geheimnis dieses Windows-Benutzers“ behandeln.

6.2. LocalMachine hat einen ziemlich eingeschränkten Einsatzbereich

LocalMachine wirkt praktisch, ist für eine gewöhnliche Desktopanwendung aber zu weitreichend.

Geeignet ist es zum Beispiel für Fälle wie:

  • ein Windows-Dienst auf einer vertrauenswürdigen Einzelzweck-Maschine
  • ein Geheimnis, das nur von einem bestimmten Prozess auf dieser Maschine genutzt wird
  • einen Fall, in dem eine Nutzung über Anmeldebenutzer hinweg auf demselben Gerät zwingend erforderlich ist

Die zu beachtenden Punkte wiegen jedoch schwer.

  • Es lässt sich breit von Prozessen entschlüsseln, die auf diesem PC laufen
  • Bei gemeinsam genutzten Geräten, RDS, Sprungservern und Umgebungen mit mehreren Benutzern wird es leicht gefährlich
  • Wählt man es nach dem Motto „bequem, weil alle es nutzen können“, gerät man meist später in Schwierigkeiten

Und die Beweggründe, aus denen man zu LocalMachine neigt, sind meist diese drei:

  • Es bleibt auch nach einem Benutzerwechsel lesbar
  • Auch ein Dienst kann darauf zugreifen
  • Es ist bequem, wenn es einfach funktioniert

Das ist alles „bequem“, aber nicht „geschützt“. Wählt man LocalMachine bei einer gewöhnlichen Desktopanwendung, weitet man die Entschlüsselungsmöglichkeit auf andere Prozesse auf demselben PC aus, wodurch sich die Bedeutung erheblich ändert.

6.3. Bei Zweifel so denken

  • Gewöhnliche UI-App -> CurrentUser
  • Sonderfall, bei dem wirklich auf Maschinenebene geschützt werden muss -> LocalMachine
  • Jeder Benutzer muss entschlüsseln können, aber auf dem Gerät sind auch andere Benutzer -> meist lohnt sich eine Überarbeitung des Designs

6.4. Bei Diensten oder Impersonation ist etwas mehr Vorsicht nötig

Kommen Windows-Dienste oder Impersonation ins Spiel, wird die Bedeutung von CurrentUser etwas schwerer.

  • Wer ist das Ausführungskonto?
  • Ist das Profil dieses Kontos geladen?
  • In welchem Kontext findet die Entschlüsselung statt?

Verschiebt sich das, kommt es leicht zu „es hat verschlüsselt, lässt sich aber nicht entschlüsseln“. Bei Diensteinsätzen reicht „einfach CurrentUser“ nicht immer aus.

Bei Impersonation ist ein typischer, auch in Microsoft Learn dokumentierter Fehler „Key not valid for use in specified state.“. DPAPI hält die Schlüsseldaten im Benutzerprofil, daher kann ohne geladenes Profil nicht entschlüsselt werden. Vor dem Impersonate-Aufruf muss das Profil des Zielbenutzers geladen sein.

6.5. Fälle kennen, in denen im Betrieb „nicht mehr entschlüsselt werden kann“

Schmerzhafter als die Verschlüsselung selbst ist in der Praxis oft der Vorfall, dass sich nicht mehr entschlüsseln lässt. Da DPAPI die entschlüsselungsfähige Partei an den Windows-Benutzer bzw. den Computer bindet, wird die Datei unlesbar, sobald diese Bindung reißt.

Diese fünf Fälle sollte man vorab kennen:

Fall Was passiert Vorsorge
Passwort-Reset durch einen Administrator Der an das Benutzerpasswort gebundene Schutz löst sich, und der Zugriff auf mit DPAPI geschützte Daten kann verloren gehen. Auch Microsofts Support-Informationen beschreiben den Fall, dass nach einem administrativen Passwort-Reset kein Zugriff mehr auf DPAPI-Daten besteht Das Geheimnis so gestalten, dass es sich „erneut beschaffen“ lässt. Bei Entschlüsselungsfehlern zur erneuten Eingabe führen
Neuanlage des Profils Ein neues Profil besitzt anderes Schlüsselmaterial, daher lässt sich das bisherige Chiffrat nicht mehr entschlüsseln Die Konfigurationsdatei versionieren und einen Entschlüsselungsfehler nicht zum Absturz führen lassen
Nur das Chiffrat wird auf einen anderen PC kopiert Das für die Entschlüsselung nötige Schlüsselmaterial liegt im Benutzerprofil, daher lässt sich mit CurrentUser allein das mitgenommene Chiffrat nicht lesen (das ist die Kehrseite der in 3.1 genannten Stärke) So gestalten, dass pro Gerät neu gespeichert wird
Roamingprofil Das lässt sich lesen. Da das Schlüsselmaterial zusammen mit dem Profil umzieht, hält auch Microsoft Learn ausdrücklich fest, dass ein Benutzer mit Roamingprofil von einem anderen Computer im Netzwerk aus entschlüsseln kann. Behandelt man das wie die vorherige Zeile, werden bei einer Migration unnötig Anmeldeinformationen neu erzeugt Nicht pauschal annehmen, „anderer PC = nicht lesbar“. Erst prüfen, ob Roaming vorliegt, dann das Migrationsverfahren festlegen
Wechsel des Ausführungskontos eines Dienstes Ändert sich das Ausführungskonto zwischen Schutz und Entschlüsselung, lässt sich mit CurrentUser nicht mehr lesen Ein Verfahren zum erneuten Schützen bei Kontowechsel in den Betrieb aufnehmen

Kurz gesagt: Man schreibt den Code unter der Annahme, dass ProtectedData.Unprotect fehlschlagen kann. Schlägt die Entschlüsselung fehl, wird eine CryptographicException ausgelöst; diese fängt man ab und leitet zur erneuten Eingabe über.

using System;
using System.Security.Cryptography;
using System.Text;

// protectedBase64: aus der Konfigurationsdatei gelesenes Chiffrat (Base64)
// entropy: denselben Wert wie beim Schützen übergeben. Auch null ist möglich
static bool TryUnprotect(string protectedBase64, byte[]? entropy, out string plaintext)
{
    plaintext = string.Empty;

    try
    {
        byte[] plainBytes = ProtectedData.Unprotect(
            Convert.FromBase64String(protectedBase64),
            optionalEntropy: entropy,
            scope: DataProtectionScope.CurrentUser);

        plaintext = Encoding.UTF8.GetString(plainBytes);
        return true;
    }
    catch (CryptographicException)
    {
        // Entschlüsselung nicht möglich = die Umgebung hat sich vermutlich geändert.
        // Hier nicht abstürzen, sondern beim Aufrufer zur erneuten Eingabe führen
        return false;
    }
    catch (FormatException)
    {
        // Wenn das Base64 selbst fehlerhaft ist
        return false;
    }
}

Wenn sich Support-Anfragen häufen mit „habe verschlüsselt, kann aber nicht entschlüsseln“, liegt meist einer dieser Tabellenfälle vor.

7. Minimale Implementierungsrichtlinien

Geht es einer Windows-App nur darum, „mit dem Klartext in der Konfigurationsdatei aufzuhören“, muss das Design nicht besonders komplex sein. Ein paar Punkte sollte man aber nicht auslassen.

7.1. Nur das Geheimnis schützen

Statt die gesamte Konfiguration komplett zu verschlüsseln, ist es einfacher zu handhaben, wenn man zunächst nur die geheimen Einträge schützt.

Zum Beispiel so aufteilen:

  • Server-URL
  • Benutzername
  • DB-Name
  • Feature-Flags

können oft weiterhin im Klartext bleiben.

Dagegen sind

  • Passwörter
  • API-Tokens
  • Refresh-Tokens
  • Anmeldeinformationen für Netzwerkfreigaben

Schutzziele.

Bei dieser Aufteilung erhält man:

  • leichter zu bearbeitende Konfiguration
  • leichter überprüfbare Diffs
  • klar erkennbar, was geheim ist
  • insgesamt einfacheren Betrieb

7.2. Den Speicherort grundsätzlich pro Benutzer wählen

Bei einer gewöhnlichen Desktopanwendung sollte der Speicherort grundsätzlich benutzerbezogen sein.

  • %LocalAppData%\Vendor\App\settings.json
  • %AppData%\Vendor\App\settings.json

Zumindest sollte man es nicht achtlos unter dem Installationsordner oder an einem leicht freigebbaren Ort ablegen.

Selbst mit DPAPI-Schutz führt eine nachlässige ACL am Speicherort zu Aussagen wie „das Chiffrat kann gelesen werden“, „die Konfigurationsstruktur ist sichtbar“, „Bedienfehler passieren“. Verteidigung wirkt besser gestaffelt als einstufig.

7.3. optionalEntropy ist kein allmächtiger zweiter Schlüssel

An ProtectedData lässt sich optionalEntropy übergeben. Das ist praktisch, aber kein „magischer zweiter Schlüssel, der alles sicher macht, sobald man ihn in die Binärdatei einbettet“.

  • Legt man ihn in dieselbe Datei, ist er kein Geheimnis
  • Bettet man ihn als festen Wert in die Binärdatei ein, ist er ebenfalls kein starkes Geheimnis
  • Trotzdem ist er zur Zweckerkennung und zur Vermeidung von Fehlanwendung nützlich

In der Praxis ist es angemessen,

  • den App-Namen,
  • den Verwendungszweck,
  • eine Versionskennung

als festen Byte-String zu übergeben und damit „das versehentliche Akzeptieren von Chiffrat aus einem anderen Verwendungszweck“ zu verhindern.

7.4. Chiffrat darf in Git – das ist damit nicht gemeint

Auch das ist unauffällig wichtig.

Das DPAPI-Chiffrat ist deutlich besser als Klartext, aber das heißt nicht, dass man die gesamte Konfigurationsdatei ins Repository aufnehmen darf.

Der Grund ist einfach:

  • Chiffrat bleibt lange bestehen
  • Irgendwann könnte derselbe Gerätekontext erneut hergestellt werden
  • Die Datei enthält auch andere Informationen als das Geheimnis
  • Es entsteht eine Kultur nach dem Motto „ist ja geschützt, also darf man nachlässig damit umgehen“

„Besser als Klartext“ und „überall sicher, wo man es ablegt“ sind völlig verschiedene Dinge.

7.5. Nicht ins Protokoll ausgeben

Überraschend häufig ist das Muster, dass man nach der Entschlüsselung alles durch eine Protokollausgabe zunichtemacht.

  • Bei einem Verbindungsfehler die gesamte Verbindungszeichenfolge ausgeben
  • Bei einem API-401 den Authorization-Header stehen lassen
  • Geheimnisse in Ausnahmemeldungen einmischen

Macht man das, wird trotz Abschaffung des Konfigurationsdatei-Klartexts letztlich das Protokoll zum Klartextlager. Traurig, aber ziemlich praxisnah.

8. Minimales Implementierungsbeispiel in C# / .NET

8.1. Zuerst ist eine Referenz hinzuzufügen

ProtectedData wirkt so, als gehöre es zur BCL, doch je nach Ziel-Framework unterscheidet sich, „woher es kommt“. Stolpert man hier, lässt sich der Typname ProtectedData nicht auflösen.

Ziel Erforderlicher Schritt Quelle
.NET Framework Dem Projekt eine System.Security-Assemblyreferenz hinzufügen System.Security.dll
.NET Core / .NET 5 oder neuer (einschließlich .NET 6 / 8) Das NuGet-Paket System.Security.Cryptography.ProtectedData hinzufügen System.Security.Cryptography.ProtectedData.dll

Dieses Paket ist in keinem der Shared Frameworks von .NET Core / .NET 5 oder neuer enthalten. Auch bei einem Windows-Ziel wie net8.0-windows ist die Referenz ausdrücklich erforderlich.

dotnet add package System.Security.Cryptography.ProtectedData

Noch ein Punkt, den man vor der Implementierung kennen sollte: ProtectedData ist nur für Windows. Da es von DPAPI abhängt, löst ein Aufruf unter .NET auf einer anderen Plattform als Windows eine PlatformNotSupportedException aus. Bei einer von vornherein plattformübergreifenden Codebasis wählt man wie in 10.1 von Anfang an ein anderes Design.

8.2. Minimale Implementierung

Im Folgenden ein minimales Beispiel, das eine in der Konfigurationsdatei zu speichernde Zeichenfolge mit CurrentUser schützt. Zur Zweckerkennung wird eine feste optionalEntropy eingefügt, doch bitte nicht als geheimen Schlüssel verstehen.

using System;
using System.Security.Cryptography;
using System.Text;

public static class DpapiSecretProtector
{
    // Zur Zweckerkennung. Kein zweiter geheimer Schlüssel.
    private static readonly byte[] Entropy =
        Encoding.UTF8.GetBytes("ComComponent:DesktopApp:SettingsSecret:v1");

    public static string ProtectToBase64(string plaintext)
    {
        ArgumentNullException.ThrowIfNull(plaintext);

        byte[] plainBytes = Encoding.UTF8.GetBytes(plaintext);
        byte[] protectedBytes = Array.Empty<byte>();

        try
        {
            protectedBytes = ProtectedData.Protect(
                plainBytes,
                optionalEntropy: Entropy,
                scope: DataProtectionScope.CurrentUser);

            return Convert.ToBase64String(protectedBytes);
        }
        finally
        {
            Array.Clear(plainBytes, 0, plainBytes.Length);

            if (protectedBytes.Length > 0)
            {
                Array.Clear(protectedBytes, 0, protectedBytes.Length);
            }
        }
    }

    public static string UnprotectFromBase64(string protectedBase64)
    {
        ArgumentNullException.ThrowIfNull(protectedBase64);

        byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
        byte[] plainBytes = Array.Empty<byte>();

        try
        {
            plainBytes = ProtectedData.Unprotect(
                protectedBytes,
                optionalEntropy: Entropy,
                scope: DataProtectionScope.CurrentUser);

            return Encoding.UTF8.GetString(plainBytes);
        }
        finally
        {
            Array.Clear(protectedBytes, 0, protectedBytes.Length);

            if (plainBytes.Length > 0)
            {
                Array.Clear(plainBytes, 0, plainBytes.Length);
            }
        }
    }
}

Die Verwendung ist einfach.

string protectedPassword = DpapiSecretProtector.ProtectToBase64(password);

// In JSON o. Ä. speichern
// settings.DbPasswordProtected = protectedPassword;

string password = DpapiSecretProtector.UnprotectFromBase64(settings.DbPasswordProtected);

Die Konfigurationsdatei kann dann zum Beispiel so aussehen.

{
  "ApiBaseUrl": "https://api.example.com/",
  "UserName": "app-user",
  "PasswordProtected": "AQAAANCMnd8BFdERjHoAwE..."
}

Das Gute an dieser Form:

  • URL und Benutzername lassen sich normal bearbeiten
  • Nur das Passwort ist geschützt
  • Die Konfigurationsstruktur bleibt übersichtlich
  • Weniger vorfallanfällig, als es im Klartext zu belassen

9. Designs, die trotzdem gefährlich sind

Selbst mit DPAPI im Einsatz sind folgende Designs noch gefährlich.

9.1. Den entschlüsselten Wert lange herumreichen

Man möchte vermeiden, den entschlüsselten Wert

  • ins Protokoll auszugeben,
  • auf dem Bildschirm anzuzeigen,
  • in eine Ausnahme aufzunehmen,
  • dauerhaft auf einem langlebigen Objekt liegen zu lassen.

„Beim Speichern verschlüsselt“ und „auch während der Nutzung sicher“ sind unterschiedliche Probleme.

9.2. Allen Installationen ein gemeinsames Geheimnis geben

Ein Design, bei dem alle Benutzer denselben API-Schlüssel besitzen, wird durch Speicherung mit DPAPI nicht grundlegend gelöst. Grund und Alternative, wohin man ausweicht, sind in 5.2 zusammengefasst.

9.3. LocalMachine wählen, weil es „bequem“ ist

Auch das kommt wirklich häufig vor. Aber „bequem“ heißt nicht „geschützt“. Die Beweggründe, die zur Wahl verleiten, und was sich dabei ausweitet, stehen in 6.2. Bei Unsicherheit die drei Zeilen aus 6.3 ansehen.

9.4. Sich mit einer eigenen Verschlüsselung in Sicherheit wiegen

Statt DPAPI folgende Implementierungen einzusetzen:

  • einen AES-Schlüssel in den Quellcode einbetten
  • einen AES-Schlüssel in einem anderen Feld der Konfigurationsdatei ablegen
  • eine „etwas verschleierte Zeichenfolge“ als Schlüssel behandeln

bringt meist nur wenig.

Zwischen „nicht Klartext“ und „sicher“ liegt eine ziemlich große Kluft.

10. Fälle, in denen DPAPI nicht ausreicht

DPAPI ist praktisch, aber nicht allmächtig. In folgenden Fällen sollte man eine andere Option in Betracht ziehen.

10.1. Wenn es auch außerhalb von Windows laufen soll

DPAPI / ProtectedData sind auf Windows ausgerichtet. Eine plattformübergreifende App lässt sich auf dieser Grundlage nicht aufbauen.

10.2. Wenn dasselbe Geheimnis auf mehreren Geräten und von mehreren Benutzern genutzt werden soll

Eine Anforderung wie „dasselbe Chiffrat auf mehreren PCs entschlüsseln“ oder „von mehreren Benutzern gemeinsam nutzen“ liegt außerhalb der Stärke von DPAPI, das an „dieses Gerät, diesen Benutzer“ bindet.

In diesem Fall sollte man ein anderes, zur Anforderung passendes Design in Betracht ziehen, etwa:

  • serverseitige Geheimnisverwaltung
  • eine Infrastruktur für Anmeldeinformationen
  • Windows-Authentifizierung / integrierte Authentifizierung
  • einen Anmeldeinformationsspeicher für die App

10.3. Wenn das zu Speichernde selbst eine Benutzeranmeldeinformation ist

Wenn das, was gespeichert werden soll, eindeutig ein Paar aus

  • Benutzername
  • Passwort

ist, ist es naheliegender, den von Windows bereitgestellten Anmeldeinformationsspeicher zu verwenden, statt es mit DPAPI in eine eigene Datei zu schreiben. Das ist in der Praxis ein häufiger Zweifelspunkt, daher hier ein Vergleich.

Aspekt DPAPI (ProtectedData) Credential Locker (PasswordVault) Credential Manager (CredWrite / CredRead)
Speicherort Eine selbst gewählte Datei (wie das Chiffrat abgelegt wird, entscheidet die App) Von Windows verwalteter Anmeldeinformationsspeicher Von Windows verwalteter Anmeldeinformationsspeicher
Speicherbares Beliebige Byte-Folge (Verbindungszeichenfolge, Token oder auch nur ein Teil der Konfiguration) Paar aus Benutzername und Passwort Anmeldeinformationen (typspezifische Struktur)
API System.Security.Cryptography WinRT-Windows.Security.Credentials Win32 (wincred.h / Advapi32.dll)
Aus Desktop-Apps nutzbar? Direkt nutzbar Nicht nur aus WinUI, sondern auch aus WPF / WinForms nutzbar (Konfiguration für WinRT-API-Aufrufe nötig) Direkt nutzbar
Synchronisation Keine Roaming zwischen Geräten über das Microsoft-Konto Keine (lokale, benutzergebundene Anmeldeinformationsmenge)
Einschränkung Praktisch keine Bis zu 20 Einträge pro App. Nicht für große Datenmengen gedacht An die aktuelle Anmeldesitzung des Tokens gebunden
Verwaltende Instanz Die App (Speicherort und ACL bestimmt sie selbst) Das Betriebssystem (Speicherort muss nicht selbst entworfen werden) Das Betriebssystem (verwaltbar über den Anmeldeinformations-Manager der Systemsteuerung)

Die Faustregel für die Wahl lautet:

  • Zu speichern ist ein Paar aus Benutzername und Passwort, und die Anzahl ist gering -> Credential Locker / Credential Manager sind die erste Wahl. Weder Speicherort noch ACL müssen selbst entworfen werden
  • Das zu Speichernde hat nicht die Form „Benutzername + Passwort“ -> Verbindungszeichenfolgen, API-Tokens, Refresh-Tokens oder Teile einer Konfigurationsdatei passen besser zu DPAPI. Damit befasst sich dieser Artikel
  • Es soll zwischen Geräten mitgenommen werden -> Das Roaming des Credential Locker greift hier. Bei DPAPI mit CurrentUser ist es dagegen gerade ein Vorteil, „nicht mitgenommen zu werden“ – hier stehen die Ziele einander entgegen
  • Viele Einträge / große Datenmenge -> Man stößt an das 20-Einträge-Limit des Credential Locker. Dann weicht man mit DPAPI auf eine eigene Datei aus

Zu beachten ist außerdem, dass auch der Anmeldeinformationsspeicher intern auf dem Schutzmechanismus des Betriebssystems aufsetzt – es handelt sich also nicht um eine Rangfolge „sicherer als DPAPI“ oder „DPAPI ist unterlegen“. Praktisch entscheidet man nach der Form dessen, was gespeichert werden soll, und danach, ob Roaming nötig ist.

Und unabhängig davon, wofür man sich entscheidet, lohnt es sich bei einer neuen App, zuerst ein passwortloses Verfahren wie Windows Hello / Passkeys zu prüfen. Braucht man von vornherein kein langlebiges Passwort, ist das die stärkste Lösung überhaupt.

Der Schwerpunkt dieses Artikels bleibt aber die praktische DPAPI-Grundlinie dafür, „auf einem Windows-Client den Konfigurationsdatei-Klartext abzuschaffen“.

11. Empfohlene Prioritätenreihenfolge in der Praxis

Zum Schluss: Ist man in der Praxis unsicher, hilft diese Reihenfolge beim Einordnen. Von oben nach unten prüfen und nur dann eine Stufe weiter absteigen, wenn die Bedingung nicht erfüllt ist.

lässt sich vermeidenlässt sich nicht vermeidenlässt sich nicht trennenlässt sich trennendieses Paar, und wenige EinträgejajaDomänen-/lokales KontoneinToken o. Ä., andere Formjaneinnur eines genügt (der Benutzer selbstoder ein dediziertes Dienstkonto)muss von mehrerenKonten entschlüsselt werdenlässt sich ausschließenlässt sich nicht ausschließenLässt sich vermeiden, dem Gerätein langlebiges Geheimnis zu geben?Priorität 1: nicht halten(Windows-Authentifizierung, kurzlebige Tokens)Lässt sich das Geheimnispro Benutzer trennen?Prüfen, ob ein gemeinsamerSchlüssel vorliegt, und das Design überarbeitenHat das zu Speichernde die FormBenutzername + Passwort? (Abschnitt 10.3)Muss es zwischen Gerätenmitgenommen werden?Ein Gerät, das über einMicrosoft-Konto synchronisiert? (Abschnitt 10.3)Roaming desCredential Locker nutzenMit DPAPI nicht mitnehmbar.Serverseitig verwalten (Abschnitt 10.2)Credential Locker /Credential ManagerMuss es zwischen Gerätenmitgenommen werden?Von wie vielen Konten aus mussdasselbe Geheimnis entschlüsselt werden?Priorität 3: DPAPI + CurrentUserBei unbeaufsichtigter Ausführung das Ladendes Profils prüfen (Abschnitt 6.4)Lässt sich ausschließen, dassandere Benutzer auf dem Gerät sind?Priorität 4: DPAPI + LocalMachineAls Ausnahmefall behandeln und begründenAuch andere Benutzer können entschlüsseln.Das Authentifizierungsverfahren überarbeiten

Abbildung 2: Reihenfolge bei der Wahl der Speichermethode. Zuerst die Form des Geheimnisses und den Roaming-Bedarf klären, dann erst zu DPAPI übergehen. LocalMachine wählt man nicht aus Bequemlichkeit, sondern als Ausnahme, wenn keine andere Option infrage kommt

Priorität 1: Von vornherein nicht halten

  • Windows-Authentifizierung
  • Integrierte Authentifizierung
  • Interaktive Anmeldung
  • Serverseitige Geheimnishaltung
  • Kurzlebige Tokens

Priorität 2: Auf benutzerbezogene Geheimnisse setzen

  • Benutzerbezogen statt gemeinsam genutzt
  • Erneuerbare Tokens statt langlebiger fester Anmeldeinformationen
  • Einen für alle Clients gemeinsamen Schlüssel vermeiden

Priorität 3: Ist lokale Speicherung nötig, DPAPI

  • Normalerweise CurrentUser
  • Speicherort pro Benutzer
  • Nur die geheimen Einträge schützen
  • Nicht im Protokoll ausgeben

Priorität 4: LocalMachine als Ausnahme behandeln

  • Ist es wirklich auf Maschinenebene nötig?
  • Kommen auf diesem Gerät keine anderen Benutzer vor?
  • Ist es als Dienstdesign angemessen?

12. Zusammenfassung

Muss eine Windows-App vertrauliche Informationen in einer Konfigurationsdatei speichern, möchte man vermeiden, sie im Klartext liegen zu lassen.

Und auf die Frage:

„Der Schlüssel wird doch ohnehin irgendwo gespeichert, ist das nicht dasselbe?“

lautet die praxisnahe Antwort so:

  • Bei eigener Verschlüsselung mit dem Schlüssel am selben Ort: ziemlich dasselbe
  • DPAPI ist nicht dasselbe
    • Die Schlüsselverwaltung lässt sich an das Betriebssystem delegieren
    • Die entschlüsselungsfähige Partei lässt sich an einen Windows-Benutzer / Computer binden
    • Ein Leck der Datei allein muss nicht sofort zu einem Leck des Geheimnisses werden
  • Allerdings löst es nicht
    • Code, der mit denselben Benutzerrechten läuft,
    • ein vollständig kompromittiertes Gerät,
    • ein langlebiges, gemeinsames Geheimnis, das gar nicht erst auf dem Client liegen sollte

Kurz gesagt: DPAPI ist keine allmächtige Festungsmauer. Aber es hat ungefähr den Effekt, die durchsichtige Fensterscheibe eines Klartext-Konfigurationsdatei gegen wenigstens ein anständiges Fenster auszutauschen.

In der Praxis von Windows-Clients ist dieser Unterschied ziemlich groß. Am besten beginnt man damit, genau diesen Punkt nicht zu verfehlen.

13. Referenzen

  • Vorheriger Artikel: Checkliste für die minimale Sicherheit bei der Windows-App-Entwicklung
  • Microsoft Learn: CryptProtectData https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata
  • Microsoft Learn: ProtectedData https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata?view=windowsdesktop-10.0
  • Microsoft Learn: DataProtectionScope https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotectionscope?view=windowsdesktop-10.0
  • Microsoft Learn: How to: Use Data Protection https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection
  • Microsoft Learn: Credential locker for Windows apps https://learn.microsoft.com/en-us/windows/apps/develop/security/credential-locker
  • Microsoft Learn: CredWrite (Win32-API des Windows Credential Manager) https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credwritew
  • NuGet: System.Security.Cryptography.ProtectedData https://www.nuget.org/packages/System.Security.Cryptography.ProtectedData
  • Microsoft-Support: Kein Zugriff mehr auf DPAPI-Daten, nachdem ein Administrator das Passwort zurückgesetzt hat https://support.microsoft.com/en-us/topic/you-cannot-access-dpapi-data-after-an-administrator-resets-your-password-on-a-windows-server-2012-based-domain-controller-4aa890cd-12b5-fe5c-9e68-06244e70673d

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Windows-App-Entwicklung

Die Wahl der Speichermethode für Anmeldeinformationen, der Speicherort für benutzerbezogene Einstellungen und der Umgang mit Protokollausgaben betreffen das Gesamtdesign einer Windows-App – ein Thema, das gut zur Windows-App-Entwicklung passt.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Was ist DPAPI?
DPAPI (Data Protection API) ist ein von Windows bereitgestellter Datenschutzmechanismus, der die Schlüsselverwaltung an das Betriebssystem delegiert und die entschlüsselungsfähige Partei an diesen Windows-Benutzer oder diesen Computer bindet. Aus C# / .NET lässt es sich über die Klasse System.Security.Cryptography.ProtectedData nutzen, ohne zusätzliche Bibliotheken hinzuzufügen. Es ist weniger eine API zur Wahl eines Verschlüsselungsalgorithmus als vielmehr eine API, die die Schlüsselverwaltung an das Betriebssystem delegiert – so trifft man den Kern besser. Es ist eine realistische Option, um Passwörter oder API-Tokens in Konfigurationsdateien nicht im Klartext liegen zu lassen.
Der Schlüssel muss doch ohnehin irgendwo gespeichert werden – ist das dann nicht dasselbe wie Klartext oder DPAPI?
Nein, das ist nicht dasselbe. Legt man den Schlüssel bei einer selbstgebauten AES-Verschlüsselung in derselben App oder Konfigurationsdatei ab, kommt das dem Klartext ziemlich nahe. DPAPI dagegen verlagert die Schlüsselverwaltung an das Betriebssystem und bindet die entschlüsselungsfähige Partei an den Windows-Benutzer oder den Computer. Dadurch ändert sich die Widerstandsfähigkeit gegenüber Vorfällen wie dem alleinigen Durchsickern der Konfigurationsdatei, dem Mitnehmen auf einen anderen PC, einem Fehlversand, einem Backup-Leck oder einer versehentlichen Aufnahme ins Repository erheblich. Der entscheidende Unterschied zu Klartext ist, dass DPAPI „die Datei lesen können“ von „das Geheimnis nutzen können“ trennen kann.
Was schützt DPAPI nicht?
Code, der mit denselben Benutzerrechten ausgeführt wird, kann grundsätzlich alles entschlüsseln, was auch dieser Benutzer entschlüsseln kann. Deshalb schützt DPAPI nicht bei einem bereits mit Malware kompromittierten Gerät, bei einer Übernahme mit Administratorrechten und auch nicht den Klartext im Arbeitsspeicher nach der Entschlüsselung. Auch ein langlebiges Geheimnis, das an alle Clients gleich verteilt wird, ist durch DPAPI-Speicherung nicht grundlegend abgesichert, da es sich, sobald es von einem einzigen Gerät extrahiert wurde, leicht auf das Gesamtsystem auswirkt. DPAPI wirkt vor allem gegen Dateidiebstahl, Fehlplatzierung, Offline-Mitnahme und den Zugriff durch andere Benutzer.
Sollte man bei DataProtectionScope CurrentUser oder LocalMachine verwenden?
Bei einer gewöhnlichen Windows-Desktopanwendung ist CurrentUser die Grundregel. Es lässt sich als Geheimnis dieses Benutzers behandeln, und man erhält die Eigenschaft, dass das reine Kopieren des Chiffrats auf einen anderen PC nicht ohne Weiteres nutzbar ist. LocalMachine lässt sich dagegen breit von Prozessen entschlüsseln, die auf diesem PC laufen, wodurch es bei gemeinsam genutzten Geräten oder Umgebungen mit mehreren Benutzern gefährlich werden kann; die Einsatzfälle beschränken sich weitgehend auf Windows-Dienste auf einer vertrauenswürdigen Einzelzweck-Maschine. Wählt man LocalMachine, weil „alle es nutzen können und das bequem ist“, gerät man damit meist später in Schwierigkeiten.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog