Von
Anes Hodza
Anes Hodza
/31.10.22

«Let’s Talk About Secure Passwords»: US R&B and hip-hop band Salt 'n' Pepa.

Warum die sichere Speicherung von Kennwörtern so wichtig ist

Früher habe ich mich immer gefragt: Warum muss ich Passwörter sicher in meiner Datenbank speichern? Mit der richtigen Sicherheitsvorkehrung sollte doch niemand in der Lage sein, sie zu lesen. Damals war ich ein Amateurprogrammierer, der noch nie an einem richtigen Projekt gearbeitet hatte...

Nachdem ich meine Stelle bei der Renuo angetreten bin, wurde mir schnell klar, wie naiv ich war. Entwickler:innen sollten immer auf das Schlimmste vorbereitet sein, vor allem wenn sie für die Sicherheit verantwortlich sind.

Nehmen wir an, wir betreiben eine Datenbank für eine Social-Media-Plattform und eine Gruppe von Hackern dringt in die Datenbank ein. Es ist nicht wichtig, wie sie dort hineingekommen sind, aber oft sind es Insider, die für solche Angriffe verantwortlich sind. Nun extrahieren die Hacker alle Kennwörter aus unserer Datenbank. Da wir als naive Programmierende alle Passwörter im Klartext gespeichert haben, sind wir für den Verlust von Tausenden, wenn nicht Millionen von Passwörtern verantwortlich.

Erster Ansatz – Verschlüsselung

Nach dem obigen Beispiel haben wir unseren Anfängerfehler erkannt. Wir verwenden jetzt einen Verschlüsselungsalgorithmus, z. B. DES, um unsere Kennwörter zu schützen. Das mag eine gute Idee sein, ist es aber in Wirklichkeit nicht. Selbst wenn ein Insider nicht über die Verschlüsselung und damit auch nicht über den Entschlüsselungsschlüssel verfügt, können Hacker ihn mit Gewalt erzwingen. Oder sie machen es sich noch einfacher: Einer von ihnen meldet sich bei unserem Social-Media-Dienst an, wodurch er die input und output erhaltet, was es viel einfacher macht, an den key zu finden. Auch die asymmetrische Verschlüsselung ist kein guter Ansatz, da Passwörter immer noch entschlüsselt werden können.

Zweiter Ansatz – Digest

Also versuchen wir es mit digest. In Ruby sieht das wie folgt aus:

  def register
  encrypt = Passwort  
  sha2_digest = Digest::SHA2.new(256).hexdigest(encrypt)  
  @Passwort = sha2_digest
end

Dieser Ansatz ist fast so nutzlos wie die Speicherung im Klartext, weil er im Grunde auch nur aus Klartext besteht. Heutzutage gibt es riesige Tabellen mit Klartext und ihrer jeweiligen Digest-Version, so genannte Rainbow-Tabellen. Ein Angreifer kann unseren hash nachschlagen und das verwendete Passwort finden. Crackstation verfügt über 15GiB an Zeichenketten und deren digest!

Dritter Ansatz – Salting

Jetzt endlich werden wir etwas schlauer. Unser nächster Ansatz ist das Hinzufügen eines salt zu jedem Passwort. Ein salt ist eine zufällige Zeichenkette (vorzugsweise eine sehr lange), die in das Kennwort eingefügt wird, bevor es verschlüsselt wird. Mit einem guten salt werden die meisten Rainbow-Tabellen völlig nutzlos. Da sich ein digest komplett verändert, wenn wir nur ein Zeichen in der Eingabekette austauschen, können wir das salt einfach als Klartext in der Datenbank speichern.

In Ruby fügen wir ein salt wie folgt hinzu:

  def register
  salt = ('a'..'z').to_a.sample(8).join
  encrypt = Passwort + salt
  sha2_digest = Digest::SHA2.new(256).hexdigest(verschlüsseln)
  @salt = salt
  @Passwort = sha2_digest
end

Etwas müssen wir bei diesem Ansatz jedoch unbedingt beachten: Ein schwacher salt ist zwar besser wie kein salt, aber auch nicht ideal. Rainbow-Tabellen enthalten definitiv password123, aber sie können auch salt-password123 enthalten. In unserem Fall haben wir einen sehr schwachen Salt gesetzt, also verwendet diesen Ansatz bitte nicht in einer echten Software.

Vierter Ansatz – Peppering

Jetzt nutzen wir zwar salt, aber wenn ein Hacker Zugriff auf die Datenbank erhält, erhält er auch Zugriff auf alle Klartext-salts. Mit den salts kann er nicht viel anfangen, auch nicht im Klartext, aber wir wollen dennoch so sicher wie möglich agieren. Das können wir erreichen, indem wir einen pepper hinzufügen. Pepper ist eine weitere Zeichenkette, die sehr ähnlich wie ein salt funktioniert bloss mit einem kleinen, aber wichtigen Unterschied: Sie ist nicht von User zu User unterschiedlich, sondern besteht aus einer fest codierten Zeichenkette in unserem Code. In Ruby könnte das wie folgt aussehen:

  PEPPER = 'mypepper'

def registrieren
  salt = ('a'..'z').to_a.sample(8).join
  encrypt = Passwort + salt + PEPPER
  sha2_digest = Digest::SHA2.new(256).hexdigest(verschlüsseln)
  @salt = salt
  @Passwort = sha2_digest
end

Hinweis: Auch dieser pepper ist nur ein Beispiel und überhaupt nicht sicher. Bitte verwendet einen längeren und zufälligeren pepper für echte Software.

Wie ein Login funktioniert

Wenn sich nun ein User anmelden will, können wir nicht einfach die Eingabe mit dem Passwort vergleichen. Denn auf der einen Seite haben wir zusätzliche Zeichenfolgen hinzugefügt, auf der anderen Seite haben wir einen digest verwendet, der es unmöglich macht, ihn wieder in Klartext umzuwandeln. Wir müssen die Eingabe nehmen, salt und pepper anhängen, sie «verdauen» (digest) und dann mit unserem Datenbankeintrag vergleichen. Das könnte folgendermassen aussehen:

  PEPPER = 'mypepper'

def login
  encrypt = input + @salt + PEPPER
  sha2_digest = Digest::SHA2.new(256).hexdigest(verschlüsseln)
  @Passwort == sha2_digest
end

Wenn das Kennwort und unser digest übereinstimmen, wissen wir, dass der User das richtige Kennwort eingegeben hat, ohne das Kennwort zu kennen, was es sehr sicher macht.

Wenn ihr nun ein Beispiel dafür sehen möchtet, wie diese Methoden in einem echten Backend verwendet wurden, lest euch gerne Teil zwei dieses Blogeintrags auf DEV durch.

Wenn ihr einen Blick auf den Quellcode werfen möchtet, findet ihr ihn auf GitHub.

Diese Blogeintrag ist zuerst auf DEV erschienen.