27 Kryptografie 

Kryptografische Algorithmen sind ein wichtiger Bestandteil moderner Software. Sie werden zur klassischen symmetrischen oder asymmetrischen Verschlüsselung eingesetzt, beispielsweise um sensible Daten des Benutzers zu schützen. Ein weiterer Anwendungsfall kryptografischer Algorithmen ist das Hashing, bei dem es darum geht, aus komplexen Instanzen, etwa langen Dokumenten, einen kurzen, möglichst kollisionsfreien Hash-Wert zu bestimmen. Dieser Hash-Wert kann dann zum Beispiel zum effizienten Vergleich zweier Dokumente verwendet werden.
Wir werden im Folgenden zunächst das Modul hashlib der Standardbibliothek besprechen, das verschiedene Hash-Funktionen implementiert, und danach das Drittanbietermodul PyCrypto behandeln, das eine umfassende Sammlung kryptografischer Algorithmen bereitstellt.
27.1 Hash-Funktionen – hashlib 

Das Modul hashlib der Standardbibliothek implementiert die gängigsten Hash-Funktionen. Das sind komplexe Algorithmen, die aus einem Parameter, zumeist einem String, einen Hash-Wert berechnen. Wozu kann ein solcher Hash-Wert verwendet werden?
Stellen Sie sich vor, Sie würden eine Forensoftware entwickeln, die später für eine Community im Internet eingesetzt werden soll. Bevor ein Benutzer Beiträge im Forum verfassen darf, muss er sich mit seinem Benutzernamen und dem dazu passenden Passwort anmelden. Natürlich ist es im Sinne des Forenbetreibers und vor allem des Benutzers selbst, dass das Passwort nicht in falsche Hände gerät. Es stellt sich also die Frage, wie die Anmeldeprozedur möglichst sicher gestaltet werden kann.
Die intuitivste Möglichkeit wäre es, Benutzernamen und Passwort an die Forensoftware zu übermitteln. Dort werden diese beiden Informationen mit den Anmeldedaten aller Benutzer verglichen, und bei einem Treffer wird der Zugang zum Forum ermöglicht.
Würde eine solche Software die Anmeldeprozedur tatsächlich so durchführen, müssten Benutzername und Passwort im Klartext in der internen Datenbank des Forums gespeichert werden. Das ist beim Benutzernamen kein größeres Problem, da es sich dabei im Allgemeinen um eine öffentliche Information handelt. Doch das Passwort im Klartext in einer solchen Datenbank zu speichern, wäre grob fahrlässig. Ein Angreifer, der über eine Sicherheitslücke in einem anderen Teil der Software Zugriff auf die Datenbank erlangt, wäre sofort im Besitz aller Passwörter der angemeldeten Benutzer. Das wird besonders dann brisant, wenn man bedenkt, dass viele Leute das gleiche Passwort für mehrere Benutzerkonten verwenden.
Wünschenswert wäre es also, die Korrektheit eines Passworts mit an Sicherheit grenzender Wahrscheinlichkeit zu ermitteln, ohne Referenzpasswörter im Klartext speichern zu müssen. Und genau hier kommen Hash-Funktionen ins Spiel. Eine Hash-Funktion bekommt einen Parameter übergeben und errechnet daraus den sogenannten Hash-Wert. Wenn sich jetzt ein neuer Benutzer bei der Forensoftware anmeldet und sein Passwort wählt, wird dieses nicht im Klartext in die Datenbank eingetragen, sondern es wird der Hash-Wert des Passworts gespeichert.
Beim Einloggen schickt der Benutzer sein Passwort an den Server. Dieser errechnet dann den Hash-Wert des übertragenen Passworts und vergleicht ihn mit den gespeicherten Hash-Werten.[ 111 ](Da Hash-Funktionen deterministisch sind, ist es für den Angreifer weiterhin möglich, Passwörter auszuprobieren und die Hash-Werte mit den in der Datenbank gespeicherten zu vergleichen. Ein solcher Wörterbuchangriff wird mithilfe eines Salts erschwert. Das ist eine Zufallszahl, die an ein Passwort gehängt wird, bevor dessen Hash-Wert bestimmt wird. )
Damit eine solche Anmeldeprozedur funktioniert und ein potenzieller Angreifer auch mit Zugriff auf die Datenbank keine Passwörter errechnen kann, müssen Hash-Funktionen einige Bedingungen erfüllen:
- Eine Hash-Funktion stellt eine Einwegcodierung dar. Das heißt, dass die Berechnung des Hash-Wertes nicht umkehrbar ist, man also aus einem Hash-Wert nicht auf den ursprünglichen Parameter schließen kann.
- Bei Hash-Funktionen treten grundsätzlich sogenannte Kollisionen auf, das sind zwei verschiedene Parameter, die denselben Hash-Wert ergeben. Ein wesentlicher Schritt zum Knacken einer Hash-Funktion ist es, solche Kollisionen berechnen zu können. Eine Hash-Funktion sollte also die Berechnung von Kollisionen so stark erschweren, dass sie nur unter extrem hohem Zeitaufwand zu bestimmen sind.
- Eine Hash-Funktion sollte möglichst willkürlich sein, sodass man nicht aufgrund eines ähnlichen Hash-Wertes darauf schließen kann, dass man in der Nähe des gesuchten Passworts ist. Sobald der Parameter der Hash-Funktion minimal verändert wird, sollte ein völlig anderer Hash-Wert berechnet werden.
- Zu guter Letzt sollte eine Hash-Funktion schnell zu berechnen sein. Außerdem müssen sich die entstehenden Hash-Werte untereinander effizient vergleichen lassen.
Das Anwendungsfeld von Hash-Funktionen ist weit gefächert. So werden sie, abgesehen von dem oben genannten Passwortbeispiel, unter anderem auch zum Vergleich großer Dateien verwendet. Anstatt diese Dateien untereinander Byte für Byte zu vergleichen, werden ihre Hash-Werte berechnet und verglichen. Mit den Hash-Werten lässt sich sagen, ob die Dateien mit Sicherheit verschieden oder mit großer Wahrscheinlichkeit identisch sind. Das ist besonders dann interessant, wenn es aufgrund eingeschränkter Bandbreite gar nicht möglich ist, die Dateien direkt zu vergleichen. So ist der Vergleich der Hash-Werte beispielsweise die effizienteste Methode, die Authentizität einer aus dem Internet heruntergeladenen Datei zu überprüfen.
Beachten Sie, dass die Wahrscheinlichkeit einer Kollision bei den im Modul hashlib implementierten Verfahren sehr gering, aber theoretisch immer noch vorhanden ist.
27.1.1 Verwendung des Moduls 

