Sicheres Passwort-Hashing in Go
5. Dezember 2023
0 Min. LesezeitBenutzeranmeldedaten sind die Informationen, die erforderlich sind, um die Identität eines Benutzers zu authentifizieren und ihm Zugriff auf ein System oder eine Anwendung zu gewähren. Dazu gehören üblicherweise ein Benutzername oder eine E-Mail-Adresse und ein Passwort. Während ein Benutzername als Klartext in einer Datenbank gespeichert werden kann, sollten sensible Informationen wie E-Mail-Adressen oder Passwörter dort nicht abgelegt werden.
Wenn ein Angreifer Zugriff auf Ihre Datenbank erhält, in der Sie diese Informationen speichern, möchten Sie ihm diese nicht einfach aushändigen. Stattdessen sollten Sie sicherstellen, dass er nicht auf diese Informationen zugreifen kann – selbst wenn er mit allen verfügbaren Ressourcen hartnäckig danach sucht. Hier kommt das Hashing ins Spiel.
Auf unseren vorherigen Beitrag zum Thema Passwort-Hashing in Java aufbauend, geht es in diesem Beitrag um Passwort-Hashing in Go. Sehen wir uns zunächst einige Konzepte und Best Practices zum Passwort-Hashing noch einmal an.
Passwörter hashen
Ein Hash ist eine mathematische Funktion, die eine Eingabe (in diesem Fall ein Passwort) entgegennimmt und eine Zeichenfolge mit fester Länge ausgibt, die üblicherweise aus Buchstaben und Zahlen besteht. Die Ausgabe, der sogenannte Hashwert oder Hashcode, ist eindeutig der Eingabe zugeordnet. Hashfunktionen sind als Einwegfunktionen konzipiert. Das bedeutet, dass es mit vertretbarem Rechenaufwand nicht möglich ist, die ursprüngliche Eingabe aus dem Hashwert zurückzugewinnen. Beim sicheren Passwort-Hashing werden Passwörter gehasht und als Hashwerte statt im Klartext gespeichert, um die Sicherheit zu erhöhen.
Der Unterschied zwischen Hashing und Verschlüsselung besteht darin, dass Hashing eine Einwegfunktion ist, während Verschlüsselung eine Zweiwegfunktion ist, mit der sich der ursprüngliche Wert aus dem verschlüsselten Wert wiederherstellen lässt. Verschlüsselung wird häufig für sensible Daten wie E-Mail-Adressen und andere personenbezogene Informationen verwendet. Für Passwörter empfiehlt sich jedoch Hashing.
Wenn Hashing eine Einwegfunktion ist, wie überprüfen wir dann Benutzeranmeldedaten? Das Verfahren ist einfach: Wir erstellen einen Hash des Klartext-Passworts, das der Benutzer angibt, und vergleichen ihn mit dem in unserer Datenbank gespeicherten Hash. Stimmen die Hashes überein, ist das Passwort korrekt. Stimmen sie nicht überein, sind die Anmeldedaten ungültig. Das Schöne am Hashing ist, dass wir das tatsächliche Passwort eines Benutzers nie kennen müssen!
Angriffsmethoden
Wie bereits erwähnt, möchten Sie sicherstellen, dass Angreifer selbst dann nicht auf diese Informationen zugreifen können, wenn sie mit allen verfügbaren Ressourcen hartnäckig danach suchen. Es gibt Methoden, mit denen Angreifer das Passwort aus dem Hash ermitteln können.
Ein Hacker verfügt möglicherweise über eine Liste Ihrer Passwörter aus anderen Quellen, etwa gehackten Konten. Mit einer Rainbow Table kann er versuchen, den Hash zu knacken. Stimmen die Hashes überein, hat er das Passwort geknackt. Dazu können auch Brute-Force-Angriffe gehören, bei denen verschiedene Passwortkombinationen ausprobiert werden, um festzustellen, ob der Hash des generierten Passworts mit dem Hash des ursprünglichen Passworts übereinstimmt.
Diese Methoden erfordern zwar leistungsstarke Ressourcen oder eine hohe Rechenleistung, haben sich aber bei älteren Hashing-Algorithmen wie der SHA1-Familie angesichts der heute verfügbaren Rechenleistung als erfolgreich erwiesen.
Daher ist es äußerst wichtig, neuere Hashing-Algorithmen und weitere Best Practices zu verwenden, damit der erzeugte Hash unmöglich zu knacken ist – oder das Knacken mehrere Jahrzehnte an Permutationen und Kombinationen erfordern würde.
Best Practices für Hashing
Hier sind einige Best Practices für den Einsatz moderner Hashing-Verfahren.
Salt-Werte
Ein Salt-Wert ist ein zufällig generierter Wert (meist zufällige Binärdaten wie zufällige Bytes), der beim Erstellen eines Hashes zum Passwort hinzugefügt wird. Jedes Passwort erhält einen eigenen, eindeutigen Salt-Wert. Das bietet eine zusätzliche Sicherheitsebene, denn Angreifer müssten neben dem Hash auch den Salt-Wert knacken. Das Knacken des Hashes wird dadurch zu einem zweistufigen Verfahren, das mehr Ressourcen erfordert.
Die Länge des Salt-Werts hängt vom verwendeten Hashing-Algorithmus ab. Bei neueren Algorithmen wird der Salt-Wert automatisch generiert, sodass Sie keinen angeben müssen. Weitere Informationen finden Sie in diesem OWASP Password Storage Cheat Sheet.
Hashing-Algorithmen
Hashing-Algorithmen spielen beim Passwort-Hashing eine entscheidende Rolle. Daher ist es wichtig, einen Algorithmus auszuwählen, der gegen Knacken resistent ist. Bei modernen Hashing-Algorithmen lassen sich Einstellungen wie der Arbeitsfaktor und die Anzahl der Hashing-Iterationen anpassen, wodurch sich der Hash schwerer knacken lässt. Angesichts der verfügbaren Rechenleistung empfiehlt es sich, höhere Werte zu wählen. Grundsätzlich gilt: Je höher der Arbeitsfaktor oder die Anzahl der Iterationen, desto schwieriger lässt sich der Hash knacken.
Als Best Practice sollten Sie den höchstmöglichen Arbeitsfaktor oder die höchstmögliche Anzahl an Iterationen wählen, die Ihr System ohne erhebliche Leistungseinbußen bewältigen kann.
Überlegungen zu Hash-Algorithmen
Bei der Implementierung eines Hash-Algorithmus sollten Sie Folgendes beachten.
Umgang mit Hash-Kollisionen
Hash-Kollisionen treten auf, wenn zwei verschiedene Eingaben denselben Hashwert erzeugen. Moderne Hashing-Algorithmen sind zwar darauf ausgelegt, die Wahrscheinlichkeit von Kollisionen zu minimieren, dennoch können sie auftreten. Um das Kollisionsrisiko zu verringern, empfiehlt es sich, längere Hashwerte wie SHA-256 oder SHA-512 zu verwenden. Sie bieten einen größeren Ausgaberaum und senken damit die Wahrscheinlichkeit von Kollisionen.
Anzahl der Iterationen
Die Anzahl der Iterationen ist ein wichtiger Faktor beim Passwort-Hashing. Je höher die Anzahl der Iterationen, desto mehr Ressourcen erfordert es, den Hash zu knacken, wodurch der Angriff verlangsamt wird. Wichtig ist jedoch, ein Gleichgewicht zwischen Sicherheit und Leistung zu finden, denn zu viele Iterationen können bei der Authentifizierung zu Verzögerungen für legitime Benutzer führen. Es empfiehlt sich, einen Wert zu wählen, der ausreichende Sicherheit bietet, ohne die Leistung erheblich zu beeinträchtigen.
Hashing-Algorithmen regelmäßig aktualisieren
Mit dem technologischen Fortschritt und zunehmender Rechenleistung werden neue Hashing-Algorithmen entwickelt und ältere als veraltet eingestuft. Der Grund dafür ist, dass ältere Algorithmen mit diesen Fortschritten nicht mehr Schritt halten können und zu einfachen Zielen für Angreifer werden.
Passwort-Hashing in Go
Das crypto-Paket von Go bietet integrierte Hashing-Funktionen für gängige Algorithmen wie argon2, scrypt, bcrypt, pbkdf2 und weitere. Diese Funktionen sind äußerst nützlich, um Passwort-Hashing zu implementieren. Das bcrypt-Paket stellt sogar eine Funktion CompareHashAndPassword bereit, mit der Klartextpasswörter mit gehashten Passwörtern verglichen und auf Übereinstimmung geprüft werden können.
Argon2
Argon2 gilt derzeit als sicherster Hashing-Algorithmus (auch wenn sich das in Zukunft ändern kann). Es gibt drei Varianten: Argon2d, das den Schutz vor GPU-basierten Knackversuchen maximiert; Argon2i, das für den Schutz vor Seitenkanalangriffen optimiert ist; und Argon2id, eine Kombination aus beiden.
Das OWASP Password Storage Cheat Sheet empfiehlt, den hybriden Argon2id-Algorithmus zur Passwortspeicherung zu verwenden. Das Cheat Sheet gibt folgende Empfehlungen für die Mindestgröße des Arbeitsspeichers (m), die Mindestanzahl der Iterationen (t) und den Parallelitätsgrad (p):
m=47104 (46 MiB), t=1, p=1 (Nicht mit Argon2i verwenden)
m=19456 (19 MiB), t=2, p=1 (Nicht mit Argon2i verwenden)
m=12288 (12 MiB), t=3, p=1
m=9216 (9 MiB), t=4, p=1
m=7168 (7 MiB), t=5, p=1
Implementierung in Go
Sehen wir uns nun eine Beispielimplementierung von Argon2id an, mit der ein Hash aus einem Klartextpasswort erstellt und mit diesem verglichen wird.
Einrichtung
Zunächst erstellen wir eine Struct, in der alle Konfigurationsparameter für das Hashing mit Argon2id gespeichert werden:
Die Struct Argon2idHash enthält die folgenden Parameter: time, memory (Rechenaufwand), threads (Parallelität), keyLen und saltLen. Diese Parameter werden verwendet, um aus einem Klartextpasswort einen Hash zu erzeugen.
Als Nächstes schreiben wir eine Konstruktorfunktion, um das Struct mit den angegebenen Werten zu initialisieren.
Salt-Wert generieren
Um die Generierung zufälliger Werte, insbesondere von Salt-Werten, zu vereinfachen, haben wir die Funktion randomSecret entwickelt. Diese Funktion übernimmt die gewünschte Länge des zufälligen Geheimnisses und verwendet die Funktion rand.Read, um es zu generieren.
Hash generieren
Als Nächstes erstellen wir die Hashing-Methode GenerateHash für die Struct Argon2idHash. Mit dieser Methode können wir unter Verwendung der konfigurierten Werte einen Hash erzeugen. Wurde kein zufälliger Salt-Wert angegeben, generieren wir einen mit der konfigurierten Salt-Länge. Anschließend verwenden wir alle konfigurierten Werte, um mithilfe der Funktion argon2.IDKey den Hash zu generieren. Bei erfolgreicher Generierung geben wir das Hash-Salt-Paar zurück.
Passwortvergleich
Für den Passwortvergleich erstellen wir eine weitere Methode namens Compare für die Struct Argon2idHash. Diese Methode erhält den gespeicherten Hash und Salt-Wert sowie das zu vergleichende Passwort.
Zunächst generieren wir mithilfe der im vorherigen Schritt erstellten Funktion GenerateHash einen Hash. Als Eingabe verwendet die Funktion das angegebene Passwort und den Salt-Wert.
Anschließend vergleichen wir den generierten Hash mithilfe der Funktion bytes.Equal mit dem gespeicherten Hash. Sind die beiden Hashes gleich, liegt eine Übereinstimmung vor. Sind sie nicht gleich, stimmt der gespeicherte Hash nicht mit dem angegebenen Passwort überein.
In diesem Beispiel haben wir die Funktion argon2.IDKey verwendet, um den Hash zu generieren. Wie bereits erwähnt, bieten auch andere von Go bereitgestellte crypto-Pakete ähnliche Funktionen, mit denen sich sichere Passwort-Hashes erzeugen lassen – wie in unserem Beispiel.
Scrypt
Scrypt steht an zweiter Stelle auf der Liste der Hashing-Algorithmen. Argon2id sollte Ihre erste Wahl sein. Falls dieser Algorithmus nicht verfügbar ist, verwenden Sie scrypt als Ausweichlösung.
Wie bei Argon2id empfiehlt das OWASP Password Storage Cheat Sheet folgende Werte für die Felder: Mindestkostenparameter für CPU/Speicher (N), Blockgröße (r) und Parallelitätsgrad (p).
N = 2^17 (128 MiB), r = 8 (1024 Byte), p = 1
N = 2^16 (64 MiB), r = 8 (1024 Byte), p = 2
N = 2^15 (32 MiB), r = 8 (1024 Byte), p = 3
N = 2^14 (16 MiB), r = 8 (1024 Byte), p = 5
N = 2^13 (8 MiB), r = 8 (1024 Byte), p = 10
Die Go-Paketdokumentation für das scrypt-Paket enthält ein Beispiel dafür, wie Sie mit scrypt einen Hash-Schlüssel generieren. Die Generierung und der Vergleich sollten dem Vorgehen für die Struct Argon2idHash sehr ähnlich sein.
Bcrypt
bcrypt sollte Ihre letzte Wahl sein. Ist die FIPS-140-Konformität erforderlich, verwenden Sie stattdessen PBKDF2 (das wir als Nächstes behandeln). Laut OWASP Password Storage Cheat Sheet sollte der Arbeitsfaktor so hoch wie möglich sein, ohne die Leistung des Verifizierungsservers zu beeinträchtigen; der Mindestwert beträgt 10.
Das bcrypt-Paket in Go enthält Funktionen zum Generieren und Vergleichen von Hashes. Daher gehen wir hier kein Beispiel durch. Beispiele finden Sie in der Go-Paketdokumentation.
PBKDF2 (FIPS-140-konform)
Laut OWASP Password Storage Cheat Sheet wird der Arbeitsfaktor für PBKDF2 über die Anzahl der Iterationen festgelegt. Der Wert sollte je nach verwendetem internem Hashing-Algorithmus unterschiedlich sein.
PBKDF2-HMAC-SHA1: 1.300.000 Iterationen
PBKDF2-HMAC-SHA256: 600.000 Iterationen
PBKDF2-HMAC-SHA512: 210.000 Iterationen
Hashing-Beispiel
Da PBKDF2 der einzige FIPS 140-konforme Algorithmus auf unserer Liste ist, sehen wir uns an, wie Sie ihn für das Passwort-Hashing in Go verwenden. Legen wir los.
Die Key-Funktion aus dem pbkdf2-Paket, mit der wir aus einem Klartextpasswort einen Hash generieren, sehen Sie hier:
Für das Hashing benötigen Sie das Passwort und den Salt als Byte-Slices sowie die Anzahl der Iterationen und die gewünschte Länge des generierten Schlüssels oder Hashes. Außerdem wird eine Hashing-Funktion benötigt. In unserem Fall bezeichnet das Passwort das Klartextpasswort, das wir hashen möchten, während der Salt ein zufällig generierter Wert ist. Die Anzahl der Iterationen und die Schlüssellänge lassen sich konfigurieren. Als Hashing-Funktion verwenden wir sha3.New256.
Einrichtung
Hash generieren
Passwortvergleich
Das ist unserer Implementierung von Argon2idHash sehr ähnlich. Sie können daher das vollständige Codebeispiel so anpassen, dass Sie stattdessen PBKDF2Hash verwenden.
Vollständiger Code
Fügen wir nun alles zusammen.
Projektstruktur
Die Datei argon2id.go:
Die Datei main.go:
Wählen Sie Ihren Algorithmus mit Bedacht
Die obigen Beispiele bieten Ihnen einen guten Einstieg in die Implementierung sicherer Hashing-Algorithmen zur Passwortspeicherung in Go. Denken Sie daran, dass nicht alle Hashing-Algorithmen gleichwertig sind. Wählen Sie daher entsprechend Ihren Anforderungen und der aktuellen Sicherheit des Algorithmus mit Bedacht. Was heute als sicherer Algorithmus gilt, kann sich im Laufe der Zeit ändern. Deshalb ist es ratsam, dies regelmäßig neu zu bewerten.
Beachten Sie außerdem, dass SAST-Tools wie Snyk Code Ihnen dabei helfen können, veraltete Algorithmen zu erkennen und Sie bei Bedarf über geeignete Aktualisierungen zu informieren. Wählen Sie Ihren Passwort-Hashing-Algorithmus mit Bedacht, bleiben Sie auf dem Laufenden und viel Spaß beim Programmieren!



