Skip to content

Commit

Permalink
Implement new password hashing (PBKDF2-SHA1)
Browse files Browse the repository at this point in the history
This change modifies the database and requires a database upgrade
from within the lansuite admin area.

After the database upgrade, the old MD5 user passwords will be
automatically converted to PBKDF2-SHA1 on the next user login.
Clan and team passwords will only be upgraded, when new passwords
are set.

PBKDF2-SHA1 was choosen for compatibility with the ejabberd XMPP
server password storage.
  • Loading branch information
lxp committed Jul 17, 2017
1 parent d1955a8 commit 05cdfc5
Show file tree
Hide file tree
Showing 16 changed files with 253 additions and 25 deletions.
12 changes: 11 additions & 1 deletion inc/classes/class_auth.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

require_once("inc/classes/class.crypt.php");
require_once("inc/classes/class_pwhash.php");


/**
Expand Down Expand Up @@ -202,7 +203,7 @@ public function login($email, $password, $show_confirmation = 1)
$func->information(t('Du hast deine Email-Adresse (%1) noch nicht verifiziert. Bitte folge dem Link in der dir zugestellten Email.', $user['email']).' <a href="index.php?mod=usrmgr&action=verify_email&step=2&userid='. $user['userid'] .'">'. t('Klicke hier, um die Mail erneut zu versenden</a>'), '', 1);
$func->log_event(t('Login fehlgeschlagen. Email (%1) nicht verifiziert', $user['email']), "2", "Authentifikation");
// User login and wrong password?
} elseif ($user["user_login"] and $tmp_login_pass != $user["password"]) {
} elseif ($user["user_login"] and !PasswordHash::verify($password, $user["password"])) {
($cfg["sys_internet"])? $remindtext = t('Hast du dein Passwort vergessen?<br/><a href="./index.php?mod=usrmgr&action=pwrecover"/>Hier kannst du ein neues Passwort generieren</a>.') : $remindtext = t('Solltest du dein Passwort vergessen haben, wende dich bitte an die Organisation.');
$func->information(t('Die von dir eingebenen Login-Daten sind fehlerhaft. Bitte überprüfe deine Eingaben.') . HTML_NEWLINE . HTML_NEWLINE . $remindtext, '', 1);
$func->log_event(t('Login für %1 fehlgeschlagen (Passwort-Fehler).', $tmp_login_email), "2", "Authentifikation");
Expand All @@ -224,6 +225,15 @@ public function login($email, $password, $show_confirmation = 1)
$func->log_event(t('Login für %1 fehlgeschlagen (Account ausgecheckt).', $tmp_login_email), "2", "Authentifikation");
// Everything fine!
} else {
if ($user["user_login"] and PasswordHash::needsRehash($user["password"])) {
try {
$db->qry('UPDATE %prefix%user SET password = %string% WHERE userid = %int%', PasswordHash::hash($password), $user["userid"]);
$func->information(t('Es wurde ein Sicherheitsupgrade von deinem Passwort durchgeführt.'), '', 1);
} catch (Exception $e) {
$func->error(t('Sicherheitsupgrade von deinem Passwort ist fehlgeschlagen!'));
}
}

// Set Logonstats
$db->qry('UPDATE %prefix%user SET logins = logins + 1, changedate = changedate, lastlogin = NOW() WHERE userid = %int%', $user['userid']);

Expand Down
4 changes: 3 additions & 1 deletion inc/classes/class_masterform.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

require_once("inc/classes/class_pwhash.php");

define('FIELD_OPTIONAL', 1);
define('HTML_ALLOWED', 1);
define('LSCODE_ALLOWED', 1);
Expand Down Expand Up @@ -779,7 +781,7 @@ public function SendForm($BaseURL, $table, $idname = '', $id = 0)
// Convert Passwords
if ($field['type'] == IS_NEW_PASSWORD and $_POST[$field['name']] != '') {
$_POST[$field['name'] .'_original'] = $_POST[$field['name']];
$_POST[$field['name']] = md5($_POST[$field['name']]);
$_POST[$field['name']] = PasswordHash::hash($_POST[$field['name']]);
}
}
}
Expand Down
169 changes: 169 additions & 0 deletions inc/classes/class_pwhash.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

class PasswordHash
{
public static function hash($password)
{
$algo_cfg = self::getAlgoCfg();

switch ($algo_cfg['algo']) {
case 'md5':
return md5($password);

case 'pbkdf2-sha1':
/*
* Hash format: $pbkdf2-sha1$(iterations)$(salt)$(hash)
* Example: $pbkdf2-sha1$500000$o2ermOW/WQy1XFFDVfx/Zw==$otf1NOkfKFTrIh9Au1oTPdwdnTc=
* Parameters:
* - iterations: integer
* - salt: base64-encoded salt
* - hash: base64-encoded hash
*/

$iterations = $algo_cfg['iterations'];
if (!is_numeric($iterations) || $iterations < 1) {
throw new Exception('Unexpected iterations value');
}
$iterations = intval($iterations);

$rawsalt = random_bytes(16);
$rawhash = hash_pbkdf2('sha1', $password, $rawsalt, $iterations, 0, true);

$b64salt = base64_encode($rawsalt);
$b64hash = base64_encode($rawhash);
if ($b64salt === false || $b64hash === false) {
throw new Exception('Unexpected base64_encode error');
}

return '$pbkdf2-sha1$'.$iterations.'$'.$b64salt.'$'.$b64hash;
}

throw new Exception('Unsupported password hashing algorithm');
}

public static function verify($password, $hash)
{
$info = self::getInfo($hash);

switch ($info['algo']) {
case 'md5':
$newhash = md5($password);
return hash_equals($info['hash'], $newhash);
case 'pbkdf2-sha1':
$newhash = hash_pbkdf2('sha1', $password, $info['salt'], intval($info['iterations']), 0, true);
return hash_equals($info['hash'], $newhash);
}

return false;
}

public static function needsRehash($hash)
{
global $config;

$algo_cfg = self::getAlgoCfg();

$info = self::getInfo($hash);

foreach ($info as $key => $value) {
if ($key === 'hash' || $key === 'salt') {
continue;
}

if ($algo_cfg[$key] !== $value) {
return true;
}
}

return false;
}

private static function getInfo($hash)
{
if ($hash[0] === '$') {
$parts = explode('$', $hash);
if (count($parts) !== 5) {
throw new Exception('Unexpected hash format');
}
list($dummy, $algo, $iterations, $b64salt, $b64hash) = $parts;

if (!is_numeric($iterations) || $iterations < 1) {
throw new Exception('Unexpected iterations value');
}
$rawsalt = base64_decode($b64salt, true);
$rawhash = base64_decode($b64hash, true);
if ($rawsalt === false || $rawhash === false) {
throw new Exception('Unexpected base64_decode error');
}

return array('algo' => $algo, 'iterations' => $iterations, 'hash' => $rawhash, 'salt' => $rawsalt);
} else {
return array('algo' => 'md5', 'hash' => $hash);
}
}

private static function getAlgo()
{
global $cfg;

if (array_key_exists('pwhash_algo', $cfg)) {
$pwhash_algo = $cfg['pwhash_algo'];
} else {
$pwhash_algo = 'md5';
}

if ($pwhash_algo === 'default') {
$pwhash_algo = 'pbkdf2-sha1';
}

return $pwhash_algo;
}

private static function getDefaultAlgoCfg($algo)
{
switch ($algo) {
case 'pbkdf2-sha1':
return array('iterations' => '500000');
}

return array();
}

private static function parseAlgoCfg($algo_cfg_str)
{
$algo_cfg = array();

$pairs = explode(',', $algo_cfg_str);

foreach ($pairs as $pair) {
$parts = explode('=', $pair, 2);

$key = $parts[0];

if (count($parts) > 1) {
$value = $parts[1];
} else {
$value = null;
}

$algo_cfg[$key] = $value;
}

return $algo_cfg;
}

private static function getAlgoCfg()
{
global $cfg;

$algo = self::getAlgo();
$algo_cfg = self::getDefaultAlgoCfg($algo);

if (array_key_exists('pwhash_algo_cfg', $cfg)) {
$custom_algo_cfg = self::parseAlgoCfg($cfg['pwhash_algo_cfg']);
$algo_cfg = array_merge($algo_cfg, $custom_algo_cfg);
}

return array_merge($algo_cfg, array('algo' => $algo));
}
}
8 changes: 4 additions & 4 deletions modules/clanmgr/clanmgr.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

require_once("inc/classes/class_pwhash.php");

function ShowRole($role)
{
global $auth, $line;
Expand All @@ -22,10 +24,8 @@ function CheckClanPW($clanpw)
global $db, $auth;

$clan = $db->qry_first("SELECT password FROM %prefix%clan WHERE clanid = %int%", $_GET['clanid']);
if ($clan['password'] and $clan['password'] == md5($clanpw)) {
return true;
}
return false;

return $clan['password'] and PasswordHash::verify($clanpw, $clan['password']);
}

function CheckExistingClan()
Expand Down
8 changes: 4 additions & 4 deletions modules/clanmgr/class_clan.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

require_once("inc/classes/class_pwhash.php");

class Clan
{

Expand Down Expand Up @@ -60,9 +62,7 @@ public function CheckClanPW($clanid, $clanpw)
global $db, $auth;

$clan = $db->qry_first("SELECT password FROM %prefix%clan WHERE clanid = %int%", $clanid);
if ($clan['password'] and $clan['password'] == md5($clanpw)) {
return true;
}
return false;

return $clan['password'] and PasswordHash::verify($clanpw, $clan['password']);
}
}
2 changes: 1 addition & 1 deletion modules/clanmgr/mod_settings/db.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</field>
<field>
<name>password</name>
<type>char(32)</type>
<type>varchar(255)</type>
<null></null>
<key></key>
<default></default>
Expand Down
5 changes: 4 additions & 1 deletion modules/install/adminaccount.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<?php

require_once("inc/classes/class_pwhash.php");

$db->connect();

$dsp->NewContent(t('Adminaccount anlegen'), t('Lege hier einen Adminaccount an, über welchen du Zugriff auf diese Admin-Seite erhälst. Wenn du bereits Benutzer-Daten importiert hast musst du hier keinen weiteren Account anlegen.'));
Expand All @@ -23,7 +26,7 @@
email=%string%,
password = %string%,
type = '3'
", $_POST["email"], md5($_POST["password"]));
", $_POST["email"], PasswordHash::hash($_POST["password"]));
$userid = $db->insert_id();
// Admin zur Party hinzufügen
$party->add_user_to_party($userid, 1, "1", "1");
Expand Down
31 changes: 31 additions & 0 deletions modules/install/mod_settings/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,25 @@
</entry>
</entries>
</typedefinition>
<typedefinition>
<head>
<name>passwordhashalgo</name>
</head>
<entries>
<entry>
<value>default</value>
<description>Standard</description>
</entry>
<entry>
<value>pbkdf2-sha1</value>
<description>PBKDF2-SHA1 (kompatibel mit ejabberd)</description>
</entry>
<entry>
<value>md5</value>
<description>MD5 (Legacy, DANGEROUS!)</description>
</entry>
</entries>
</typedefinition>
<group>
<head>
<name>Allgemein</name>
Expand Down Expand Up @@ -257,6 +276,18 @@ eintagsmail.de
nospamfor.us</default>
<description>Email-Domain-Blacklist (Um Wegwerf-Mailadressen zu vermeiden)</description>
</item>
<item>
<name>pwhash_algo</name>
<type>passwordhashalgo</type>
<default>default</default>
<description>Passwort Hashing Algorithmus</description>
</item>
<item>
<name>pwhash_algo_cfg</name>
<type>string</type>
<default></default>
<description>Parameter für Passwort Hashing Algorithmus</description>
</item>
</items>
</group>
<group>
Expand Down
6 changes: 4 additions & 2 deletions modules/install/wizard.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

require_once("inc/classes/class_pwhash.php");

if ($_POST["resetdb"]) {
$db->success = 0;
}
Expand All @@ -25,15 +27,15 @@
if ($row['email']) {
$db->qry(
"UPDATE %prefix%user SET password = %string%, type = '3' WHERE email=%string%",
md5($_POST["password"]),
PasswordHash::hash($_POST["password"]),
$_POST["email"]
);
} // If not found, insert
else {
$db->qry(
"INSERT INTO %prefix%user SET username = 'ADMIN', firstname = 'ADMIN', name = 'ADMIN', email=%string%, password = %string%, type = '3'",
$_POST["email"],
md5($_POST["password"])
PasswordHash::hash($_POST["password"])
);
$userid = $db->insert_id();
}
Expand Down
8 changes: 5 additions & 3 deletions modules/tournament2/class_team.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

require_once("inc/classes/class_pwhash.php");

include_once("modules/seating/class_seat.php");
$seat2 = new seat2();

Expand Down Expand Up @@ -158,7 +160,7 @@ public function join($teamid, $userid, $password = null)
", $teamid);

// Check password, if set and if acction is not performed, by teamadmin or ls-admin
if (($auth['userid'] != $team['leaderid']) and ($auth['type'] <= 1) and ($team['password'] != '') and (md5($password) != $team['password'])) {
if (($auth['userid'] != $team['leaderid']) and ($auth['type'] <= 1) and ($team['password'] != '') and !PasswordHash::verify($password, $team['password'])) {
$func->information(t('Das eingegebene Kennwort ist nicht korrekt'));
return false;

Expand Down Expand Up @@ -244,7 +246,7 @@ public function create($tournamentid, $leaderid, $name = null, $password = null,
comment = %string%,
banner = %string%,
password = %string%
", $tournamentid, $name, $leaderid, $comment, $_FILES[$banner]["name"], md5($password));
", $tournamentid, $name, $leaderid, $comment, $_FILES[$banner]["name"], PasswordHash::hash($password));

$func->log_event(t('Der Benutzer %1 hat sich zum Turnier %2 angemeldet', $auth["username"], $t["name"]), 1, t('Turnier Teamverwaltung'));
}
Expand Down Expand Up @@ -278,7 +280,7 @@ public function edit($teamid, $name = null, $password = null, $comment = null, $

// Set Password
if ($password != "") {
$db->qry("UPDATE %prefix%t2_teams SET password = %string% WHERE teamid = %int%", md5($password), $_GET["teamid"]);
$db->qry("UPDATE %prefix%t2_teams SET password = %string% WHERE teamid = %int%", PasswordHash::hash($password), $_GET["teamid"]);
}

$db->qry("UPDATE %prefix%t2_teams
Expand Down

0 comments on commit 05cdfc5

Please sign in to comment.