Schon vor einiger Zeit bin ich auf eine sehr schöne Lösung gestoßen, mit der sich ein sicherer Login mit Einmalpassworte realisieren lässt. Dabei wird ein Hardware USB Dongle verwendet, welcher nach einem Druck auf einen Taster ein Einmalpasswort generiert. Die serverseitige Prüfung kann aus einer beliebigen Sprache heraus passieren. Für mich bietet sich hier natürlich PHP an. In meinem Fall verwende ich für die Applikation das Zend Framework, aber die hier vorgestellte Lösung ist allgemein verwendbar und unabhängig vom restlichen Backend.

Was ist ein Einmalpasswort?

Wie der Name bereits sagt, kann ein Einmalpasswort nur für einen Login verwendet werden. Dann erlischt die Gültigkeit und ein neues Einmalpasswort wird benötigt. Ein solches Passwort wird durch eine spezielle Hardware (hier OpenKubus Stick) erzeugt. Eine genaue Definition findet ihr auch in Wikipedia, wo auch verschiedene Algorithmen vorgestellt werden. Der englischsprachige Begriff ist One Time Passwort und hierzu findet man auch auf dem internationalen Wikipedia eine interessante Beschreibung.

Vorteile Einmalpasswörter

Zu Beginn stellt sich die Frage, welche Vorteile ein Login mit Einmalpasswörter mit sich bringt. Hier macht es Sinn, sich der Problemstellung von Seite der Bedrohungen aus zu nähern und zu analysieren, ob ein solches System für den eigenen Anwendungsfall eine sinnvolle Maßnahme ist oder nicht. Mögliche Bedrohungen, gegen die ein Einmalpasswort eine risikominimierende Wirkung hat sind:

  • an erster Stelle natürlich eine sichere Authentifikation: nur wer den Stick besitzt kann sich auch in das System einloggen
  • ungeeigneter Umgang mit Passwörter: es kann nicht sichergestellt werden, dass Benutzer unsichere Passwörter verwenden
  • Abhören von Leitungen/Ausspähen des Passwortes: es kann passieren, dass in einer unsicheren Umgebung die eigenen Zugangsdaten abgehört werden. Hier wäre auch eine verschlüsselte Verbindung eine empfehlenswerte Gegenmaßnahme, aber auch ein Einmalpasswort bietet hier einen geeigneten Schutz, da dieses nur für einen Login gültig ist.
  • systematisches Ausprobieren von Passwörter: es kann passieren, dass ein Brute Force Angriff auf das System durchgeführt wird. Ein Einmalpasswort ist in der Regel sehr lange und somit schwerer zu knacken
  • Trojanische Pferde: am Clientrechner könnte ein Schadprogramm installiert sein, welches die Zugangsdaten ausspäht

Natürlich ist ein Einmalpasswort kein Allheilmittel. So schützt es nicht vor Pishing Angriffen, oder Man in the Middle Attacken, wo die Logindaten abgefangen und erst garnicht an den Server gesendet werden.

Komponenten

An dieser Stelle will ich zuerst die verschiedenen Komponenten vorstellen, die für die Lösung verwendet wurden. Natürlich ist alles OpenSource und frei verfügbar. Auch der Hardwaredongle ist frei und recht günstig zu erstehen.

OpenKubus

OpenKubus

Der OpenKubus ist ein kleiner, handlicher USB Stick (der Problemlos an den Schlüsselbund passt), dessen Hardwarelayout frei verfügbar ist und der frei programmiert werden kann. Wird der Stick an einem Rechner angesteckt, so wird er als Tastatur erkannt. Ein Druck auf einen Taster führt dazu, dass der Stick ein neues Einmalpasswort generiert und als Tastatureingabe an den Rechner sendet. Es reicht also, den Cursor auf das Eingabefeld im Login Formular zu setzen und der Stick schießt sein Passwort hinein. Dabei ist der Stick kompatibel zu nahezu allen Systemen, weil er als einfache Tastatur arbeitet.

Der Stick kostet 24,95 Euro und kann im embedded projects Shop bestellt werden. Beispielprogramme und die nötige Software um den Stick zu programmieren (was unter Linux recht leicht möglich ist), sind auf der google code Projektseite zu finden. Dort gibt es auch Anleitungen und ein Beispielserver in Perl. Der OpenKubus kann auch für einen sicheren Linux Login (PAM) verwendet werden.

phpseclib

phpseclib

