© rassco/Shutterstock.com
Block Cipher erklärt – Teil 1

Symmetrische Verschlüsselung in Ruby


Neben Stream Ciphern [1], [2] bietet die Standard-Library von Ruby auch Block Cipher, um symmetrische Verschlüsselung betreiben zu können. Allen voran wird natürlich auch der am weitesten verbreitete Block Cipher, AES [3], unterstützt. Zwar ist der Erfolg von AES nach knapp 20 Jahren ungebrochen und es gibt keinerlei Anzeichen, dass sich das in näherer Zukunft ändern könnte, doch bedeutet das Verschlüsseln mit AES nicht automatisch Sicherheit. Es kommt ganz darauf an, wie man mit AES verschlüsselt. Im Gegensatz zu Stream Ciphern bieten Block Cipher verschiedene Betriebsmodi an, die sich hinsichtlich ihrer Sicherheit teilweise gehörig unterscheiden.

So wie auch schon bei den Stream Ciphern möchte ich Ihnen anhand konkreter Beispiele zeigen, was genau ein Block Cipher ist, was sichere Verschlüsselung mit einem Block Cipher auszeichnet und auf was es zu achten gilt. Dazu werden wir uns zunächst unseren eigenen Block Cipher basteln, der uns als Anschauungsobjekt dienen wird, um mittels diesem die einzelnen Evolutionsstufen zu durchlaufen, bevor wir uns dann im nächsten Teil vor dem Hintergrund der gewonnenen Erkenntnisse konkret dem Angebot in der Standard-Library von Ruby widmen werden.

Was ist ein Block Cipher?

Am besten versteht man die Funktionsweise von Block Ciphern, wenn man sie den Stream Ciphern gegenüberstellt. Bei einem Stream Cipher hat man die Wahl, in welchen Portionen man den Plaintext verarbeiten möchte. Man kann alles auf einen Rutsch verarbeiten oder den Plaintext als einen konstanten Strom von Daten auffassen, den man Byte für Byte oder sogar Bit für Bit verarbeiten kann. Bildlich betrachtet legt man über den Plaintext-Strom einen gleichlangen Strom aus scheinbar zufällig generierten Daten, die von der Generatorfunktion, dem Herzstück eines Stream Ciphers, generiert werden. Dabei werden die ursprünglichen Daten des Plaintexts mit denen der Generatorfunktion mittels XOR verknüpft (Abb. 1).

bosslet_ruby_1.tif_fmt1.jpgAbb. 1: Stream Cipher

Man kann sich den Strom der Generatorfunktion als Schleier vorstellen, der sich mit dem Plaintext so verbindet, dass dem Außenstehenden im Nachhinein nicht mehr ersichtlich ist, was ursprünglich Schleier und was Plaintext war. Für den Uneingeweihten sieht das Resultat völlig willkürlich und nicht mehr von echten zufälligen Daten unterscheidbar aus. Um einen Ciphertext, der mit einem Stream Cipher verschlüsselt wurde, wieder zu entschlüsseln, muss man nun den exakt gleichen Strom aus scheinbar zufälligen Daten erneut generieren und ihn abermals mittels XOR mit dem Ciphertext verknüpfen. Auch hier kann man das auf einmal oder Byte für Byte machen. Wichtig ist, dass es exakt die gleichen Daten sind, die man mit der Generatorfunktion des Stream Ciphers generiert, und dass man sie an der exakt gleichen Position mit den Bytes des Ciphertexts mit XOR verknüpft. Dann kommen die Eigenschaften der XOR-Funktion zum Tragen und die scheinbar zufälligen Daten der Generatorfunktion heben sich auf. Was übrig bleibt, ist der ursprüngliche Plaintext. Der Schleier wird also sozusagen herausgekürzt und man hat am Ende wieder die unverschlüsselten Originaldaten.

Betrachten wir das Bild der Stream-Cipher-Konstruktion genauer, fällt auf, dass wir hier gar nicht wirklich von einem Ver- und Entschlüsseln reden können, da in beiden Fällen eine identische Operation durchgeführt wird: XOR mit dem Strom der Daten der Generatorfunktion. Wir können festhalten, dass Stream Cipher also im Grunde genommen nur aus einer einzigen Operation bestehen. Es macht in der Praxis keinen Unterschied, ob ich ver- oder entschlüssele, in beiden Fällen wird das Gleiche getan, nur die Daten, mit denen man das XOR durchführt, ändern sich. Bei Block Ciphern (Abb. 2) ist das anders. Sie bestehen aus zwei unterscheidbaren Funktionen, das Entschlüsseln unterscheidet sich vom Verschlüsseln. Dabei ist das Entschlüsseln die Umkehrfunktion des Verschlüsselns und ein valider Block Cipher muss unabhängig vom zugrunde liegenden Plaintext m folgende Invariante erfüllen:

D_k(E_k(m)) = m

wobei mit E_k die Verschlüsselung mit einem Schlüssel k und D_k die Entschlüsselungsfunktion mit demselben Schlüssel k bezeichnet sei. Es muss also unabhängig vom Plaintext immer gelten, dass die Verschlüsselung die Entschlüsselung aufhebt, sodass man nach dem Entschlüsseln des Ciphertexts stets den ursprünglichen Plaintext zurückerhält. Darüber hinaus arbeitet ein Block Cipher, wie es sein Name schon andeutet, nicht auf Daten beliebiger Länge, sondern man verarbeitet den Plaintext häppchenweise in Blöcken gleicher Länge. AES und viele weitere neuere Block Cipher arbeiten mit einer fixen Blockgröße von 16 Bytes, ältere Algorithmen wie etwa DES oder Blowfish, aber auch der von uns später zu implementierende TEA, arbeiteten mit einer Blockgröße von 8 Byte.

bosslet_ruby_2.tif_fmt1.jpgAbb. 2: Block Cipher

Der Plaintext wird also im Beispiel von AES zunächst in Häppchen à 16 Byte unterteilt und diese Blöcke werden dann individuell verarbeitet und am Ende wieder zusammengeführt. Je nachdem, ob man gerade ver- oder entschlüsselt, wird dabei entsprechend die Verschlüsselungs- oder die Entschlüsselungskomponente von AES auf die Blöcke angewandt. Im Unterschied zu Stream Ciphern wird der ursprüngliche Plaintext also nicht via XOR „verschleiert“, sondern die Plaintext-Blöcke werden von der Verschlüsselungsfunktion transformiert, entsprechend die Ciphertext-Blöcke von der Entschlüsselungsfunktion. Tatsächlich sind die Ent- und Verschlüsselungsfunktionen Permutationen, die nach außen hin völlig willkürlich erscheinen, sogenannte Pseudozufallspermutationen [4].

Um nun Plaintexte beliebiger Länge mit zum Beispiel AES zu verschlüsseln, wäre es also naheliegend, sie in Blöcke zu je 16 Byte aufzuteilen und die Blöcke jeweils mit der AES-Verschlüsselungsfunktion zu verschlüsseln. Dann werden die verschlüsselten Blöcke wie an einer Kette aufgereiht zusammenführt und dieses Ergebnis als finaler Ciphertext betrachtet. Zum Entschlüsseln würde man analog vorgehen, mit dem Unterschied, dass man statt der Verschlüsselungsfunktion die AES-Entschlüsselungsfunktion verwendet, um die einzelnen Plaintext-Blöcke wieder zu erhalten. Diese würde man aneinanderreihen, um den ursprünglichen Plaintext zurück zu erhalten. Leider ist es nicht ganz so einfach, denn wie wir später sehen werden, ist dieser so pragmatische und naheliegende Ansatz alles andere als sicher.

Ausführen der Codebeispiele

Sollten Sie Ruby noch nicht installiert haben, können Sie dies über ruby-lang.org oder mit einem Tool wie rvm.io oder rbenv (https://github.com/rbenv/rbenv) vornehmen.

Damit Sie auch die neueren Algorithmen nutzen können, sollten Sie auf Ihrem System OpenSSL in einer Version >= 1.1.0 installiert haben.

Danach können Sie die Codebeispiele einfach ausführen, indem Sie das Repository unter https://github.com/emboss/block-ciphers klonen, in das Verzeichnis code wechseln und dort die Beispiele mit ruby <beispiel.rb> ausführen.

Padding

Bevor wir uns Fragen nach der Sicherheit unserer Konstruktion stellen, müssen wir ein anderes Problem lösen, das unsere Konstruktion aufwirft: Was tun, wenn mein Plaintext eben nicht genau in Häppchen zu je 16 Byte teilbar ist? Das wird bei nämlich eher die Ausnahme sein. Zum Glück gibt es darauf eine einfache Antwort.

Zuerst einmal ist festzustellen, dass sich das Problem nur im allerletzten Block ...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang