Donnerstag, 24. Mai 2012


Artikel

Oktober 2003 | Artikel

Hinter Schloss und Riegel

(Link zum Artikel: http://www.entwickler.de/dotnet//000444)

Kryptographie in .NET - Teil 1: Symmetrische Verschlüsselung

Text: von Tobias Grasl
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Für den Entwickler ergibt sich in diesen Zeiten der serviceorientierten Architektur auch in eigenen Anwendungen immer öfter die Notwendigkeit, Daten über unsichere Kommunikationswege zu verteilen oder auf ungeschützten Systemen zu speichern. In diesem Fall muss er selbst dafür sorgen, dass die Daten nicht gefährdet sind. Genau dieses Thema wollen wir uns daher in dieser und der folgenden Ausgabe des dot.net magazins vornehmen.

Im Mai 2001 schloss das Europäische Parlament seine Untersuchungen zum anglo-amerikanischen Spionagenetzwerk Echelon ab. Der Bericht enthält die Empfehlung, dass sowohl Unternehmen als auch Privatpersonen ihren eMail-Verkehr routinemäßig verschlüsseln sollten, um sich vor den ungewollten Augen dieses Systems zu schützen [1]. Es gibt einige gute Programme, wie etwa PGP oder GPG, mit denen man genau das tun kann.

Konzepte
Jedermann weiß, dass Kryptographie dazu verwendet werden kann, Daten vor ungewolltem Lesen zu schützen. Mit der Weiterentwicklung der modernen Computerwissenschaft haben sich aber neben diesem Anwendungsbereich noch andere aufgetan, die mitunter noch wichtiger sind. Bevor wir uns in die Details der Implementierung vertiefen, sollten wir daher die wichtigsten Anwendungen der Kryptographie besprechen. Später zeige ich Ihnen, wie die einzelnen Anwendungen mit .NET realisiert werden und wie Sie sichere Krypto-Systeme erstellen können, um dieses Ziel zu erreichen.
  • Verschlüsselung: Ob ich Daten in meinem lokalen System speichere oder sie über ein Netzwerk verteile - mir ist lieber, dass sie nur von vertrauten Parteien gelesen werden können. Um dieses Ziel zu erreichen, muss ich meine Daten verschlüsseln - danach können sie nur von Parteien, die im Besitz des geeigneten Schlüssels sind, gelesen werden. Wenn Sie zum Beispiel im Internet mit Kreditkarte bezahlen, werden Ihre Daten verschlüsselt an den Verkäufer geschickt.
  • Authentisierung: Bei einem Anruf kann ich zumeist über die Stimme des Anrufers feststellen, wer am anderen Ende der Leitung ist. Bei elektronischer Kommunikation ist es nicht so einfach, die Identität der anderen Partei zu verifizieren. Genau zu diesem Zweck dient die Authentisierung. Wenn Sie eine geschützte Website aufrufen, stellt ihr Browser über ein so genanntes Zertifikat sicher, dass es sich tatsächlich um die gewünschte Gegenpartei handelt.
  • Datenintegrität: Manchmal liegt das Wissen im Detail. Wenn ich meinem Broker die Nachricht Kaufen sie MSFT im Wert von $10.000 schicke, er aber die Nachricht Kaufen sie MSFT im Wert von $100.000 empfängt, hat dies für mich möglicherweise unangenehme Folgen. Mit Hilfe der Kryptographie kann ich sicherstellen, dass mein Broker meine Nachricht verifizieren kann. Dies schützt mich einerseits vor einer schlechten Leitung, andererseits aber auch vor der Manipulation meiner Daten durch eine dritte Partei.
  • Nachweisbarkeit: Wenn ich einen Vertrag für ein neues Auto unterschrieben habe, ist dies für den Verkäufer relativ einfach nachzuweisen - meine Unterschrift genügt. Beim elektronischen Austausch von Dokumenten wird das gleiche mithilfe von Kryptographie und digitalen Signaturen erreicht. Diese Funktion geht zumeist mit der Authentisierung einher - da die Signatur meine Identität bestätigt, macht sie es mir im Nachhinein auch schwer, die Herkunft der Daten zu leugnen.
Die Basis der Kryptographie liegt in der Mathematik und die aufgezählten Aufgaben werden mittels mathematischer Algorithmen erreicht, die sich bisher als kryptographisch sicher erwiesen haben. Kryptographie ist aber eine umfangreiche Fachrichtung und entwickelt sich stets weiter. Ein Algorithmus, der heute als sicher gilt, kann morgen mit Leichtigkeit geknackt werden.

Es gibt nur einen Mechanismus, der auf immer garantierte Sicherheit gewährt - den so genannten One-Time Pad. Der Vorgang ist sehr einfach: um eine n-bit lange Nachricht M zu verschlüsseln, nehmen sie einen zufällig erzeugten n-bit Schlüssel K und berechnen die verschlüsselte Nachricht C=M?K (? ist die XOR-Funktion). Um die Nachricht zu entschlüsseln, verwenden Sie ganz einfach M=C?K. Danach verwerfen Sie den Schlüssel K. Die Sicherheit des One-Time Pad liegt darin, dass der Schlüssel gleich lang ist wie die Nachricht und nur einmal zur Anwendung kommt. Da es für jede n-bit lange Nachricht M' einen n-bit Schlüssel K' gibt, für den C=M'?K' gilt, ist es ohne Kenntnis des tatsächlichen Schlüssels nicht möglich, die ursprüngliche Nachricht zu entziffern. Leider ist dieser Mechanismus nicht praktisch, da Sie so viele Schlüsselbits sicher austauschen müssten, wie Sie Datenbits senden wollen. Dieser Mechanismus wurde angeblich zur sicheren Kommunikation zwischen den Supermächten im Kalten Krieg verwendet [2], da diese leicht im vornhinein die Schlüsselbits physisch austauschen konnten - uns würde das schwer fallen.

Somit ist garantierte Sicherheit nicht möglich. Sie können es einem Angreifer aber unerschwinglich teuer machen, Ihre Daten und Nachrichten zu lesen oder sich elektronisch als Sie auszugeben. .NET bietet uns dafür zahlreiche Algorithmen. Die Algorithmen allein sorgen nicht für ein sicheres System. Dazu ist ein Protokoll nötig, das definiert, wie Schlüssel erzeugt, ausgetauscht und zerstört werden und wie die Kommunikation zwischen den beteiligten Parteien abläuft. Ein derartiges Protokoll wird als Kryptographisches Protokoll bezeichnet. In diesem Artikel stelle ich Ihnen Methoden vor, mit denen Sie Daten verschlüsseln und Datenintegrität erreichen können. Authentisierung und Nachweisbarkeit sind Themen für einen weiteren Artikel, in dem ich auch Kryptographische Protokolle besprechen werde.
Der System.Security.Cryptography-Namespace
Die für die Kryptographie relevanten Klassen befinden sich im .NET Framework im Namespace System.Security.Crytography. Es handelt sich dabei um eine umfangreiche Sammlung von Algorithmen, mit denen Sie all die oben genannten Anwendungen der Kryptographie realisieren können. Diese Algorithmen sollten in den meisten Szenarien ausreichend sein - sie entsprechen den aktuellen Standards und sind für allgemeine Daten sicher genug. Wie erwähnt entwickelt sich die Kryptographie aber immer weiter, daher ist das Security Framework sehr flexibel und kann sowohl über neue Algorithmen als auch über bessere Implementierungen dieser Algorithmen leicht erweitert werden.

Um diese Flexibilität zu erreichen, sind die Algorithmen in diesem Namespace hierarchisch in drei Ebenen geteilt: Auf der obersten, abstrakten Ebene liegen Klassen, die jeweils einen algorithmischen Typ darstellen: zum Beispiel SymmetricAlgorithm, die Oberklasse für alle Klassen, die symmetrische Algorithmen (eine Erklärung dazu später) implementieren. Klassen der mittleren Ebene sind ebenfalls abstrakt, repräsentieren aber jeweils einen spezifischen Algorithmus. Abgeleitet von SymmetricAlgorithm finden wir zum Beispiel die Klasse DES, die den DES-Algorithmus darstellt.

Die unterste Ebene enthält dann konkrete Klassen, die die Algorithmen implementieren. Zum Beispiel leitet die Klasse DESCryptoServiceProvider von der Klasse DES ab und implementiert den DES-Algorithmus. Die in dieser Ebene liegenden Klassen folgen einer Nomenklatur, die es uns ermöglicht, die Art der Implementierung zu erraten: Endet eine Klasse mit CryptoServiceProvider, so wurde sie mittels eines Cryptographic Service Providers des Windows CryptoAPI implementiert, endet sie mit Managed, wurde sie mittels Managed Code implementiert. Diese Architektur ermöglicht es dem Programmierer, einfach über die statische Factory DES.Create() eine Instanz der Klasse DES zu erzeugen. Die konkrete Klasse dieser Instanz wird durch die Konfiguration des Cryptography Frameworks bestimmt und kann vom Administrator jederzeit geändert werden - zum Beispiel, um die neu erworbene DES-Karte, die viel schneller ist, zu benutzen. In diesem Artikel zeige ich ihnen die folgenden Algorithmustypen und die dazugehörigen Implementierungen:
  • System.Security.Cryptography.SymmetricAlgorithm
  • System.Security.Cryptography.HashAlgorithm
  • System.Security.Cryptography.RandomNumberGenerator
Ein weiterer wichtige Typ ist System.Security.Cryptography.AsymmetricAlgorithm. Diesen werde ich Ihnen in meinem nächsten Artikel vorstellen.
SymmetricAlgorithm
Symmetrische Algorithmen sind eine sehr wichtige Klasse von kryptographischen Algorithmen, die hauptsächlich zur Verschlüsselung eingesetzt werden. Aber was genau ist ein symmetrischer Algorithmus? Die Definition ist wie folgt: Ein kryptographischer Algorithmus ist symmetrisch, wenn der Schlüssel für die Entschlüsselung aus dem Schlüssel für die Verschlüsselung berechnet werden kann, und umgekehrt [2]. Eine Partei, die den Schlüssel für die Verschlüsselung besitzt, kann also relativ einfach den Schlüssel für die Entschlüsselung berechnen und somit die Daten lesen. Daher müssen bei symmetrischen Algorithmen beide Schlüssel geheim gehalten werden. In der Praxis ist es ohnehin zumeist der Fall, dass die beiden Schlüssel identisch sind.

Der einfachste Angriff gegen einen Algorithmus dieser Art ist zumeist die Brute Force Attack. Dabei probiert der Angreifer, mit jedem möglichen Schlüssel die Nachricht zu entschlüsseln, bis er einen Schlüssel findet, mit dem es funktioniert. Die Sicherheit des Algorithmus liegt also neben der Komplexität der Entschlüsselung in der Vielzahl der möglichen Schlüssel. Da der Angreifer allerdings keine Basis hat, um den Schlüssel auf eine andere Art und Weise zu finden - wie etwa bei asymmetrischen Algorithmen, wo er den Public Key hat - kann die Schlüssellänge, verglichen mit asymmetrischen Algorithmen, dennoch relativ gering sein. Der große Vorteil dieser Algorithmen liegt daher in der Performanz der Verarbeitung. Dies ist mit ein Grund, warum Sie asymmetrische Algorithmen im Allgemeinen nur dazu einsetzen, um einen Schlüssel für einen symmetrischen Algorithmus auszutauschen - den so genannten Session Key. Danach verwenden Sie bei der eigentlichen Kommunikation diesen Schlüssel, um große Datenmengen performant verarbeiten zu können. Wie Sie dem Klassendiagramm in Abpictureung 1 entnehmen können, stellt das .NET Framework vier verschiedene symmetrische Algorithmen zur Verfügung:
  • DES (Data Encryption Standard): Dieser Algorithmus wurde von IBM entwickelt und 1976 von der US-Behörde National Bureau of Standards (NBS, heute National Institute of Technology, NIST) standardisiert. Der Algorithmus wurde von der NSA (National Security Agency) evaluiert und geändert, was zum Verdacht geführt hat, dass sie eine Hintertür eingebaut hat. Der Algorithmus ist heute auf jeden Fall nicht mehr als sicher einzustufen - schon 1993 kostete eine Maschine, die den Algorithmus in durchschnittlich 3.5 Stunden knacken konnte, nur 1 Million Dollar [2].
  • Rijndael: Joan Daemen und Vincent Rijmen, beide aus Belgien, haben diesen Algorithmus entwickelt und für den Advanced Encryption Standard (AES) nominiert, den die NIST 1997 suchte. Der Algorithmus gewann diesen Wettbewerb und wurde somit 2001 zum Standard erklärt, der DES ersetzen soll [3].
  • RC2: RC steht für Ron´s Code oder Rivest Cipher. Er wurde von Ron Rivest (Mitentwickler des RSA-Algorithmus und Mitbegründer von RSA Security) entwickelt und die Details des Algorithmus wurden lange von RSA Security geheimgehalten. Um Teil des S/MIME-Standards zu werden, wurde der Algorithmus 1997 veröffentlicht.
  • TripleDES: eine Variation von DES, bei der wie der Name besagt, dreimal DES eingesetzt wird, wodurch der Schlüssel dreimal so lang sein kann.
Alle diese Algorithmen sind so genannte Block Ciphers. Die Daten werden beim Verschlüsseln in Blöcke gleicher Größe eingeteilt und jeder dieser Blöcke wird dann separat verarbeitet. Die Größe der Blocks hängt vom Algorithmus ab: DES, TripleDES und RC2 verwenden jeweils 64-blocks, Rijndael kann unabhängig von der Länge des Schlüssels 128, 192 oder 256-bit Blocks einsetzen. Die Standardwerte sind 128-bit Blocks bei einer Schlüssellänge von 256 Bit. Block Ciphers können in verschiedenen Modi angewendet werden (siehe Kasten Block Cipher-Modi). Falls sie keinen besonderen Grund haben einen anderen Modus einzusetzen, sollten sie grundsätzlich immer den .NET Default-Modus CBC einsetzen, da dies der sicherste verfügbare Modus ist.

Block Cipher-Modi
Block Cipher können in verschiedenen Modi arbeiten, die bestimmen, wie die veschlüsselten Blocks voneinander abhängen. Dabei gibt es die folgenden Modi:
ECB
(Electronic Code Book): Bei diesem Modus wird jeder Block ganz unabhängig von allen anderen Blocks verarbeitet, d.h. wenn die Funktion Ek die Verschlüsselung mit dem Schlüssel K bezeichnet, Mi der i-te Inputblock ist und Ri der entsprechende Outputblock, dann gilt Ri = Ek(Mi). Dabei entsteht das Problem, dass zwei gleiche Inputblocks den gleichen Outputblock ergeben, was die Sicherheit des Systems erheblich beeinträchtigt.
CBC
(Cipher Block Chaining): In diesem Modus gilt Ri = Ek(Mi?Ri-1), wobei ? die XOR-Funktion bezeichnet. Dadurch hängt jeder Outputblock von allen vorangegangenen Daten ab, womit nun zwei gleiche Blöcke nicht unbedingt das gleiche Resultat erzeugen - außer wenn sie beide der erste Block einer Datei sind. Um auch dieses Problem zu umgehen, wird ein sogenannter Initialization Vector (IV) eingesetzt, der mit dem ersten Block verknüpft wird und sich von Nachricht zu Nachricht unterscheidet.
CFB
(Cipher Feedback): Cipher Feedback sorgt ähnlich wie CBC dafür, dass jeder Block von allen vorangegangenen abhängt. Mit diesem Vorgang ist es aber auch möglich, die Nachricht in kleineren Abschnitten als der Blockgröße (wie etwa 8-bit) zu verarbeiten, was zum Beispiel bei manchen Netzwerkanwendungen wichtig ist.
CTS
(Cipher Text Stealing): Beim Einsatz dieses Modus ist der Output immer genauso lang wie der Input. Der Vorgang ist fast der gleiche wie bei CBC, nur die letzten zwei Blocks werden so manipuliert, dass die resultierende Cipher gleich lang wie der Input ist.
OFB
(Output Feedback): Dieser Modus ähnelt CFB nicht nur im Namen. Der Unterschied besteht darin, wie die Blocks in kleinere Einheiten geteilt werden. Dies sind alle von .NET zur Verfügung gestellten Modi. Der Default ist CBC, da dies auch der sicherste der vorhandenen Modi ist.
Anwendung
In der Praxis sind symmetrische Algorithmen sehr einfach anzuwenden. Beispiel 1 (auf der beiliegenden CD) enthält das Programm Cryptor.cs, mit dem Dateien verschlüsselt und entschlüsselt werden können. Wir werden anhand dieses Beispiels den Einsatz symmetrischer Algorithmen sehen. Cryptor hat vier Parameter - drei Dateipfade und ein Flag. Die erste Datei enthält den Schlüssel, der zweite Pfad zeigt auf die zu verarbeitende Datei und der dritte auf die Datei, in die die verabeiteten Daten geschrieben werden sollen. Das Flag ist entweder encrypt oder decrypt, je nachdem, ob ver- oder entschlüsselt werden soll. Die erste für uns interessante Zeile ist
  1. Rijndael rijndael = Rijndael.Create();
Dieser Aufruf liefert uns ein Objekt, welches den Rijndael-Algorithmus implementiert. Per Default ist dies ein Objekt der Klasse System.Security.Cryptography.RijndaelManaged, dies kann aber in der Konfiguration des Cryptography Service geändert werden.

Wie bei allen .NET-Algorithmusklassen enthält das so erzeugte Objekt einen zufällig erzeugten Schlüssel und Initialwert (siehe Kasten Block Cipher-Modi). Zum Entschlüsseln der Daten brauchen wir aber den gleichen Schlüssel und daher wird durch
  1. initialiseKey(rijndael, keyFileName);
die Funktion initialiseKey aufgerufen. Diese schreibt entweder den erzeugten Schlüssel in die genannte Datei, falls diese noch nicht existiert, oder liest den vorgefertigten Schlüssel. In diesem Fall setzen wir den Schlüssel ganz einfach mit
  1. rijndael.Key = buffer;
Ähnliches gilt auch für den Initialization Vector (IV). Dieser muss zwar nicht geheim gehalten werden, wir müssen aber beim Entschlüsseln den gleichen Wert verwenden wie beim Verschlüsseln. Daher schreiben wir beim Verschlüsseln diesen Wert an den Anfang der Zieldatei, um ihn beim umgekehrten Verfahren wieder aus dieser lesen zu können. Somit sind sowohl der Schlüssel als auch der IV bei unserem Algorithmus gesetzt und es steht dem Verarbeiten der Daten nun nichts mehr im Wege. Um dieses Ziel zu erreichen, verwenden wir ein Objekt des Typs System.Security.Cryptography.ICryptoTransform. Wir erhalten dieses Objekt je nachdem, ob wir ver- oder entschlüsseln wollen, über
  1. transform = rijndael.CreateEncryptor();
oder
  1. transform = rijndael.CreateDecryptor();
Dieses Interface hat zwei Funktionen: TransformBlock und TransformFinalBlock. Beide verarbeiten einen Block, die letztere allerdings den letzten Block, der möglicherweise auf die korrekte Länge aufgepolstert werden muss. Wir müssen dieses Interface allerdings nie direkt einsetzen, da wir unserer Daten über einen CryptoStream verarbeiten wollen.

Diese Klasse leitet von System.IO.Stream ab und kann prinzipiel als Filter vor jeden Stream gestellt werden. Je nach Modus und Transform werden dann die gelesenen bzw. geschriebenen Daten entweder entschlüsselt oder verschlüsselt. Dieser CryptoStream wird mit
  1. CryptoStream cStream =new CryptoStream(fileStream,transform,CryptoStreamMode.Write);
erzeugt. Ein CryptoStream ist immer unidirektional, d.h. es kann von diesem Stream entweder nur gelesen oder nur geschrieben werden. Neben dem Stream, der gefiltert werden soll, und dem zu verwendenden ICryptoTransform wird also auch ein CryptoStreamMode mit übergeben. In diesem Beispiel verwenden wir sowohl beim Verschlüsseln als auch beim Entschlüsseln einen CryptoStream im Modus Write. Zuletzt müssen wir nur noch die Daten schreiben und den Stream schließen (in diesem Fall besonders wichtig, da der CryptoStream den letzten Block erst beim Aufruf von Flush oder Close verarbeitet):
  1. cStream.Write(content,offset,length);
  2. cStream.Close();
Wenn sie nun Cryptor aufrufen, geben sie einfach beim ersten Mal den Pfad auf eine nicht existierende Datei an, dann die zu verschlüsselnde Datei und die Zieldatei und zuletzt das Flag encrypt, z.B:
  1. Cryptor key test.txt cipher encrypt
Mit diesem Aufruf werden die Daten aus der Datei test.txt im aktuellen Verzeichnis in die Datei cypher verschlüsselt und der Schlüssel kommt in die Datei key. Um die Daten dann wieder in decipher.txt zu entschlüsseln, rufen wir
  1. Cryptor key cipher decipher.txt decrypt
auf. Dies liest aus der Datei key den Schlüssel und verarbeitet dann die Daten, um sie in die wieder entschlüsselte Datei decipher.txt zu schreiben.

Somit haben wir ein Program, mit dem wir unsere Daten verschlüsselt speichern und wieder lesen können. Wenn sie die Datei key auf einer Diskette oder Ihrem USB-Storage speichern, dann muss ein Angreifer sowohl Zugriff auf Ihren Computer als auch auf diesen Speicher haben, um Ihre Daten lesen zu können! Da wir den Algorithmus auf einen Stream angewandt haben, ist es ganz einfach, das Beispiel so zu ändern, dass die verschlüsselten Daten nicht in eine Datei geschrieben, sondern über ein Netzwerk verschickt werden. Dies ist der erste Baustein eines kryptographischen Systems.
RandomNumberGenerator
Es erscheint vielleicht seltsam, dass der .NET Cryptography Service einen Zufallszahlengenerator enthält - schließlich haben wir ja die klasse System.Random, die genau zu diesem Zweck dient. Zufallszahlen werden in der Kryptographie sehr häufig eingesetzt, hauptsächlich, um Schlüssel und Initialization Vectors zu erzeugen. Herkömmliche Generatoren reichen hierzu nicht aus, da sie sowohl reproduzierbar als auch voraussagbar sind. Zusätzlich sollte ein kryptographisch sicherer Generator alle möglichen Werte (z.B. Schlüssel) mit der gleichen Wahrscheinlichkeit erzeugen - ist dies nicht der Fall, kann ein Angreifer die Werte mit der größten Wahrscheinlichkeit zuerst probieren.

RandomNumberGenerator ist, wie SymmetricAlgorithm, eine abstrakte Klasse. Die eigentliche Implementierung des Generators bleibt den Unterklassen überlassen und .NET stellt uns mit RNGCryptoServiceProvider einen Generator zur Verfügung, der auf die Funktionen des Windows CryptoAPI zurückgreift. Der Generator wird mit der statischen Methode RandomNumberGenerator.Create() erzeugt, die in der Standardeinstellung eine Instanz von RNGCryptoServiceProvider erzeugt. Mit RandomNumberGenerator.getBytes(byte[]) wird dann der übergebene Byte Array mit zufällig erzeugten Bytes gefüllt. Zumeist wird der Generator dazu verwendet, Zufallswerte für andere Algorithmen zu erzeugen - oft geschieht dies intern, wie etwa bei der automatischen Erzeugung eines Schlüssels, wenn wir einen symmetrischen Algorithmus instanziieren. Wir werden den Generator im zweiten Beispiel, nach dem nächsten Abschnitt, in der Praxis einsetzen.
PasswordDeriveBytes
Kein kryptographischer Algorithmus kann Sicherheit garantieren - Sie können es einem Angreifer aber unerschwinglich teuer machen, Ihr Geheimnis elektronisch zu knacken. Im vorangegangenen Beispiel haben wir den Schlüssel in eine Datei gespeichert, um ihn später wiederfinden zu können. Ihre verschlüsselten Daten sind dabei nur so sicher wie diese Schlüsseldatei - ein Angreifer, der diese besitzt, kann Ihre Daten lesen. Zusätzlich dürfen Sie diese Datei auch nicht verlieren, da Sie ansonsten selbst die Daten nicht mehr lesen könnten! Am besten hinterlegen Sie eine Kopie in ihrem Bankschließfach.

Manchmal ist es einfach nicht praktisch, eine solche Datei immer bei sich zu haben, wenn man die Daten lesen will. Andererseits kann ich mir einen 256-bit Schlüssel schwer merken (in meiner Schlüsseldatei steht in Hexadezimalform 567D 4FA8 3F62 4455 F997 69DB BA28 E356 8655 C739 E8F5 3B9F 16FD A357 8E23 C5BD). Genau für solche Fälle ist die Klasse PasswordDeriveBytes gedacht. Sie dient dazu, aus einem Passwort einen kryptographisch sicheren Schlüssel zu erzeugen. Der Algorithmus dieser Klasse basiert auf dem PBKDF1-Algorithmus, der im PKCS #5 v2.0 Standard definiert und im IETF RFC 2898 dokumentiert ist [4]. Er nimmt zur Berechnung des Schlüssels vier Parameter: das Passwort, einen so genannten Salt Value, der das Knacken des Schlüssels erschwert, einen Iterationszähler n und einen kryptographischen Hash-Algorithmus. Um den Schlüssel zu berechnen, wird der Hash-Algorithmus n Mal auf eine Kombination des Passworts und des Salt Values angewandt. Wie der IV, der bei Block-Algorithmen angewandt wird, sollte der Salt Value zufällig erzeugt sein, muss aber nicht geheim gehalten werden. Es liegt also nahe, diesen Salt Value in der verschlüsselten Datei zu speichern, damit wir ihn von dort wieder lesen können. Dies hat einen zusätzlichen Vorteil: wir können für jede Datei einen eigenen Salt Value verwenden und erhalten somit jedes Mal einen anderen Schlüssel. Sollte der Schlüssel für eine Datei, nicht aber das Passwort geknackt werden, so kann der Angreifer nur diese eine Datei lesen.

Der Konstruktor nimmt als Parameter das Passwort und den Salt Value. Der Hash-Algorithmus und Zähler haben Defaultwerte, und zwar SHA-1 und 100, können aber mit HashName und IterationCount gesetzt werden. Die Defaults sollten reichen, es sei denn, sie müssen mit einem System kompatibel sein, welches andere Werte verwendet. Der gewünschte Schlüssel wird dann ganz einfach mit getBytes(m) geholt, wobei m die gewünschte Schlüssellänge ist. Um zum Beispiel für unseren Rijndael-Algorithmus einen 256-bit Schlüssel zu erzeugen, reicht der folgende Quelltext:
  1. byte[] salt;
  2. PasswordDeriveBytes deriveBytes = new PasswordDeriveBytes(passwort, salt);
  3. byte[] key = deriveBytes.getBytes(32);
In unserem zweiten Beispiel, CryptorPW.cs (siehe CD), verwenden wir diese Methode, um aus einem Passwort einen Schlüssel zu erzeugen. Dabei wird jedes Mal ein neuer Salt Value verwendet, den wir wie den IV mit in die verschlüsselte Datei speichern und beim Entschlüsseln wieder aus dieser lesen. Wenn Sie einen solchen Algorithmus einsetzen, müssen Sie bedenken, dass das System viel leichter anzugreifen ist. Der Angreifer muss nun nicht mehr jeden möglichen Schlüssel probieren, sondern kann sich auf alphanumerische Kombinationen beschränken.

Das Passwort für PasswordDeriveBytes kann beliebig lang sein - wählen Sie also einen ganzen Ausdruck, der nicht bekannt ist, den Sie sich aber leicht merken können.
HashAlgorithm
Hash-Algorithmen sind eine weitere wichtige Kategorie von Algorithmen. Sie werden zur Sicherstellung der Datenintegrität verwendet. Diese Algorithmen basieren auf dem mathematischen Konzept der Einwegfunktion, haben aber noch zusätzliche Eigenschaften, die sehr wichtig sind. Insgesamt muss ein Hash-Algorithmus H folgenden Kriterien gerecht werden:
  • Für eine Nachricht M ist es einfach, H(M) zu berechnen, aber sehr schwierig, aus H(M) M zu berechnen (Einweg). Somit kann ein Angreifer, der H(M) kennt, keine Rückschlüsse auf M ziehen.
  • Für eine Nachricht M beliebiger Länge produziert H immer einen Hash der gleichen Länge n und wird als n-bit Hash bezeichnet. Dies macht es viel einfacher, Hash-Werte auszutauschen und zu verifizieren.
  • Es ist sehr schwierig, zwei Nachrichten M und N zu finden sodass H(M) = H(N) gilt. Ein Angreifer kann also nicht M mit N ersetzen, ohne dass Sie das erkennen, wenn Sie den Hash-Wert H(M) verifizieren.
Diese Eigenschaften bedeuten gemeinsam, dass wir Hash-Algorithmen einsetzen können, um Daten auf Fehler in der Übertragung oder Manipulation durch einen Angreifer zu überprüfen. Der Hash Wert wird gemeinsam mit der Nachricht verschlüsselt übertragen und der Empfänger kann verifizieren, dass die entschlüsselte Nachricht tatsächlich den gleichen Hash ergibt. Dies bedeutet, dass ein Angreifer, der Nachricht und Hash nicht entschlüsseln kann, diese auch nicht manipulieren kann - etwa durch das Entfernen eines verschlüsselten Blocks.
Wie Sie dem Klassendiagram in Abpictureung 2) entnehmen können, bietet .NET fünf verschiedene Hash-Algorithmen, die keinen Schlüssel benötigen. Daneben bietet .NET auch zwei so genannte Keyed Hash Algorithms, also Hash-Algorithmen, die einen Schlüssel benötigen. Diese besprechen wir ein anderes Mal. MD5 wurde von Ron Rivest entwickelt und ist eine 128-bit Hash-Funktion. MD steht dabei für Message Digest, ein anderer Name für einen Hash-Wert. SHA1 (Secure Hash Algorithm) wurde von NIST und NSA entwickelt, um mit dem Digital Signature Standard (DSS) verwendet zu werden. Dies ist ein 160-bit Hash-Algorithmus SHA256, SHA384 und SHA512 sind für die Verwendung mit AES (also Rijndael) gedacht und produzieren Hash-Werte, die jeweils zwei Mal so lang sind wie die erlaubten Schlüssellängen dieses Algorithmus. Die Algorithmen sind so konzipiert, dass ein Angriff gegen den Hash-Wert in etwa so viel Aufwand braucht wie ein Angriff gegen den korrespondierenden Rijndael-Algorithmus. Da wir in unserem Beispiel einen Rijndael mit 256-bit Schlüssel verwenden, setzen wir als Hash Algorithms SHA512 ein.

Die Klasse HashAlgorithm bietet drei Versionen der Funktion ComputeHash, mit denen sie direkt den Hash-Wert eines byte[] oder eines Stream berechnen können. Daneben implementiert HashAlgorithm auch ICryptoTransform und kann mit einem CryptoStream verwendet werden, um die Daten in einem Stream zu filtern. Anders als bei den bisherigen Transforms gibt HashAlgorithm einfach die Daten, die übergeben werden, unverändert weiter. Den Hash-Wert selbst können Sie dann aus dem Feld Hash lesen. In unserem letzten Beispiel, CryptorPWHash.cs (ebenfalls auf der CD), zeige ich Ihnen, wie Sie diesen Algorithmus verwenden können. Dabei wird sowohl beim Lesen der Datei als auch beim Schreiben der verarbeiteten Daten ein ICryptoTransform eingesetzt - auf der einen Seite ein HashAlgorithm, auf der anderen Seite die von SymmetricAlgorithm erhaltene Instanz. Welcher liest oder schreibt, hängt davon ab, ob ver- oder entschlüsselt wird - es muss auf jeden Fall immer die nicht verschlüsselte Nachricht durch den HashAlgorithm gefiltert werden.
Fazit
Sie können jetzt also Daten sowohl verschlüsseln als auch die Integrität dieser Daten sicherstellen. Diese zwei Aufgaben sind sehr nützlich und wenn Sie, wie in unseren Beispielen, die Daten verschlüsselt speichern wollen oder zur Kommunikation leicht einen geeigneten Schlüssel austauschen können, dann können Sie mit den gezeigten Methoden ein sicheres System erstellen.

Bei der Kommunikation zwischen Systemen ist es oft unpraktisch, Schlüssel für symmetrische Algorithmen physisch auszutauschen, damit sie nicht in die falschen Hände geraten. Für solche Fälle gibt es sowohl geeignete Protokolle als auch weitere Algorithmen, mit denen wir auch verteilte Systeme sichern können. Diese Themen bespreche ich in meinem nächsten Artikel. Bis dahin Wünsche ich Ihnen viel Spaß beim Verschlüsseln!

Links und Literatur

Kommentare