Um das Passwort zu entschlüsseln (AES), benötigt man eine passende Bibliothek. phpseclib bietet eine komfortable und objektorientierte Lösung, die auf keine PHP Extensions oder ähnliches angewiesen ist. Diese Bibliothek ist in PHP geschrieben, entwickelt man in einer anderen Sprache, so benötigt man irgendwie eine Möglichkeit mit AES Verschlüsselung zu arbeiten.

phpseclib ist aus meiner Sicht für Verschlüsselung allgemein ein Tipp. Es stehen Klassen für die verschiedensten Algorithmen zur Verfügung (z.B. RSA, SSH, DES, 3DES, AES uvm.). Die Bibliothek ist OpenSource und steht unter der LGPL Lizenz.

Ablauf

Um die unten stehende Implementierung zu verstehen, muss man zuerst den gesamten Ablauf kennen. Dieser ist recht simpel. Zuerst muss der Stick einmalig vorbereitet werden. Der Authentifizierungsprozess kann dann beliebig oft durchgeführt werden.

Initialisierung

Der Stick muss mit dem AES Schlüssel und einem zufälligen Datenblock beschrieben werden. Dies funktioniert recht einfach unter Linux:

sudo ./stick-write -p AESKeyundDatenblock

Wobei AESKeyundDatenblock ein 46 Byte langer String ist, der zuerst aus dem AES Key (32 Bytes lang) und dann dem Datenblock (14 Bytes lang) besteht. Der AES Schlüssel und der Datenblock müssen auch dem Server bekannt sein. Zudem muss auf dem Server der Zähler auf 0 gesetzt werden (=das nächste Einmalpasswort ist das erste).

Authentifizierung

Client:

  1. Der Stick wird am Client eingesteckt und das Loginformular wird aufgerufen. Nun gibt der Benutzer seinen Benutzernamen ein, setzt den Cursor auf das Einmalpasswort-Feld und drückt auf den Taster am Stick.
  2. Der Stick erzeugt nun das Einmalpasswort: Ein interner Zählwert wird mit dem Datenblock verknüpft und mit dem angegebenen Schlüssel (key) AES verschlüsselt. Anschließend erhöht der Stick seinen internen Zähler um eins.
  3. Das so generierte Einmalpasswort wird base64 kodiert, so dass es durch gewöhnliche (ASCII) Zeichen darstellbar ist.
  4. Der Stick sendet das so generierte, base64 kodierte Einmalpasswort als Tasteneingabe und befüllt so das selektierte Feld. Ein Klick auf “Login” sendet das HTML Formular wie bei einem gewöhnlichen Login an den Server.

Server:

  1. Der Server nimmt die Anfrage entgegen und lädt seinen eigenen, zum Stick passenden AES Schlüssel und Datenblock.
  2. Dann wird der durch den Client übermittelte base64 String erst einmal korrigiert: ein z am Anfang kennzeichnet ob ein amerikanisches Tastaturlayout vorliegt (entsprechend müssen alle z durch y und alle y durch z ersetzt werden). Zudem werden einige Sonderzeichen ersetzt.
  3. Anschließend wird das noch verschlüsselte Einmalpasswort von base64 wieder in einen Bytevektor dekodiert.
  4. Nun wird das Einmalpasswort mit AES entschlüsselt.
  5. der Zählstand wird ausgelesen und der Datenblock entnommen.
  6. Der Login ist erfolgreich wenn der gegebene Zählstand größer dem Zählstand am Server ist und der gegebene Datenblock mit dem intern gespeicherten übereinstimmt.
  7. War der Login erfolgreich, so wird am Server der Zählstand des Sticks übernommen: es werden nur Einmalpasswörter akzeptiert, die einen höheren Zählstand haben (alte funktionieren also nicht mehr).

Auch wenn sich alles nun nach einer Menge Arbeit anhört, ist die Implementierung übersichtlich. Ich habe dazu in meiner Applikation einen Action Helper verwendet (in Zend Framework Applikationen eine Hilfsklasse, die in allen Controller zur Verfügung steht). An dieser Stelle eine PHP Funktion, welche nur von phpseclib abhängt und alle Schritte durchführt, die oben unter Server gelistet sind. Jeder Schritt wird nochmal als Kommentar erklärt.


/**
 * @param string $givenOtp Einmalpasswort, dass geprueft werden soll
 * @param string $aesKey der AES Schluessel und Datenteil
 * @param int $number aktueller Zaehlstand des Servers
 */
