Blockchiffre Operationsmodi |
Samstag, den 29. März 2008 um 21:11 Uhr | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Inhaltsverzeichnis
Einführung in die Blockchiffre BetriebsartenIn der Kryptographie operieren Blockchiffre auf einem Datenblock fester Länge, oft 64 oder 128 Bits. Da eine zu verschlüsselnde Nachricht eine beliebige Länge haben kann, und weil die Verschlüsselung desselben Klartextes mit demselben Schlüssel immer denselben Geheimtext produziert, wurden verschiedene Betriebsarten entwickelt, um die Vertraulichkeit beliebig langer Nachrichten bei den Blockverschlüsselungen sicherzustellen. Die frühesten Betriebsarten, die in der Literatur (z.B. ECB, CBC, OFB und CFB) beschrieben wurden, gewährleisten nur Vertraulichkeit, nicht jedoch die Datenintegrität. Andere Betriebsarten wurden seitdem entwickelt um beides, Vertraulichkeit und Datenintegrität sicherzustellen, beispielsweise der IAPM, CCM, EAX, GCM und OCB-Modus. In dem Tutorial über den Advanced Encryption Standard (AES) haben Sie erfahren, wie der Algorithmus funktioniert und man damit Blockdaten verschlüsseln kann. Nun werden Sie erfahren, wie sich bekannte Blockchiffre Operationsmodi auf den AES anwenden lassen, so dass Sie auch in die Lage versetzt werden, Nachrichten beliebiger Länge zu verschlüsseln. Dazu sehen wir uns fünf bekannte Betriebsarten, namentlich den ECB, CBC, OFB und CFB genauer an und implementieren einige davon in C. Der Initialisierungsvektor (IV)Der Initialisierungsvektor bezeichnet einen Block von Zufallsdaten, der in bestimmten Modi einiger Blockchiffren verwendet wird, wie dem Cipher Block Chaining Mode, kurz CBC. Beim Verschlüsseln von Nachrichten muss vermieden werden, dass gleiche Klartextblöcke immer wieder gleiche Geheimtextblöcke ergeben. Ein förmlicher Brief fängt im Deutschen in der Regel mit „Sehr geehrter Herr/Frau“ gefolgt vom Namen an. Aus diesem Wissen könnte ein Angreifer versuchen Rückschlüsse auf den verwendeten Schlüssel zu ziehen. Um das zu vermeiden, wird der erste Klartextblock mit einem IV XOR-verknüpft. Da der IV zufällig erzeugt wurde, unterscheiden sich die entstehenden Geheimtexte auch dann, wenn die Klartexte mit identischen Daten beginnen. Da bei den Verschlüsselungsalgorithmen in der Regel Modi gewählt werden, bei denen der Geheimtext eines Blocks vom Geheimtext seines Vorgängerblocks abhängt, muss der IV nicht geheim gehalten werden. Sie werden jedoch schon sehr bald erkennen, das ein öffentlicher IV Informationen preisgibt, die bei Nutzung mit ständig gleichem Schlüssel sicherheitsrelevant sein können. Bedenken Sie, dass der IV ein Initialisierungsblock, so wie unser Chiffreblock ist (128 Bit beim AES). PaddingDas Padding (von engl. to pad für auffüllen) dient dazu einen vorhandener Datenbestand zu vergrößern. Die Füllbytes werden auch Pad-Bytes genannt. Da Blockchiffre mit einer festen Blockgröße arbeiten, die Nachrichten allerdings variabel sind, benötigen einige Modi Pad-Bytes um den letzten Block ggf. aufzufüllen. Wie Sie in dem Artikel erfahren werden, existieren diverse Paddingschemen. Die einfachste Methode (die auch wir implementieren werden) ist es, den Klartext mit Null-Bytes aufzufüllen um auf diese Weise ein Vielfaches der Länge, der Blockgröße zu erhalten. Nur ein wenig komplexer ist das Verfahren beim alten DES, bei dem zunächst ein einzelnes Bit ‘1’ hinzugefügt wird, um anschließend den Block mit Nullen ‘0’ aufzufüllen. Falls die Nachricht exakt an einer Blockgrenze endet, wird ein vollständig neuer Block hinzugefügt.
Tabelle 1. Darstellung eines aufgefüllten 128-Bit Blocks (Padding) Für die ECB, CBC und CFB Modi, muss der Klartext eine Sequenz von vollständigen Datenblöcken (im CFB Modus Datensegmente) sein. Mit anderen Worten, für alle drei Modi muss die Anzahl an Bits im Klartext dem positiven Vielfachen der Rijndael Block- bzw. Segmentgröße entsprechen. Falls die Größe der Datenzeichenkette dieser Anforderung nicht genügt, muss der Klartext in der Anzahl an Bits vergrößert werden, d.h gepaddet werden. Beim Padding können mit der oben genannten Null-Byte-Methode, bei Übertragungen Datenvolumen eingespart werden, indem die aufgefüllten Nullen verworfen werden. Das setzt allerdings voraus, dass dem Empfänger bekannt ist das die Nachricht verkürzt übertragen wurde und nachträglich wieder aufgefüllt werden muss. Das kann beispielsweise durch protokollspezifische Nachrichtengrenzen erreicht werden, ist für unseren Artikel allerdings nicht weiter relevant. Betriebsmodi:Nachfolgend werden die theoretischen Grundlagen zu den verschiedenen Betriebsarten detailliert erklärt. Wir beginnen mit dem einfachsten und zugleich unsichersten Modus, dem ECB. Electronic Codebook (ECB)Beim ECB werden die Klartextblöcke nacheinander und unabhängig voneinander einfach in den Geheimtextblock überführt, nachdem die Nachricht in gleich große, für den Algorithmus geeignete Blöcke aufgeteilt wurde. Der Name ECB rührt daher, dass Codebücher über die Zuordnung von Chiffretexten und Klartexten erstellt werden können. Der ECB birgt große Gefahren, da durch den Modus Klartextmuster nicht verwischt werden. Gleiche Klartextblöcke ergeben bei gleichen Schlüssel auch immer den gleichen Geheimtextblock, wodurch man bei hinreichend vielen Geheimtextblöcken und partiellen Annahmen über den Klartext Rückschlüsse auf den geheimen Schlüssel ziehen kann. Aus verschiedenen Gründen ist der Modus nicht sonderlich sicher, und sollte vermieden werden. Es wird dringend davon abgeraten diesen Modus dennoch zu verwenden. Der Vollständigkeit halber möchten wir den Modus dennoch auflisten: Der Electronic Codebook (ECB) Modus ist, wie folgt definiert: ECB Verschlüsselung: Cj = CIPHK(Pj) für j = 1 … n. ECB Entschlüsselung: Pj = CIPH-1K(Cj) für j = 1 … n. Wenn man eine Grafik damit verschlüsselt, die nur aus einigen schwarzen Linien besteht, und dabei 0 (Bit) für Weiß und 1 (Bit) für Schwarz steht, wird man sehr viele Blöcke finden, die nur aus 0 bestehen. Alle Geheimtextblöcke die dann anders sind, enthalten min. eine 1 (Bit). Dadurch könnte man die Zeichnung bis auf ein paar Millimeter Abweichung rekonstruieren, ohne den Schlüssel zu kennen. Das zweite Bild zeigt diese Schwäche deutlich auf: Darüberhinaus kann man beim ECB Geheimtextblöcke austauschen, wodurch sich zum Beispiel die Summe oder der Empfänger einer Überweisung ändern könnte. Das dritte Bild zeigt dasselbe Bild, bei dem allerdings der deutlich sicherere CBC-Modus verwendet wurde. Cipher-Block Chaining (CBC)Der Cipher Block Chaining (CBC) Modus ist ein auf Vertraulichkeit abzielender Modus, dessen Verschlüsselungsprozess den Klartextblock mit dem im letzten Schritt erzeugten Geheimtextblock per XOR (exklusives Oder) verknüpft. Daher der Name “chaining”, was für verketten steht. Der CBC-Modus benötigt einen Initialisierungsvektor, um diesen mit dem ersten Block zu verknüpfen. Der IV muss nicht geheim bleiben, aber er sollte zufällig gewählt sein. Er wird oft entweder durch einen Zeitstempel gebildet oder durch eine zufällige Zahlenfolge. Der Cipher Block Chaining (CBC) Modus ist, wie folgt definiert: CBC Verschlüsselung: C1 = CIPHK(P1 ⊕ IV); Cj = CIPHK(Pj ⊕ Cj-1) für j = 2 … n. CBC Entschlüsselung: P1 = CIPH-1K(C1) ⊕ IV; Pj = CIPH-1K(Cj) ⊕ Cj-1 für j = 2 … n. Der CBC-Mode hat einige wichtige Vorteile:
Da ein Geheimtextblock nur von dem vorherigen Blöck abhängt, verursacht ein beschädigter Geheimtextblock, wie beispielsweise ein Bitfehler bei der Datenübertragung, beim Entschlüsseln keinen allzugroßen Schaden, denn es werden nur der betroffene Klartextblock zu 50% und im darauffolgenden Klartextblock ein Bit falsch im Klartext dechiffriert. Dies ist unmittelbar aus der Definition der Entschlüsselung und obiger Abbildung ersichtlich, da ein beschädigter Geheimtextblock Cj nur die Klartextblöcke Pj und Pj+1 beeinflusst und sich nicht unbeschränkt weiter verbreitet. Trotzdem kann diese beschränkte Vervielfachung nur eines einzigen Bitfehlers im Chiffrat bei CBC eine Vorwärtsfehlerkorrektur des Klartextes erschweren bzw. unmöglichen machen. Genauso verursacht ein beschädigter Initialisierungsvektor beim Entschlüsseln keinen allzugroßen Schaden, da dadurch nur der Klartextblock P1 beschädigt wird. Der CBC-Modus ist wesentlich sicherer als der ECB-Modus, vor allem wenn man keine zufälligen Texte hat. Unsere Sprache und andere Dateien, wie z. B. Video-Dateien, sind keinesfalls zufällig, weswegen der ECB-Mode im Gegensatz zum CBC-Mode Gefahren birgt. Generell sollte ein Blockchiffre immer im CBC-Modus betrieben werden - Ausnahmen sollten gut begründet sein. Ausnahmen bestätigen bekanntlich die Regel. So hat der CBC-Modus einen entscheidenden Nachteil. Die Nachricht muss auf Vielfache der Chiffreblockgröße aufgefüllt werden. Daher wird der CBC-Modus zunehmend von synchronen Stromchiffren abgelöst, wie dem CFB oder OFB.
Cipher feedback (CFB)In diesem Modus wird, wie in der Abbildung unten dargestellt, die Ausgabe der Blockchiffre mit dem Klartext bitweise XOR (exklusives ODER) verknüpft um daraus den Geheimtext zu bilden. Diese Betriebsart bzw. dieser Modus ergibt damit eine sogenannte Stromchiffre. Der Cipher Feedback (CFB) Modus zielt auf Vertraulickeit ab, da auch in diesem Modus Klartextmuster verwischt werden. Auch bei diesem Modus wird ein IV benötigt. Darüberhinaus benötigt der CFB einen ganzzahligen Parameter, s bezeichnet, der mit 1 ≤ s ≤ b festgelegt ist. In der Spezifikation besteht jedes Klartextsegment (P#j) und Geheimtextsegment (C#j) aus s Bits. Die Zahl der Bits wird oft in den Namen des Modus eingearbeitet, z.B. der 1-Bit CFB Modus, der 8-Bit CFB Modus, der 64-Bit CFB Modus oder der 128-Bit CFB Modus. Die Entschlüsselung beim Empfänger, wie in obiger Abbildung dargestellt, funktioniert wie die Verschlüsselung, erzeugt also bei gleichem Initialisierungsvektor und gleichem Schlüssel die gleiche binäre Datenfolge mit der die XOR-Operation des Sender rückgängig gemacht werden kann. Die Grafik zeigt auch den wesentlichen Nachteil dieser Stromchiffre: Durch nur einen einzigen Bitfehler der bei der Übertragung auftreten kann, wird im aktuellen Klartextdatenblock genau ein Bitfehler erzeugt und zusätzlich im nachfolgenden Datenblock im Mittel 50% der Datenbits zerstört. Diese Fehlerfortpflanzung ist ähnlich wie bei der Betriebsart Cipher Block Chaining (CBC) und erschwert die Entschlüsselung des Klartextes. Der Cipher feedback (CFB) Modus ist folgendermaßen definiert: CFB Verschlüsselung: I1 = IV; Ij = LSBb-s(Ij–1) | C#j–1 für j = 2 … n. Oj = CIPHK(Ij) für j = 1, 2 … n; C#j = P#j ⊕ MSBs(Oj) für j = 1, 2 … n. CFB Entschlüsselung: I1 = IV; Ij = LSBb-s(Ij–1) | C#j–1 für j = 2 … n. Oj = CIPHK(Ij) für j = 1, 2 … n; P#j = C#j ⊕ MSBs(Oj) für j = 1, 2 … n. Bei der CFB Verschlüsselung ist der erste Eingabeblock, der IV und die vorwärts gerichtete Chiffreoperation wird auf den IV angewendet um den ersten Ausgabeblock zu produzieren. Das erste Geheimtextsegment wird durch eine exlusiv-ODER Verknüpfung des ersten Klartextsegmentes mit den s meist signifikanten Bits (MSB) des ersten Ausgabeblocks generiert. (Die übriggebliebenen b-s Bits des ersten Ausgabeblocks werden verworfen.) Die letzten signifikanten Bits (LSB) des IV werden anschließend mit den s Bits des ersten Geheimtextsegmentes verkettet um den nächsten Eingabeblock zu formieren. Eine alternative Erklärung, wie der nächste Eingabeblock erzeugt wird, ist, sich die Operation als zyklischen Links-Shift um s Stellen vorzustellen und anschließend ersetzt das Geheimtextsegment die s letzten signifikanten Bits des Ergebnisses. Der Vorgang wird mit allen aufeinanderfolgenden Eingabeblöcken wiederholt bis für jedes Klartextsegment ein Geheimtextsegment erzeugt wurde. Allgemein gesprochen wird jeder Eingabeblock verschlüsselt um einen Ausgabeblock zu produzieren. Die s meist signifikanten Bits jedes Ausgabeblocks werden mit dem korrespondierenden Klartextsegment exklusiv-ODER verknüpft, um das Geheimtextsegment zu generieren. Jedes Geheimtextsegment (außer dem letzen) wird “rückgekoppelt” in den vorangegangenen Eingabeblock, so wie oben beschrieben, um einen neuen Eingabeblock zu formieren. Dieser Feedback kann in Form von den individuellen Bits in den Zeichenketten folgendermaßen beschrieben werden: wenn i1i2…ib der j-te Eingabeblock ist und c1c2…cs das j-te Geheimtextsegment, dann ist der (j+1)-te Eingabeblock is+1is+2…ib c1c2…cs. Bei der CFB Verschlüsselung kann, wie beim CBC-Modus, die Chiffreoperation nicht parallel durchgeführt werden, da der Eingabeblock von dem vorangegangenen Ergebnis abhängt. Bei der CFB Entschlüsselung kann die Operation allerdings parallelisiert werden, falls die Eingabeblöcke zuerst aus dem IV und dem Geheimtext konstruiert werden. Trotz des Vorteils der Selbstsynchronisation wird der CFB in der Praxis nur selten eingesetzt: Spielt die Fehlerfortpflanzung auf den nächsten Block in einer bestimmten Anwendung keine Rolle bzw. wird durch geeignete zusätzliche Verfahren kompensiert, kommt meist der CBC zur Anwendung. Wird eine Stromchiffre ohne Fehlerfortpflanzung in einer Anwendung benötigt, kommt meist der Modus OFB zu Anwendung. Output feedback (OFB)Wenn Sie den CFB Modus verstanden haben, sollten Sie auch keine Probleme mit dem OFB Modus haben. Der Output Feedback (OFB) Modus verhält sich wie der CFB, mit dem Unterschied das er vor der XOR Operation den AES Ausgabeblock als Eingabe für die nächste Iteration verwendet. Der OFB ist folgendermaßen definiert: CFB Verschlüsselung: I1 = IV; Ij = Oj-1 für j = 2 … n. Oj = CIPHK(Ij) für j = 1, 2 … n; Cj = Pj ⊕ Oj für j = 1, 2 … n-1. C*j = P*j ⊕ MSBu(Ou) CFB Entschlüsselung: I1 = IV; Ij = Oj-1 für j = 2 … n. Oj = CIPHK(Ij) für j = 1, 2 … n; Pj = Cj ⊕ Oj für j = 1, 2 … n-1. P*j = C*j ⊕ MSBu(Ou) Sowohl bei der OFB-Verschlüsselung, als auch der OFB-Entschlüsselung, hängt jede vorwärts gerichtete Chiffreoperation, mit Ausnahme der ersten Operation, von der vorigen Chiffreoperation ab. Daher können multiple Vorwärtsverschlüsselungen nicht parallel durchgeführt werden. Allerdings kann bei bekannten IV der Ausgabeblock generiert werden, bevor der Klartext oder der Geheimtext verfügbar sind. Der OFB Modus benötigt einen eindeutigen IV für jede Nachricht unter einem gegebenen Schlüssel. Falls entgegen dieser Notwendigkeit, derselbe IV für die Verschlüsselung genutzt wird, dann ist die Vertraulichkeit der Nachricht nicht mehr gewährleistet. Implementierung:Bevor wir damit beginnen die verschiedenen Betriebsarten zu implementieren, werden wir zunächst eine Struktur definieren, die es uns ermöglichen wird den entsprechenden Modus auszuwählen, sowie den Prototypen der Verschlüsselungsfunktion: // Block cipher modes of operation typedef enum { // Output feedback OFB, // Cipher feedback CFB, // Cipher-block chaining CBC } CIPHERMODE; int32_t encryptFile(FILE* in, FILE* out, KEYSIZE keySize, CIPHERMODE mode, const uint8_t* password, uint32_t passwordLength); Zu einem späteren Zeitpunkt, wenn Sie verschiedene Blockchiffre implementiert haben, können Sie diesen Prototypen abändern, damit dieser dann auf einen Blockchiffre zeigt, der dann intern vom Operationsmodi verwendet wird. Ich habe mich dazu entschlossen verschiedene Blöcke, der Größe 128 Bit zu nutzen, die alle einem eigenen Zweck dienen:
Wie Sie später sehen werden, kann man auch weniger Blöcke verwenden, aber da jede Operation eine eigene spezielle Struktur hat, kann man den Code durch die Einführung von ein wenig Overhead sauber halten. Ich verwende darüberhinaus eine zusätzliche Variable, die anzeigt ob wir uns gegenwärtig in der ersten Runde (hier brauchen wir den IV) oder in einer anderen Runde befinden. fseek(in, 0, SEEK_END); fileSize = ftell(in); fseek(in, 0, SEEK_SET); // Add the file header fwrite(&mode, sizeof(CIPHERMODE), 1, out); fwrite(&fileSize, sizeof(fileSize), 1, out); Da wir mit Binärdateien arbeiten, benutze ich fread() und fwrite() um die zwei Dateien zu verarbeiten. Unser Algorithmus liest 16 Bytes ein (der Returnwert von fread() wird gespeichert, falls wir weniger als 16 Bytes einlesen und wir padden müssen), dann führen wir bei Bedarf das Padding durch, XORieren den Klartext mit dem IV oder der Ausgabe (in CBC ist die Ausgabe gleich dem Chiffretext), wenden die AES Verschlüsselung an und schreiben das Resultat in die Datei. Implementierung: Cipher-Block Chaining (CBC)while ((read = fread(plaintext, sizeof(uint8_t), 16, in)) > 0) { // Padd with 0 bytes if (read < 16) { for (i = read; i < 16; i++) { plaintext[i] = 0; } } for (i = 0; i < 16; i++) { input[i] = plaintext[i] ^ ((firstRound) ? IV[i] : ciphertext[i]); } firstRound = 0; cipher(input, ciphertext, &context); // Always 16 bytes because of the padding for CBC fwrite(ciphertext, sizeof(uint8_t), 16, out); } Wie Sie sehen können, benötigen wir momentan nicht den Ausgabeblock, da der Chiffretext identisch mit der Ausgabe ist. Die CBC Entschlüsselung ist ähnlich, ausgenommen die Tatsache das wir auf die originale Dateigröße acht geben müssen, die wir aus der Eingabedatei wiederhergestellt haben und für den Fall das wir weniger als 16 Byte für die Entschlüsselung übrig haben, schreiben wir die übrigen Bytes anstatt die 16 Bytes, die wir von der Datei gelesen haben: while ((read = fread(ciphertext, sizeof(uint8_t), 16, in)) > 0) { decipher(ciphertext, output, &context); for (i = 0; i < 16; i++) { plaintext[i] = ((firstRound) ? IV[i] : input[i]) ^ output[i]; } firstRound = 0; if (originalFileSize < 16) { fwrite(plaintext, sizeof(uint8_t), originalFileSize, out); } else { fwrite(plaintext, sizeof(uint8_t), read, out); originalFileSize -= 16; } memcpy(input, ciphertext, 16 * sizeof(uint8_t)); } Sie haben wahrscheinlich bemerkt das ich den Inhalt des Chiffretextes in den Eingabeblock kopiert habe. Das liegt daran das der Chiffretext während der nächsten Iteration durch den neuen Chiffretext Block überschrieben wird, aber wir brauchen nach wie vor den Chiffretext der vorherigen Iteration um die XOR-Verknüpfung durchzuführen. In diesem Fall, verwende ich nicht die Eingabe für den AES Blockchiffre, sondern für die XOR-Verknüpfung. Machen Sie sich keine Gedanken, falls Sie diesen Code nicht in ein funktionierendes Beispiel implementieren können, ich werden den vollständigen Code am Ende dieses Artikels nachliefern. Implementierung: Cipher Feedback (CFB)CFB besitzt zusammen mit dem Streamchiffre OFB und CTR zwei Vorteile gegenüber dem CBC Modus: der Blockchiffre wird nur in der Verschlüsselungsrichtung verwendet und die Nachricht muss nicht auf ein Vielfaches der Blockgröße gepaddet werden. Ich persönlich bin der Ansicht das der Modus relativ leicht zu implementieren ist. Deshalb zeige ich Ihnen ohne Umschweife den fertigen Code für die Verschlüsselung: while ((read = fread(plaintext, sizeof(uint8_t), 16, in)) > 0) { if (firstRound) { cipher(IV, output, &context); firstRound = 0; } else { cipher(input, output, &context); } for (i = 0; i < 16; i++) { plaintext[i] = output[i] ^ ciphertext[i]; } fwrite(plaintext, sizeof(uint8_t), read, out); memcpy(input, ciphertext, 16 * sizeof(uint8_t)); } In diesem Fall, haben Sie vielleicht bemerkt das ich den Chiffretext sofort als Eingabe für die AES Verschlüsselung verwenden könnte. Allerdings habe ich das aufgrund der Namenskonvention als unlogisch angesehen, weshalb ich den Inhalt einfach vom Chiffretext in die Eingabe kopiert habe. Falls Sie die Geschwindigkeit verbessern möchten, können Sie das selbstverständlich ändern. while ((read = fread(ciphertext, sizeof(uint8_t), 16, in)) > 0) { if (firstRound) { cipher(IV, output, &context); firstRound = 0; } else { cipher(input, output, &context); } for (i = 0; i < 16; i++) { plaintext[i] = output[i] ^ ciphertext[i]; } fwrite(plaintext, sizeof(uint8_t), read, out); memcpy(input, ciphertext, 16 * sizeof(uint8_t)); } Implementierung: Output feedback (OFB)Hier ist der Code für die Verschlüsselung: while ((read = fread(plaintext, sizeof(uint8_t), 16, in)) > 0) { if (firstRound) { cipher(IV, output, &context); firstRound = 0; } else { cipher(input, output, &context); } for (i = 0; i < 16; i++) { ciphertext[i] = plaintext[i] ^ output[i]; } fwrite(ciphertext, sizeof(uint8_t), read, out); memcpy(input, output, 16 * sizeof(uint8_t)); } Der einzige Unterschied zum CFB-Modus ist das ich den Inhalt der Ausgabe in die Eingabe kopiert habe und nicht den Inhalt des Chiffretextes. Der folgende Code zeigt die Entschlüsselung: while ((read = fread(ciphertext, sizeof(uint8_t), 16, in)) > 0) { if (firstRound) { cipher(IV, output, &context); firstRound = 0; } else { cipher(input, output, &context); } for (i = 0; i < 16; i++) { plaintext[i] = output[i] ^ ciphertext[i]; } fwrite(plaintext, sizeof(uint8_t), read, out); memcpy(input, output, 16 * sizeof(uint8_t)); } Alles zusammenfügenNun da wir alle drei Modi erklärt und implementiert haben, ist alles was noch zu tun ist, die Teile des Puzzles zu einem Gesamtbild zusammenzufügen. PasswortIn der Informatik wird die Stärke von Passwörtern anhand der Entropie bewertet. Die Entropie ist ein Maß für den mittleren Informationsgehalt oder auch Informationsdichte eines Zeichensystems. Ein gutes Passwort muss demnach maximal unerwartet sein, um die hohe Sicherheit zu erreichen. Zufall und Entropie werden oft verwechselt und das ist bei der Wahl des Passwortes entscheidend. Eine zufällige Zahlenfolge ist per Definition nur eine Folge ohne ein auftretendes Muster. Diese Eigenschaft trifft unter anderem auch auf die Kreiszahl π zu. Dennoch ist die Kreiszahl kein geeignetes Passwort. Der Grund dafür ist ihre geringe Entropie. Die Kreiszahl ist deterministisch berechenbar und lässt sich jederzeit in Suchtabellen abgleichen. Die Entropie ist demzufolge ein Gradmaß für das überraschende Auftreten von Zeichen. Für einen Chinesen weisen deutsche Wörter eine hohe Entropie auf, weil ihm sowohl das Vokabular, als auch die Syntax der deutschen Sprache unbekannt sind. Man könnte auch vereinfacht davon sprechen, dass die Passwortstärke umso besser ist, je länger und für den Angreifer sinnloser das Passwort ist. Das Passwort "Papiereimer" ist nicht sicher, da es im deutschen Raum leicht durch Wörterbuchattacken geknackt werden kann. Dagegen erscheint uns "Karatasi ndoo" sicherer, obwohl es nur die Übersetzung von Papiereimer auf Swahili ist. Da der AES nur Schlüsselgrößen von 128, 192 und 256 Bit verwerten kann, muss das Passwort in der Initialisierungsfunktion auf die richtige Größe gebracht werden. Passwörter über 256 Bit tragen demnach im AES nicht zur Sicherheit bei. Viel wichtiger ist das der 256 Bit lange Schlüssel so gewählt wird, dass er eine hohe Entropie aufweist. Die Passwortfunktion nimmt eine wichtige Rolle im gesamten Sicherheitskonzept ein. Die Anwendung muss darüber entscheiden was sie mit zu kurzen oder zu langen Passwörtern macht, die der Anwender eingibt. Hier gibt es viele unterschiedliche Ansätze, vom einfachen Auffüllen mit Nullen und Abschneiden zu langer Chiffreschlüssel bis hin zur Bildung von Hashwerten etc. Für den Initialisierungsvektor verwenden Programme, wie TrueCrypt, sogar Mauszeigerbewegungen zu Steigerung der Entropie. In unserer Anwendung werden Passwörter mit der richtigen Länge vollständig übernommen, zu lange Passwörter werden abgeschnitten und zu kurze Passwörter mit einem festen Algorithmus aufgefüllt. Diesen Teil übernimmt die Initialisierungsfunktion. In der Funktion /// <summary> /// Creates a new cipher instance, initializes AES context. The AES /// context stores Nk, Nr, Nb, cipher mode, key size and reserves space /// for the key and expanded cipher key. Call this function always first, /// before using the AES algorithm. /// </summary> /// <param name="context">A reference to the context to fill</praram> /// <param name="keySize">The AES key size (128, 192, 256)</praram> /// <param name="mode">The cipher mode, e.g. OFB</praram> /// <param name="key">A byte array with the key (password)</praram> /// <param name="keyLength">The key length</praram> /// <returns>A positive number or 0 if no error occurs</returns> int32_t create(AESCONTEXT* context, KEYSIZE keySize, CIPHERMODE mode, const uint8_t* key, uint32_t keyLength) { uint32_t i; // The expanded keySize uint32_t expandedKeySize; // Set the number of rounds, etc. switch (keySize) { case Bits128: context->Nk = 4; // key size = 4 words = 16 bytes = 128 bits context->Nr = 10; break; case Bits192: context->Nk = 6; // 6 words = 24 bytes = 192 bits context->Nr = 12; break; case Bits256: context->Nk = 8; // 8 words = 32 bytes = 256 bits context->Nr = 14; break; default: return UNKNOWN_KEYSIZE; break; } // Reserve space for the cipher key 16, 24, 32 bytes if ((context->cipherKey = (uint8_t *)malloc(context->Nk * 4 * sizeof(uint8_t))) == NULL) { return MEMORY_ALLOCATION_PROBLEM; } // Check if the password has the right size if (keyLength == context->Nk * 4) { for(i = 0; i < context->Nk * 4; i++) { context->cipherKey[i] = key[i]; // Just copy the array } } else { // Password is different size, do a manual copy for (i = 0; i < context->Nk * 4; i++) { // Make sure we can use the keyBytes if (i < keyLength) { context->cipherKey[i] = key[i]; } else { // We need to add some extra bytes with a *fixed* algorithm context->cipherKey[i] = i ^ (context->Nk << 14) % 256; } } } // Set the key size and the cipher mode context->keySize = keySize; context->mode = mode; // Create the expanded key expandedKeySize = (16 * (context->Nr + 1)); if ((context->expandedKey = (uint8_t *)malloc(expandedKeySize * sizeof(uint8_t))) == NULL) { return MEMORY_ALLOCATION_PROBLEM; } // Expand the key into an 176, 208, 240 bytes key expandKey(context->expandedKey, expandedKeySize, context->cipherKey, context->keySize); return 0; } Wie genau mit einem (unpassenden) Passwort verfahren wird, ist wie bereits erwähnt abhängig von der Implementierung. Wichtig ist nur, dass sowohl die Ver- als auch die Entschlüsselungsfunktion denselben Algorithmus verwenden. Werden bei der Verschlüsselung zu kurze Passwörter mit Nullen aufgefüllt, so muss natürlich auch bei der Entschlüsselung genauso vorgegangen werden, sonst würde der Cipher Key nicht mehr derselbe sein. Ein bekanntes Standardverfahren zur Konvertierung von Passwörtern bzw. beliebiger Bit-Strings in einen kryptographischen Schlüssel, ist der PKCS#5 (engl. Password-based Encryption Standard) aus dem RSA. PKCS#5 ist mit der Version 2 seit mehr als 10 Jahren unverändert geblieben und wurde im RFC 2898 dokumentiert. PKCS#5 definiert eine generische Funktion, die PBKDF (engl. Password-Based Key Derivation Function), um einen kryptographischen Schlüssel aus einem Passwort zu generieren. Key = PBKDF(salt, password, iteration count, size); PBKDF nimmt einen Bit-String und einen Salt entgegen und wendet eine pseudozufällige Funktion darauf an, z.B. eine Hash-Funktion, um den Schlüssel passender Länge zu generieren. Die Anzahl an angewendeten Hash-Funktionen wird mit der Iterationszahl angegeben. Mit dem Salt (dt. Salz) wird eine zufällig gewählte Zeichenfolge übergeben. Der Salt erschwert einen Angriff mit Regenbogentabellen, in denen vorberechnete Passwort-Schlüssel-Paare hinterlegt werden. Die Zahl der Iterationen erhöht die Rechenzeit beim Erraten des Passwortes. PKCS#5 empfielt mindestens 1.000 Iterationen. Zusätzlich eingefügte Operationen, die die Rechenzeit zu Gunsten der Sicherheit erhöhen, werden Spin genannt. Dateien verschlüsselnRelativ einfach gestaltet sich die Verschlüsselung von Dateien. Die Verschlüsselung: /// <summary> /// Encrypts a file, filling the output file with the encrypted /// bytes. Uses the password to encrypt. This function adds a byte header, /// storing the original file size and the cipher mode. /// </summary> /// <param name="in">File pointer of the plaintext file</praram> /// <param name="out">File pointer of the encrypted output file</praram> /// <param name="keySize">The AES key size (128, 192, 256)</praram> /// <param name="mode">The cipher mode, e.g. OFB</praram> /// <param name="password">A byte array with the password</praram> /// <param name="passwordLength">The password length</praram> /// <returns>A positive number if no error occurs</returns> int32_t encryptFile(FILE* in, FILE* out, KEYSIZE keySize, CIPHERMODE mode, const uint8_t* password, uint32_t passwordLength) { // The AES context AESCONTEXT context; // The AES input/output uint8_t plaintext[16] = { 0 }; uint8_t input[16] = { 0 }; uint8_t output[16] = { 0 }; uint8_t ciphertext[16] = { 0 }; uint8_t IV[16] = { 0 }; // Char firstRound uint8_t firstRound = 1; uint32_t i, read, fileSize; // Check paramaters if(in == NULL || in == NULL || passwordLength < 1) { return INVALID_ARGUMENT; } // Create the AES context create(&context, keySize, mode, password, passwordLength); fseek(in, 0, SEEK_END); fileSize = ftell(in); fseek(in, 0, SEEK_SET); // Add the file header fwrite(&mode, sizeof(CIPHERMODE), 1, out); fwrite(&fileSize, sizeof(fileSize), 1, out); while ((read = fread(plaintext, sizeof(uint8_t), 16, in)) > 0) { if (mode == CFB) { if (firstRound) { cipher(IV, output, &context); firstRound = 0; } else { cipher(input, output, &context); } for (i = 0; i < 16; i++) { ciphertext[i] = plaintext[i] ^ output[i]; } fwrite(ciphertext, sizeof(uint8_t), read, out); memcpy(input, ciphertext, 16 * sizeof(uint8_t)); } else if (mode == OFB) { if (firstRound) { cipher(IV, output, &context); firstRound = 0; } else { cipher(input, output, &context); } for (i = 0; i < 16; i++) { ciphertext[i] = plaintext[i] ^ output[i]; } fwrite(ciphertext, sizeof(uint8_t), read, out); memcpy(input, output, 16 * sizeof(uint8_t)); } else if (mode == CBC) { // Padd with 0 bytes if (read < 16) { for (i = read; i < 16; i++) { plaintext[i] = 0; } } for (i = 0; i < 16; i++) { input[i] = plaintext[i] ^ ((firstRound) ? IV[i] : ciphertext[i]); } firstRound = 0; cipher(input, ciphertext, &context); // Always 16 bytes because of the padding for CBC fwrite(ciphertext, sizeof(uint8_t), 16, out); } } // Shut down the AES instance shutdown(&context); return 1; } Sie können erkennen das hier der Die Entschlüsselung: /// <summary> /// Decrypts an encrypted file, filling the output file with the /// decrypted bytes. Uses the password to decrypt. /// </summary> /// <param name="in">File pointer of the enrypted file</praram> /// <param name="out">File pointer of the decrypted output file</praram> /// <param name="keySize">The AES key size (128, 192, 256)</praram> /// <param name="password">A byte array with the password</praram> /// <param name="passwordLength">The password length</praram> /// <returns>A positive number if no error occurs</returns> int32_t decryptFile(FILE* in, FILE* out, KEYSIZE keySize, const uint8_t* password, uint32_t passwordLength) { // The AES context AESCONTEXT context; // The AES input/output uint8_t ciphertext[16] = { 0 }; uint8_t input[16] = { 0 }; uint8_t output[16] = { 0 }; uint8_t plaintext[16] = { 0 }; uint8_t IV[16] = { 0 }; // Char firstRound char firstRound = 1; // The cipher mode will be extracted from the header CIPHERMODE mode; uint32_t i, read, originalFileSize = 0; // Check paramaters if(in == NULL || in == NULL || passwordLength < 1) { return INVALID_ARGUMENT; } fread(&mode, sizeof(CIPHERMODE), 1, in); fread(&originalFileSize, sizeof(originalFileSize), 1, in); // Create the AES context create(&context, keySize, mode, password, passwordLength); while ((read = fread(ciphertext, sizeof(uint8_t), 16, in)) > 0) { if (mode == CFB) { if (firstRound) { cipher(IV, output, &context); firstRound = 0; } else { cipher(input, output, &context); } for (i = 0; i < 16; i++) { plaintext[i] = output[i] ^ ciphertext[i]; } fwrite(plaintext, sizeof(uint8_t), read, out); memcpy(input, ciphertext, 16 * sizeof(uint8_t)); } else if (mode == OFB) { if (firstRound) { cipher(IV, output, &context); firstRound = 0; } else { cipher(input, output, &context); } for (i = 0; i < 16; i++) { plaintext[i] = output[i] ^ ciphertext[i]; } fwrite(plaintext, sizeof(uint8_t), read, out); memcpy(input, output, 16 * sizeof(uint8_t)); } else if(mode == CBC) { decipher(ciphertext, output, &context); for (i = 0; i < 16; i++) { plaintext[i] = ((firstRound) ? IV[i] : input[i]) ^ output[i]; } firstRound = 0; if (originalFileSize < 16) { fwrite(plaintext, sizeof(uint8_t), originalFileSize, out); } else { fwrite(plaintext, sizeof(uint8_t), read, out); originalFileSize -= 16; } memcpy(input, ciphertext, 16 * sizeof(uint8_t)); } } // Shut down the AES instance shutdown(&context); return 1; } Bytes verschlüsselnSelbstverständlich lassen sich auch noch weitere Funktionen definieren. Der Rijndael-Algorithmus verschlüsselt immer nur Bytes. Deshalb ist es sinnvoll eine Methode zur Verfügung zu stellen, die genau nur das macht, also einen Bytestrom verschlüsseln. Die folgende, leicht abgeänderte, Funktion macht das. /// <summary> /// Encrypts a given byte array with the given password. The /// resulting encrypted buffer array will be automatically allocated by /// this function with the necessary size. It is up to the caller /// to release the allocated memory later. This function adds a 8-byte /// header, storing the original size and the cipher mode. /// </summary> /// <param name="inBuffer">The byte array to encrypt</praram> /// <param name="inBufferLength">The length of the byte array</praram> /// <param name="outBuffer">A pointer to a byte array, not reserved yet</praram> /// <param name="IV">A 16-byte long initialization vector</praram> /// <param name="keySize">The AES key size (128, 192, 256)</praram> /// <param name="mode">The block cipher mode of operation, e.g. CBC</praram> /// <param name="password">A byte array with the password</praram> /// <param name="passwordLength">The password length in bytes</praram> /// <returns>The positive number of encrypted bytes, if error occurs negative</returns> /// <remarks>Always call this funtion with valid arguments.</remarks> int32_t encryptBytes(const uint8_t* inBuffer, const uint32_t inBufferLength, uint8_t** outBuffer, const uint8_t* IV, KEYSIZE keySize, CIPHERMODE mode, const uint8_t* password, uint32_t passwordLength) { // The AES context AESCONTEXT context; // The AES input/output uint8_t plaintext[16] = { 0 }; uint8_t input[16] = { 0 }; uint8_t output[16] = { 0 }; uint8_t ciphertext[16] = { 0 }; // Char firstRound char firstRound = 1; uint32_t i, position, outBufferLength, read; const uint32_t headerSize = sizeof(CIPHERMODE) + sizeof(inBufferLength); // Check paramaters if(inBufferLength < 16 || inBuffer == NULL || password == NULL || passwordLength < 1 || IV == NULL) { return INVALID_ARGUMENT; } // Compute output buffer length, since CBC buffer is padded if(mode == CBC && inBufferLength % 16 != 0) { outBufferLength = inBufferLength + 16 - inBufferLength % 16; } else { outBufferLength = inBufferLength; } // Add the header size to buffer size outBufferLength += headerSize; // Now reserve space for the buffer if (((*outBuffer) = (uint8_t *)malloc(outBufferLength * sizeof(uint8_t))) == NULL) { return MEMORY_ALLOCATION_PROBLEM; } // Create the AES context create(&context, keySize, mode, password, passwordLength); // Add a header with the cipher mode and the original buffer length memcpy((*outBuffer), &mode, sizeof(CIPHERMODE)); memcpy((*outBuffer) + sizeof(CIPHERMODE), &inBufferLength, sizeof(inBufferLength)); // Start reading from input buffer and writing encrypted bytes to output buffer for(position = 0; position < inBufferLength; position += read) { // We were always reading 16 bytes. If there are less than 16 bytes left, read the rest read = (inBufferLength - position) >= 16 ? 16 : (inBufferLength % 16); // Copy plaintext bytes into plaintext memcpy(plaintext, inBuffer + position, read * sizeof(uint8_t)); if (mode == CFB) { if (firstRound) { cipher(IV, output, &context); firstRound = 0; } else { cipher(input, output, &context); } for (i = 0; i < 16; i++) { ciphertext[i] = plaintext[i] ^ output[i]; } memcpy((*outBuffer) + headerSize + position, ciphertext, read * sizeof(uint8_t)); memcpy(input, ciphertext, 16 * sizeof(uint8_t)); } else if (mode == OFB) { if (firstRound) { cipher(IV, output, &context); firstRound = 0; } else { cipher(input, output, &context); } for (i = 0; i < 16; i++) { ciphertext[i] = plaintext[i] ^ output[i]; } memcpy((*outBuffer) + headerSize + position, ciphertext, read * sizeof(uint8_t)); memcpy(input, ciphertext, 16 * sizeof(uint8_t)); } else if (mode == CBC) { // Padd with 0 bytes if (read < 16) { for (i = read; i < 16; i++) { plaintext[i] = 0; } } for (i = 0; i < 16; i++) { input[i] = plaintext[i] ^ ((firstRound) ? IV[i] : ciphertext[i]); } firstRound = 0; cipher(input, ciphertext, &context); // Always copy 16 bytes, because of the padding for CBC memcpy((*outBuffer) + headerSize + position, ciphertext, 16 * sizeof(uint8_t)); } } // Shut down the AES instance shutdown(&context); return outBufferLength; } Die zugehörige Entschlüsselungsfunktion können Sie gerne selbst entwerfen oder Sie laden sich in unserer Download-Rubrik den kompletten Quellcode mit dem implementierten AES samt Betriebsmodi herunter. Das zur Verfügung gestellte Programm erlaubt es über die Konsole Dateien zu verschlüsseln und demonstriert die Nutzung, der oben vorgestellten Funktionen in eigenen Programmen. TestvektorenUm die eigene Implementierung des AES mit den Operationsmodi auf Korrektheit überprüfen zu können, werden sogenannte Testvektoren (engl. Test Vectors) verwendet. Diese Testvektoren bestehen aus einem Satz fester Klar- und Geheimtexte, denen ein geheimer Schlüssel zugeordnet ist. Auf diese Weise lässt sich ein Klartext mit einem Schlüssel verschlüsseln und anschließend überprüfen, ob das Programm den korrespondierenden Geheimtext produziert. Für den AES wurden die nachfolgenden Testvektoren im Dokument "NIST Special Publication 800-38A" offiziell publiziert. Liste von Testvektoren für den AES/ECB OperationsmodiAES ECB 128-bit VerschlüsselungSchlüssel: 2B7E151628AED2A6ABF7158809CF4F3C Initialisierungsvektor: nicht erforderlich
AES ECB 192-bit VerschlüsselungSchlüssel: 8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B Initialisierungsvektor: nicht erforderlich
AES ECB 256-bit VerschlüsselungSchlüssel: 603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4 Initialisierungsvektor: nicht erforderlich
Liste von Testvektoren für den AES/CBC OperationsmodiAES CBC 128-bit VerschlüsselungSchlüssel: 2B7E151628AED2A6ABF7158809CF4F3C
AES CBC 192-bit VerschlüsselungSchlüssel: 8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B
AES CBC 256-bit VerschlüsselungSchlüssel: 603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4
Liste von Testvektoren für den AES/CFB OperationsmodiAES CFB 128-bit VerschlüsselungSchlüssel: 2B7E151628AED2A6ABF7158809CF4F3C
AES CFB 192-bit VerschlüsselungSchlüssel: 8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B
AES CFB 256-bit VerschlüsselungSchlüssel: 603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4
Liste von Testvektoren für den AES/OFB OperationsmodiAES OFB 128-bit VerschlüsselungSchlüssel: 2B7E151628AED2A6ABF7158809CF4F3C
AES OFB 192-bit VerschlüsselungSchlüssel: 8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B
AES OFB 256-bit VerschlüsselungSchlüssel: 603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4
Weitergehende Links
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Zuletzt aktualisiert am Donnerstag, den 19. Mai 2011 um 09:02 Uhr |
AUSWAHLMENÜ | ||||||||
|