© buchachon - Fotolia.com

Sicherer Web-Login mit bCrypt und Javascript

User verwenden auf verschiedenen Webseiten häufig gleiche und meist auch schwache Passwörter. Die wenigsten Nutzer im Internet sind technisch so versiert, ausreichende sichere Passwörter generieren und nutzen zu können. Vielfach fehlt ein grundlegendes Basiswissen aus dem Bereich der IT-Sicherheit.

Dies ist aus mehreren Grunde problematisch, denn es kann nicht sichergestellt werden,

  • dass der Anbieter Passwörter angemessen schützt,
  • Passwörter durch den Anbieter nicht weitergegeben werden, oder
  • das Passwörter während der Übertragung an den Webserver nicht mitgeschnitten werden, hierbei ist auch https keine Garantie.

Um die Daten des Users bestmöglich zu schützen, sind an dieser Stelle in erster Linie die Anbieter und Entwickler in der Pflicht. Das Verhalten der Benutzer wird sich auf absehbare Zeit nicht ändern.

Clientseitige Implementierung

Eine Möglichkeit, dass Problem zu mindern, besteht darin, Passwörter zu keiner Zeit unverschlüsselt zu übertragen. Einwegfunktionen zur Sicherung von Passwörtern sollten bereits im Browser des Clients ausgeführt werden.

--> Das komplette Code-Beispiel findet sich auf Github!

Der heutigen Beitrag zeigt eine Beispielimplementierung auf Basis von javascript-bcrypt. Passwörter werden als salted bCrypt-Hash gespeichert.

Das Beispiel ist lediglich ein Ansatz und an vielen Stellen noch nicht ausgereift. Fehlerzustände werden z.B. teilweise noch nicht korrekt behandelt oder Brute-Force Angriffe noch nicht migriert. Das Skript stellt lediglich eine Arbeitsgrundlage zur Weiterentwicklung dar.

Passwort setzen/ ändern

Das Passwort wird mit bCrypt und einem zufälligen Salt verarbeitet und anschließend an den Webserver übertragen. Der Webserver speichert den Hash zusammen mit dem Benutzernamen. Der Anbieter hat zu keiner Zeit das Klartextpasswort erhalten.

Zusammenfassung:

  • Hash erstellen
  • Hash an Server übertragen

Passwort verifizieren

Auch bei der Verifikation darf das Passwort nicht im Klartext übermittelt werden. Hierbei ergibt sich jedoch die Schwierigkeit, dass der Salt für jeden Nutzer unterschiedlich ist und somit das Passwort nicht einfach erneut als Hash übertragen werden kann. Aus diesem Grund muss beim Login der Salt des Users an die Login-Form übertragen werden.

Zusammenfassung:

  • Username eingeben
  • Salt vom Server für Usernamen abholen
  • Passwort eingeben
  • Hash erstellen
  • Hash an Server übertragen, dort Abgleich

Implementierung: Account erstellen

Auf Seite des Clients ist folgender Code notwendig:

<html>
 
<meta http-equiv="Content-Type" content="text/html;charset=windows-1252" >
 
<head>
 
    <script src="bCrypt.js" type="text/javascript"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js" type="text/javascript"></script>
 
    <script type="text/javascript">
 
    function result(hash){    
        $("#password").val(hash);
        $("#pwdForm").submit();
    }
 
    function crypt()
    {
 
        if($("#password").val().length < 10)
        {
                alert('Password zu kurz, Mindestens 10 Zeichen');
                return;
        }
     
        // Weitere Checks ausführen
     
        var salt;
        try
        {
                salt = gensalt(13);
        }
        catch(err) 
        {
                alert(err);
                return;
        }
     
        try
        {
            hashpw($("#password").val(), salt, result);
        } 
        catch(err) 
        {
                alert(err);
                return;
        }
 
    }
 
    </script>
 