function otp($givenOtp, $aesKey, $number) {
	// AES Schluessel besteht aus
	// erste 32 Byte: AES Key
	// restliche 14 Byte: Datenblock
	$key  = substr($aesKey, 0, 32);
	$data = substr($aesKey, 32, 14);

	// base64 Kodierung des Sticks korrigieren
	$givenOtp = rtrim($givenOtp);

	// Sonderzeichen in korrekte Sonderzeichen umwandeln
	// (diese werden vom Stick anders vorgegeben, um von
	// unterschiedl. Tastaturlayouts unabhängig zu werden)
	for($i = 0; $i < strlen($givenOtp); $i++) {
		if($givenOtp[$i] == " ") { $givenOtp[$i] = "/"; }
		elseif($givenOtp[$i] == ".") { $givenOtp[$i] = "="; }
		elseif($givenOtp[$i] == "-") { $givenOtp[$i] = "+"; }
	}

	// erstes Zeichen pruefen ob z oder y
	// abhaengig davon alle y durch z ersetzen und umgekehrt
	$z = $givenOtp[0];
	$crypted = substr($givenOtp, 1);

	if($z == "y" or $z == "Y") {
		for($i = 0; $i < strlen($crypted); $i++) {
		  if	($crypted[$i] == 'y') { $crypted[$i] = "z"; }
		  elseif($crypted[$i] == 'Y') { $crypted[$i] = "Z"; }
		  elseif($crypted[$i] == 'z') { $crypted[$i] = "y"; }
		  elseif($crypted[$i] == 'Z') { $crypted[$i] = "Y"; }
		}
	}

	if($z == "Y" or $z == "Z") {
		for($i = 0; $i < strlen($crypted); $i++) {
			if(ctype_upper($crypted[$i])) {
				$crypted[$i] = strtolower($crypted[$i]);
			} else {
				$crypted[$i] = strtoupper($crypted[$i]);
			}
		}
	}

	// base64 String nun in binary dekodieren
	$crypted = base64_decode($crypted);

	// gegebenes One Time Passwort mit AES entschluesseln
	$aes = new Crypt_AES();
	$aes->disablePadding();
	$aes->setKey($key);
	$plain = $aes->decrypt($crypted);

	// entschluesseltes Passwort aufteilen in

	// aktueller Zaehlstand
	$i	 = substr($plain, 0,1);
	$j	 = substr($plain, 1,1);
	$n	 = ord($i) + (ord($j) << 8);

	// und Datenblock
	$plain = substr($plain, 2, strlen($plain)-2);

	// wenn Datenblock korrekt und Laufnummer nach aktueller Nummer:
	// aktuelle Nummer zurueckgeben (Login korrekt)
	if(($plain == $data) and ($n > $number)) {
		return $n;
	} else {
		return false;
	}
}

Natürlich kann das Entschlüsseln auch mit der mcrypt Erweiterung von PHP durchgeführt werden. So benötigt man keine zusätzliche Bibliothek wie phpseclib. Um das Passwort mit mcrypt zu entschlüsseln, muss der Codeteil


$aes = new Crypt_AES();
$aes->disablePadding();
$aes->setKey($key);
$plain = $aes->decrypt($crypted);

durch die entsprechenden mcrypt Aufrufe ersetzt werden:


$td = mcrypt_module_open("rijndael-128", "", "ecb", "");
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_DEV_RANDOM);
mcrypt_generic_init($td, $key, $iv);
$plain = mdecrypt_generic($td, $crypted);

Fazit

Ich bin mit der Lösung sehr zufrieden. Die AES Schlüssel/Datenblöcke lassen sich komfortabel in der Datenbank ablegen und auch leicht ändern, falls der Stick verloren geht. Besonders wenn man oft in unsicheren Netzen unterwegs ist (z.B. in der Firma oder an der Uni, in Internet Cafes usw.) ist die Lösung praktisch. Es müssen keine langen Einmalpasswörter von einem Display abgeschrieben werden und der Stick läuft gleichermaßen unter Linux und Windows. Die Integration in die eigene Applikation ist denkbar einfach.

Einziger Nachteil: der Stick hat keine Zeitkomponente, d.h. jemand könnte das Passwort abgreifen und dann die Netzverbindung kappen. Erst wenn sich der Besitzer einloggt, wird das zuvor abgepishte Passwort ungültig. Ebenso lassen sich mehrere Passwörter auslesen: so lange der Besitzer sich nicht einloggt, bleiben sie alle gültig.

6 Kommentare zu “Einmalpasswörter mit PHP und OpenKubus”

  1. harald

    sehr interessante sache … 25,- sind — denke ich — an der obergrenze, aber noch ok für den stick. eine zeitkomponente, wie du sie schon angesprochen hast, würde in der tat das ganze noch besser machen.

    ist jetzt nicht ganz das, was du mit dieser lösung hier erschlagen willst, aber — prinzipiell: was hältst du von SSL client zertifikaten? denkbare lösung hierfür: USB stick + portable firefox + SSL client cert und serverseitige SSL client cert authentication … ?

     
  2. Tobi

    Hi Harald,

    die 25 Euro find ich ok für eine Einzellösung. Für mehrere Leute müsste man da echt nochmal schauen, welche Lösungen es da noch gibt.

    Das Problem mit der Portable Firefox und Zertifikaten ist wieder der Einsatz an einem unsicheren Rechner, wo man nicht weiß ob dieser nicht manipuliert oder kompromittiert wurde. Dort könnte jemand das Zertifikat abgreifen, was mit dem Stick nicht geht. Der AES Schlüssel verlässt ja nie den Stick.

    Als zusätzliche Sicherheit ist so eine portable Firefox-Lösung aber interessant. Eine Idee ist es, einen portablen FF (am besten direkt als eine ausführbare Datei) zum Download anzubieten. Dieser wird dann ausgeführt und für alle weitere Zugriffe verwendet um etwa einen manipulierten Browser am aktuellen Rechner zu entgehen.

    Viele Grüße
    Tobi

     
  3. Cornelius

    Hi Tobi,
    ich auch eine Weil mit Einmalpassörtern (http://linotp.org) involviert, so dass mich allein die Nutzung des OpenKubus schon extrem interessiert. In Deiner Darstellung der Schritte beim Server ist aber ein Fehler, der die ganze Security recht fragwürdig erscheinen lässt.

    Folgendes Angriffsszenario:
    Dein OpenKubus liegt bei Dir auf dem Tisch. Ich schnappe ihn mir und tippere 10mal auf den Knopf. Einfach so – vielleicht in einem editor, egal. Der Counter wird im stick um 10 erhöht.
    Nun verschwinde ich wieder. Du loggst Dich mit dem Stick ein, drückst den Knopf und schickst quasi den Counter 11 zum Server. Der Server hat aber noch den Counter 1.

    Nun macht der Server:

    6. Der Login ist erfolgreich wenn der gegebene Zählstand größer dem Zählstand am Server ist und der gegebene Datenblock mit dem intern gespeicherten übereinstimmt.
    7. War der Login erfolgreich, so wird am Server der Zählstand um eins erhöht: es werden nur Einmalpasswörter akzeptiert, die einen höheren Zählstand haben (alte funktionieren also nicht mehr).

    Der Counter beim Server wird nur um 1 erhöht. Das ist das Problem. Ich habe nämlich auf der Leitung mitgelauscht. Nun kann ich das mitgeschnitte 9 mal verwenden, um mich zu authentisieren, weil ich nun 9 mal den Counter 11 schicken kann und jedesmal der Server das OK findet, weil er größer als sein eigener Counter ist.
    Der Counter im Server muss also auf den vom Client gesendeten Counter gesetzt werden.

    Bitte korrigier mich, wenn ich hier irgendwas übersehen habe…

    Schönen Gruß
    Cornelius

     
  4. Tobi

    Hallo Cornelius,

    vielen Dank für deine Anmerkung! Da hast du natürlich recht, hier war meine Ausführung falsch. Ich hab das gleich korrigiert. Natürlich muss der Zählstand des Sticks dann übernommen werden. Bin schon erschrocken und hab gleich meine Implementierung nochmal geprüft, aber das habe ich da auch richtig gemacht und merke mir dann den gegebenen Zählstand vom Stick.

    Viele Grüße und nochmal Danke!
    Tobi

     
  5. Daniel

    Hallo,

    wieso setzt Du in Zeile 14 $crypted auf $givenOtp um dann in Zeile 31 $crypted (bis dahin unbenutzt?) mit substr($givenOtp, 1) zu ersetzen?

    Hätte man sich da Zeile 14 nicht komplett sparen können?

    Klar, Du nimmst in 31 das erste Zeichen weg (den Z/Y Marker) aber vorher hast Du mit $crypted ja nichts mehr gemacht?

    Sorry, falls das ne doofe Frage ist – ich hab mir heute erst den Stick bestellt und wollte mich erstmal einlesen – und ich bin echt kein guter Programmierer…

     
  6. Tobi

    Hi Daniel,

    ja, du hast recht. Man bei dem Blogeintrag war ich echt nicht sorgfältig genug :/
    Da war natürlich ein Fehler, denn der rtrim ging ja auch ins leere. Habs korrigiert und hoffe, das war der letzte Fehler ;)

    Viele Grüße
    Tobi

     

Hinterlasse eine Antwort