Zunächst enthält das Modul hashlib eine Reihe von Klassen, die jeweils einen Hash-Algorithmus implementieren:
Tabelle 27.1 Unterstützte Hash-Funktionen
[»] Hinweis
Beachten Sie, dass die Algorithmen MD5 und SHA-1 bereits ansatzweise gebrochen wurden. Sie sollten daher in sicherheitsrelevanten Anwendungen nicht mehr verwendet werden.
Die Verwendung dieser Klassen ist identisch. Deshalb wird sie hier exemplarisch an der Klasse md5 gezeigt. Beim Instanziieren der Klasse md5 wird eine bytes-Instanz übergeben, deren Hash-Wert berechnet werden soll.
>>> import hashlib
>>> m = hashlib.md5(b"Hallo Welt")
Durch Aufruf der Methode digest wird der berechnete Hash-Wert als Byte-Folge zurückgegeben. Beachten Sie, dass die zurückgegebene bytes-Instanz durchaus nicht druckbare Zeichen enthalten kann.
>>> m.digest()
b'\\7*2\xc9\xaet\x8aL\x04\x0e\xba\xdcQ\xa8)'
Durch Aufruf der Methode hexdigest wird der berechnete Hash-Wert als String zurückgegeben, der eine Folge zweistelliger Hexadezimalzahlen enthält. Diese Hexadezimalzahlen repräsentieren jeweils ein Byte des Hash-Wertes. Der zurückgegebene String enthält ausschließlich druckbare Zeichen.
>>> m.hexdigest()
'5c372a32c9ae748a4c040ebadc51a829'
27.1.2 Weitere Algorithmen 

Neben den eingangs aufgelisteten Hash-Algorithmen, die garantiert in hashlib vorhanden sind, stellt das Modul eine Reihe weiterer Algorithmen bereit, deren Vorhandensein von den Gegebenheiten des Betriebssystems abhängt. Diese zusätzlichen Algorithmen lassen sich über die Funktion new instanziieren; ihr muss der Name des Algorithmus übergeben werden:
>>> m = hashlib.new("md4", b"Hallo Welt")
>>> m.hexdigest()
'5f7efe84c39847ee689edb9a7848ad74'
Die Menge der insgesamt zur Verfügung stehenden Algorithmen wird über algorithms_available bereitgestellt:
>>> hashlib.algorithms_available
{'SHA', 'mdc2', 'RIPEMD160', 'DSA', 'SHA384', 'sha384', 'shake_128', 'ripemd160', 'blake2s', 'sha3_384', 'sha3_224', 'blake2b',
'ecdsa-with-SHA1', 'dsaEncryption', 'MDC2', 'sha224', 'sha3_256', 'sha1',
'sha3_512', 'shake_256', 'SHA512', 'sha', 'SHA1', 'md5', 'dsaWithSHA', 'md4',
'sha512', 'sha256', 'whirlpool', 'SHA256', 'MD4', 'MD5', 'SHA224', 'DSA-SHA'}
27.1.3 Vergleich großer Dateien 