</head>
<body>

	<h1>Neuen Account erstellen</h1>
	<br />	
 
    	<form id='pwdForm' action='./create-account.php' method='post'>
        	<label for="username">Username: </label><input size=30 type="text" name="username" id="username"></input>
        	<br />
        	<label for="password">Password: </label><input size=30 type="password" name="password" id="password"></input>
		<br />	<br />	
        	<INPUT TYPE="button" value="Submit" onClick="crypt()"/>
    	</form>
 
</body>
</html>

Auf Seite des Servers, im Beispiel von PHP:

<?php

if(isset($_POST['username']) && isset($_POST['password'])) 
{ 
	/* An dieser Stelle Gültigkeit der übergebenen Strings prüfen */ 

	$dbconnect = new mysqli('host', 'username', 'password', 'database');

	$stmt = $dbconnect->prepare("SELECT id FROM user WHERE username=?");
	$stmt->bind_param("s", $_POST['username']);
	$stmt->execute();

	$result = $stmt->get_result();

	if ($result->num_rows > 0) 
	{ 

		echo "User konnte nicht angelegt werden, Sie werden weitergeleitet...";

		echo '<meta http-equiv="refresh" content="2; url=./register.html">';

	}
	else
	{
		if (trim($_POST['username']) != '') 
		{
			$stmt = $dbconnect->prepare("INSERT INTO user (username, password) VALUES (?, ?)");
			$stmt->bind_param("ss", $_POST['username'], $_POST['password']);
			$stmt->execute();

			echo "User erfolgreich angelegt, Sie werden weitergeleitet...";
	
			echo '<meta http-equiv="refresh" content="2; url=./login.html">';
		}
		else
		{

			echo "User konnte nicht angelegt werden, Sie werden weitergeleitet...";

			echo '<meta http-equiv="refresh" content="2; url=./register.html">';

		}

	}

}

?>

Ein Blick in die HTTP Anfrage an den Server zeigt folgendes:

HTTP Post Anfrage

Das Password wird nicht im Klartext an den Server übertragen.

Implementierung: Passwort verifizieren

Die Verifikation des Passworts ist etwas umfangreicher, da während des Loginvorgangs der Salt des Users geholt werden muss. Clientseitig könnte eine Implementierung wie folgt aussehen:

<html>

<meta http-equiv="Content-Type" content="text/html;charset=windows-1252" >

<head>

	<script src="bCrypt.js" type="text/javascript"></script>
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js" type="text/javascript"></script>

	<script type="text/javascript">

	var salt = "";

	function result(hash){	  
		$("#password").val(hash);
	  	$("#pwdForm").submit();
	}

	function getSalt()
	{

		$salt = "";

		ajax = new XMLHttpRequest();
    		if(ajax!=null)
		{
			ajax.open("POST","./salt.php",true);
			ajax.onreadystatechange = function()
			{
				if(this.readyState == 4)
				{
					if(this.status == 200)
					{
						if(this.responseText != '')
						{
							$salt = this.responseText;	
							login();
						}
					}
				}
			
			}
			var postdata= 'username='+$("#username").val();

			ajax.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
			ajax.setRequestHeader("Content-length", postdata.length);

			ajax.send(postdata);
		}
    		else
		{
			alert("Ihr Browser unterstützt kein Ajax!");
    		}
	}

	function login()
	{
	
		// Weitere Checks ausführen
	
		try
		{
			hashpw($("#password").val(), $salt, result);
		} 
		catch(err) 
		{
	    		alert(err);
	    		return;
		}

	}

	</script>

</head>
<body>
	
	<h1>Login</h1>
	<br />	

	<form id='pwdForm' action='./do-login.php' method='post'>
		<label for="username">Username: </label><input size=30 type="text" name="username" id="username"></input>
		<br />
		<label for="password">Password: </label><input size=30 type="password" name="password" id="password"></input>
		<br />	<br />		
		<INPUT TYPE="button" value="Login" onClick="getSalt()"/>
	</form>

	<br />	
	<br />	
<a href="./register.html">Registrieren</a>


</body>
</html>

Serverseitig sind 2 Dateien notwendig, einmal zur Rückgabe des Salts:

<?php

if (isset($_POST['username'])) 
{ 
	/* An dieser Stelle Gültigkeit der übergebenen Strings prüfen */ 

	$dbconnect = new mysqli('host', 'username', 'password', 'database');

	$stmt = $dbconnect->prepare("DELETE FROM tmp_salts WHERE timestamp < (NOW() - INTERVAL 1 DAY)");
	$stmt->execute();

	$stmt = $dbconnect->prepare("SELECT password FROM user WHERE username=?");
	$stmt->bind_param("s", $_POST['username']);
	$stmt->execute();

	$result = $stmt->get_result();

	if ($result->num_rows == 1) 
	{ 

		$row = mysqli_fetch_assoc($result);
		echo substr($row['password'], 0, 29);
	}
	else
	{

		$stmt = $dbconnect->prepare("SELECT salt FROM tmp_salts WHERE username=?");
		$stmt->bind_param("s", $_POST['username']);
		$stmt->execute();

		$result = $stmt->get_result();

		if ($result->num_rows == 1) 
		{ 

			$row = mysqli_fetch_assoc($result);
			echo $row['salt'];
		}
		else
		{
	   		$options = [
  				'cost' => 13,
			];
			$base = password_hash ('', PASSWORD_BCRYPT, $options);
    			$salt = "$2a$13$" . substr($base, 8, 30);

			$stmt = $dbconnect->prepare("INSERT INTO tmp_salts (username, salt) VALUES (?, ?)");
			$stmt->bind_param("ss", $_POST['username'], $salt);
			$stmt->execute();

			echo $salt;

		}


	}

}

?>

Zum anderen für den eigentlichen Login:

<?php

if ((isset($_POST['username'])) && (isset($_POST['password'])))
{ 

	session_start();

	/* An dieser Stelle Gültigkeit der übergebenen Strings prüfen */ 

	if (trim($_POST['username']) == '') 
	{
		echo "Login fehlgeschlagen, Sie werden weitergeleitet..!";
		session_destroy();
		echo '<meta http-equiv="refresh" content="2; url=./login.html">';
		exit;

	}

	$dbconnect = new mysqli('host', 'username', 'password', 'database');

	$stmt = $dbconnect->prepare("SELECT id FROM user WHERE username=? AND password=?");
	$stmt->bind_param("ss", $_POST['username'], $_POST['password']);
	$stmt->execute();

	$result = $stmt->get_result();

	if ($result->num_rows == 1) 
	{ 

		echo "Login erfolgreich,  Sie werden weitergeleitet...!";
		$_SESSION['username'] = $_POST['username'];
		echo '<meta http-equiv="refresh" content="2; url=./index.php">';

	}
	else
	{

		echo "Login fehlgeschlagen, Sie werden weitergeleitet...!";
		session_destroy();
		echo '<meta http-equiv="refresh" content="2; url=./login.html">';

	}

}
else
{
	echo "Login fehlgeschlagen, Sie werden weitergeleitet...!";
	session_destroy();
	echo '<meta http-equiv="refresh" content="2; url=./login.html">';
}

?>

Das Skript zur Rückgabe des Salts speichert die generierten Zufalls-Salts für nicht existierende Nutzer, um über das Login-Skript keinen Rückschluss darauf zu ermöglichen, ob ein Username vorhanden ist oder nicht. Die Clean-up Zeitspanne muss an eigene Bedürfnisse angepasst werden.

Fazit

Bereits mit wenigen Zeilen Code ist es möglich, die Passwörter der User deutlich besser zu schützen. Dies stellt die Integrität des Passworts auch dann sicher, wenn der Server des Anbieters beispielsweise kompromittiert wurde. Das Skript lässt sich beliebig härten und erweitern, denkbar wäre etwa eine Erweiterung um ein Challenge-Response-Verfahren.

Das Skript wird von mir weiter entwickelt, so dass es in Zukunft gerne auch produktiv von jedem genutzt werden kann.


Bildnachweise:

  • Beitragsbild: © buchachon - Fotolia.com

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert