Suchen

Anmeldung



Konfiguration absichern

E-Mail Drucken

Konfigurations-Dateien gehören zum guten Ton bei der .NET-Entwicklung. Leider ist das Verschlüsseln der dort hinterlegten Einstellungen nicht so einfach, wie man glaubt. Wir zeigen eine Variante auf.

winzip Download cfBaseTools mit Rijndael-Verschlüsselung

Situation

Eigentlich dürfte das Thema gar keinen Eintrag bei codingfreaks Wert sein. Man erstellt eine app.config, trägt seine Einstellungen ein und hakt jetzt wie gewohnt irgendwo an, dass man diese Einstellungen verschlüsselt haben möchte. So einfach ist das aber eben nicht.

Im ASP-Bereich gibt es HowTos für das Verschlüsseln von web.config und machine.config über das Framework-Tool aspnet_regiis (z.B. hier). Das funktioniert deshlab wunderbar, weil es ja hier immer nur um eine feste Datei auf einem Webserver geht. In Windows-Forms-Anwendungen (und auch bei WPF) stellt sich das Problem anders. Hier verteilt man eine Anwendung inkl. der config-Datei auf viele Rechner.

Aber warum soll das ein Problem sein?

Der eingebaute Verschlüsselungs-Mechanismus

Als Client-Entwickler kann man auf sog. DataProtectionConfigurationProvider zurück greifen. Diese erlauben das Ver- und Entschlüsseln von ganzen Konfigurations-Sektionen einer config-Datei. Das könnte man in einer normalen App mit folgendem Code erledigen:

Listing #1: DataProtectionConfigurationProvider
1
2
3
4
5
6
7
8
9
10
11
12
Configuration cfgThis = ConfigurationManager.OpenExeConfuguration();
ConnectionStringsSection secThis;
secThis = cfgThis.GetSection("connectionStrings") as ConnectionStringsSection;
if (secThis.SectionInformation.IsProtected)
{
secThis.SectionInformation.UnprotectSection();
}
else
{
secThis.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
}
cfgThis.Save();

Das Beispiel ist ein wenig gekürzt und sollte so bitte nicht auf eigenen Code los gelassen werden! Die Vorgehensweise wird aber klar: Die ConfigurationSection-Klasse bietet über ProtectSection() die Möglichkeit, beliebige Sektionen zu verschlüsseln.

Ist es aber das, was man auch will? Einen ganzen ConnectionString z.B. will man doch gar nicht verschlüsselt sehen, weil man oftmals die Server-Adresse oder die Standard-Datenbank ändern können muss. Welchem System-Admin soll man jetzt die Vorgehensweise zum Verschlüsseln beibringen?

Ein weiteres schweres Problem ist, dass die Verschlüsselung mit den lokal hinterlegten Maschinen- bzw. Benutzer-Schlüsseln erfolgt. Man muss das ganze also auf jedem Client einzeln durchführen, weil die Clients vom Server oder dem Entwicklungsrechner verschlüsselte Werte nicht entschlüsseln könnten.

Letztlich hat sich diese Technik im Client-Bereich als schwachsinnig und praxisfremd erwiesen und wird deshalb wohl eher selten angewandt. Im Web- und Service-Serverbereich ist das etwas anderes.

Ziel

Das letztliche Ziel sollte es also sein, Teile von config-Sektionen ver- und entschlüsseln zu können, egal auf welchem Client. Das ergibt zunächst einmal die Notwendigkeit einer symmetrischen Verschlüsselung. Der Standard-.NET-Verschlüsselungs-Algorithmus für symmetrische Operationen ist RijndaelManaged. Auf diesem soll also unsere Lösung basieren.

Problem

Das Verschlüsseln mit symmetrischen Verfahren benötigt immer mindestens einen Ausgangsschlüssel (den Key), mit dem die Strings ver- und entschlüsselt werden. Das größte Problem ist nun, diesen Key irgendwie abgesichert zu hinterlegen. Man kann sich den Key wie ein Passwort für die Verschlüsselung vorstellen. Wer das Passwort kennt, kann das verschlüsselte Geheimnis auch wieder entschlüsseln (deshalb werden diese Verfahren symmetrisch genannt). Wohin aber mit dem Passwort? Wie jeder .NET-Entwickler weiß, kann man den Code einer Anwendung leicht dekompilieren und somit den Key heraus bekommen.

Es gibt leider keine einfache Lösung für das Ablage-Problem, aber neben den diversen Obfuscatoren gibt es zumindest eine Erschwerungsstufe, die aus 2 Schritten besteht:

  1. Das Passwort wird nicht direkt im Anwendugscode, sondern als Ressource in die Assembly eingebunden. Verteilt man das eigentliche Kennwort nun womöglich auf 2-3 Ressourcen-Einträge oder schreibt sich gar einen Algorithmus, der es anhand eines Bitmaps liest :-), kann man schon ein wenig mehr Sicherheit in die Sache bringen.
  2. Man reichert den Rijndael mit einem sog. Salt an. Ein Salt ist ein beliebiger String, der den Initialisierungs-Vektor des Algorithmuses zusätzlich "verunreinigt", also mehr Zufall in die Sache bringt. Wer das Salt nicht kennt, dem reicht das Wissen über den Key allein nicht aus.

Achtung! Dieser Tipp schafft keine zusätzliche Sicherheit per se, sondern erschwert höchstens das Auffinden der Schlüssel bis zu einem hoffentlich unerträglichen Maß. Man nennt solche Vorgehensweisen auch security by obscurity. Da dieses Problem aber leider .NET-immanent ist bietet sich als Alternative allerhöchstens eine eigene Infrastruktur an, die per Zugriffsrechte-Steuerung und Signierung die Verschlüsselung vornimmt. Man begibt sich schnell in den Bereich der asymmetrischen Verschlüsselungs-Verfahren, was meist schlichtweg praxisuntauglich ist.

Lösung

Wer jetzt noch dabei ist, hat sich also mit den Unzulänglichkeiten abgefunden :-). Gut! Der erste Schritt besteht darin, sich irgendeine Art von Verschlüsselungs-Helper zu bauen. Über den Download zu diesem HowTo bieten wir die Klasse cfBaseTools.RijndaelTools an, die die Ver- und Entschlüsselung weitgehend transparent durchführt.

Damit man nun aber das ganze innerhalb eigener Konfigurationen nutzen kann, muss irgendeine Form von Klasse für das Ver- und Entschlüsseln sorgen. Diese Klasse sollte am besten von System.Configuration.ConfigurationSection erben. Ich werde das Beispiel hier bewusst einfach halten und nur einen einzigen String-Wert anbieten:

Listing #2: Einen eigenen Config-Abschnitt definieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class MyConfigSetting : ConfigurationSection
{
 
    private static MyConfigSetting m_setThis = ConfigurationManager.GetSection("MyConfigSettings") 
                                               as MyConfigSetting;
    private static string m_cstrSalt = @"c0frS4lt";
    private static string m_cstrIV = "19478937h482u46!";
 
    public static MyConfigSettings Settings
    {
        get 
        {                       
            return m_setThis;                
        }
    }
 
    [ConfigurationProperty("Pass",
                           IsRequired = true)]
    public string Pass
    {
        get { return this["Pass"].ToString(); }
        set { this["Pass"] = value; }
    }
 
    public string Secret { get; set; }
 
    public string ClearPass
    {
        get
        {
            string strTmp = string.Empty;
            if (!string.IsNullOrEmpty(Pass) &&
                Secret.Length > 0)
            {
                return RijndaelTools.Decrypt(Pass, Secret, m_cstrSalt, "SHA1", 2, m_cstrIV, 256);
            }
            return Pass;
        }
    }
}

Der Schlüssel ist, später immer die Eigenschaft ClearPass zu verwenden, wenn man sich einen ConnectionString aufbauen möchte. Das Problem, das man jetzt noch hat ist, dass man bei manchen Projekten mehr als eine DB-Verbindung konfigurieren möchte. Auch dafür gibt es in .NET eine Basisklasse - die ConfigurationSectionGroup.

Ich leite also eine weitere Klasse ab und benenne Sie MyConfSettings (Plural), um darauf hinzuweisen, dass sie als Container für MyConfSetting dienen soll:

 

Listing #3: ConfigurationSectionGroup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyConfSettings : ConfigurationSectionGroup
{
 
    private static MyConfSettings m_setThis = null;
 
    /// <summary>
    /// Provides access to the settings.
    /// </summary>
    public static DatabaseConfigurationSettings Settings
    {
        get 
        {
            if (m_setThis == null)
            {
                System.Configuration.Configuration conf = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
                m_setThis = conf.GetSectionGroup("MyConfSettings") as MyConfSettings;
            }
            return m_setThis; 
        }
    }
 
    public MyConfSettings GetSettings(string strKey)
    {
        return this.Sections[strKey] as MyConfSetting;
    }
 
}

 

Wie man sieht, ist auch dieser Code recht eingängig. Wichtig zu verstehen ist, dass der Key, der in Zeile 24 verwendet wird, der Name des MyConfSetting-Tags in der App.config sein soll. Eine Konfigurations-Datei mit 2 Settings könnte wie folgt aussehen:

 

Listing #4: App.config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8" ?>
<configuration>  
  <configSections>
    <sectionGroup name="DBSettings" type="MyProjectNameSpace.MyConfSettings, 
					  MyProjectAssemblyName">
      <section name="sqls01" 
               type="MyProjectNameSpace.MyConfSetting, MyProjectAssemblyName"/>
      <section name="sqls02" 
               type="MyProjectNameSpace.MyConfSetting, MyProjectAssemblyName"/>
    </sectionGroup>
  </configSections>
  <DBSettings>
    <sqls01 Type="sqls" 
            ProviderName="System.Data.SqlClient" 
	    Server=".\SQLEXPRESS" 
	    DataSource="AdventureWorks" 
	    IntegratedSecurity="false" 
	    User="test_user" 
	    Pass="uJ2Z7q11gp2323BcsKY5Kf6WGA==" />    
    <sqls02 Type="sqls" 
	    ProviderName="System.Data.SqlClient" 
	    Server=".\MSSQL1" 
	    DataSource="Northwind" 
  	    IntegratedSecurity="false" 
	    User="test_user" 
	    Pass="uJ2Z7q11gp2323BcA==" />
  </DatabaseConfigurationSettings>
</configuration>

 

Im Abschnitt <configSections /> macht man der Konfiguration zunächst klar, dass man eine SectionGroup vom Typ MyProjectNameSpace.MyConfSettings mit dem Namen "DBSettings" nutzen möchte. Der Name der eigentlichen Sektion ist also frei wählbar, solange man ihm im Attribut "type" mitteilt, was für eine Klasse sich um das Handling der Sektion kümmert. Innerhalb der SectionGroup kann man nun <section />-Tags benutzen. Jede Sektion wird sozusagen deklariert und über ihr "name"-Attribut eindeutig gemacht.

Das Nutzen ab Zeile 12 ist nun kalter Kaffee und sollte keine große Hürde darstellen. Interessant hieran ist nur, dass wir jeweils die verschlüsselten Kennwörter benutzen. Um den verschlüsselten Wert zu erhalten, kann man die Klasse Rijndael-Tools (Link oben) mit dem jeweils richtigen Secret benutzen. Die Klasse generiert immer Base64-Zeichenfolgen, sodass das Ergebnis keine UTF-vermurksten Sonderzeichen enthalten kann.

Zum Schluss muss man nun die Settings nun nur noch bekommen:

 

Listing #5: Settings auslesen
1
2
MyConfSetting setThis = MyConfSettings.Settings.GetSettings("sqls01");
Console.WriteLine(setThis.ClearPass);

 

Das sollte das Kennwort im Klartext auf der Konsole ausgeben.

Hinweis

Ich stelle den Quellcode bewusst nicht als Solution bereit, um niemanden zu verleiten, meine Implementierung gedankenverloren oder im Streß "einfach mal schnell" zu benutzen. Es handelt sich hier um extrem sicherheitskritischen Code und ich empfehle daher, sich genau mit dem Thema auseinander zu setzen! Das Prinzip wird aber denke ich klar.

 

 

 

Zuletzt aktualisiert am Donnerstag, den 13. Mai 2010 um 12:36 Uhr