Hash-Funktionen berechnen aus einer prinzipiell unbegrenzten Datenmenge einen kurzen Hash-Wert. Aufgrund der Eigenschaften einer Hash-Funktion ist die Wahrscheinlichkeit, zwei verschiedene Datenmengen zu finden, die den gleichen Hash-Wert ergeben, sehr gering. Dadurch eignen sich Hash-Funktionen dazu, große Dateien miteinander zu vergleichen, ohne dass die Dateien an einem gemeinsamen Ort liegen müssen. Auf diese Weise lässt sich beispielsweise feststellen, ob eine auf einem Server gespeicherte Datei neu hochgeladen werden muss, weil sie sich auf dem Rechner des Nutzers verändert hat.
Das folgende Beispielprogramm liest zwei Dateien ein und vergleicht sie anhand ihrer Hash-Werte:
import hashlib
with open("datei1.txt", "rb") as f1, open("datei2.txt", "rb") as f2:
if hashlib.md5(f1.read()).digest() == hashlib.md5(f2.read()).digest():
print("Die Dateien sind gleich")
else:
print("Die Dateien sind verschieden")
In diesem Fall wurde die verbreitete Hash-Funktion md5 verwendet, es können aber auch die anderen in hashlib enthaltenen Funktionen eingesetzt werden.
Für die Arbeit mit Datenströmen enthalten die Hash-Klassen die Methode update, mit deren Hilfe sich die bei der Erzeugung angegebene Datenmenge erweitern lässt:
>>> h1 = hashlib.md5(b"Erstens.")
>>> h1.update(b"Zweitens.")
>>> h1.update(b"Drittens.")
>>>
>>> h2 = hashlib.md5(b"Erstens.Zweitens.Drittens.")
>>> h1.digest() == h2.digest()
True
27.1.4 Passwörter 

Das folgende Beispielprogramm verwendet das Modul hashlib, um einen Passwortschutz zu realisieren. Das Passwort soll dabei nicht als Klartext im Quelltext gespeichert werden, sondern als Hash-Wert. Dadurch ist gewährleistet, dass die Passwörter nicht einsehbar sind, selbst wenn jemand in den Besitz der Hash-Werte kommen sollte. Auch anmeldepflichtige Internetportale wie beispielsweise Foren speichern die Passwörter der Benutzer als Hash-Werte.
import hashlib
pwhash = "578127b714de227824ab105689da0ed2"
m = hashlib.md5(bytes(input("Ihr Passwort bitte: "), "utf-8"))
if pwhash == m.hexdigest():
print("Zugriff erlaubt")
else:
print("Zugriff verweigert")
Das Programm liest ein Passwort vom Benutzer ein, errechnet den MD5-Hash-Wert dieses Passworts und vergleicht ihn mit dem gespeicherten Hash-Wert. Der vorher berechnete Hash-Wert pwhash ist in diesem Fall im Programm vorgegeben. Unter normalen Umständen stünde er mit anderen Hash-Werten in einer Datenbank oder wäre in einer Datei gespeichert. Wenn beide Werte übereinstimmen, wird symbolisch »Zugriff erlaubt« ausgegeben. Das Passwort für dieses Programm lautet »Mein Passwort«.
Einen Hash-Wert zum Speichern von Passwörtern zu verwenden, ist gängige Praxis. Die bislang besprochenen Hash-Funktionen, darunter insbesondere die oben eingesetzte Funktion md5, eignen sich dazu aber nur bedingt, da sie anfällig gegenüber Brute-Force-Angriffen sind. Damit Passwörter sicher gespeichert werden können, muss eine Hash-Funktion weitere Eigenschaften besitzen:
- Sie muss einen Salt unterstützen. Das ist eine Zeichenfolge, die an das Passwort angehängt wird, bevor der Hash-Wert berechnet wird. Auf diese Weise können für zwei Benutzer verschiedene Hash-Werte gespeichert werden, selbst wenn sie das gleiche Passwort verwenden. Das verhindert das Knacken von Passwörtern mithilfe vorberechneter Klartexttabellen, sogenannter Rainbow Tables.
- Sie muss in einer parametrisierbaren Anzahl Runden ablaufen, damit die Rechendauer der Hash-Funktion eingestellt werden kann. Das erschwert das massenhafte Ausprobieren möglicher Passwörter.
- Speziell für das Speichern von Passwörtern enthält das Modul hashlib die Funktion pbkdf2_hmac[ 112 ](für Password-Based Key Derivation Function 2 ):
pbkdf2_hmac(name, password, salt, rounds)
Sie berechnet einen Passwort-Hash für das Passwort password mit dem Salt salt unter Verwendung von round Runden des Algorithmus. Der hier implementierte Algorithmus basiert auf einer der grundlegenden Hash-Funktionen, die zu Beginn des Abschnitts besprochen wurden. Über den Parameter name kann festgelegt werden, welche der Hash-Funktionen verwendet werden soll:
>>> hashlib.pbkdf2_hmac("sha256", b"password", b"salt", 100000)
b'\x03\x94\xa2\xed\xe32\xc9\xa1>\xb8.\x9b$c\x16\x04\xc3\x1d\xf9x\xb4\xe2\xf0\xfb\xd2\xc5I\x94O\x9dy\xa5'