commit 98ef5d8618aa5292f7bf9a5ec5c16097ada04ca8
parent a287a8b369e5dcf4c33d8e2fbfd63b2694375204
Author: Jan Dankert <devnull@localhost>
Date: Fri, 27 Oct 2017 01:10:25 +0200
Security-Erweiterung mit neuen Datenbank-Spalten: Längere Kennwort-Hashes (jetzt 255 statt 50 Zeichen), Vorbereitung für TOTP-Token.
Diffstat:
5 files changed, 228 insertions(+), 33 deletions(-)
diff --git a/db/update/DBVersion000004.class.php b/db/update/DBVersion000004.class.php
@@ -0,0 +1,26 @@
+<?php
+
+
+/**
+ * Add Columns for user language and user timezone.
+ *
+ * @author dankert
+ *
+ */
+class DBVersion000004 extends DbVersion
+{
+ public function update()
+ {
+ $not_nullable = false;
+ $nullable = true;
+
+ // Add user language
+ $this->addColumn('user','language','VARCHAR', 2,null,$nullable);
+
+ // Add user timezone
+ $this->addColumn('user','timezone','VARCHAR',64,null,$nullable);
+
+ }
+}
+
+?>+
\ No newline at end of file
diff --git a/db/update/DBVersion000005.class.php b/db/update/DBVersion000005.class.php
@@ -0,0 +1,33 @@
+<?php
+
+
+/**
+ * Security enhancements.
+ *
+ * @author dankert
+ *
+ */
+class DBVersion000005 extends DbVersion
+{
+ public function update()
+ {
+ $not_nullable = false;
+ $nullable = true;
+
+ // longer Passwords! 50 is not enough.
+ $this->addColumn('user','password_hash','VARCHAR',255,null,$not_nullable);
+
+ $db = $this->getDb();
+ $table = $this->getTableName('user');
+ $updateStmt = $db->sql('UPDATE '.$table.
+ ' SET password_hash=password'
+ );
+ $updateStmt->query();
+
+ $this->dropColumn('user','password');
+
+ $this->addColumn('user','password_salt','VARCHAR',255,null,$not_nullable);
+ }
+}
+
+?>+
\ No newline at end of file
diff --git a/db/update/DBVersion000006.class.php b/db/update/DBVersion000006.class.php
@@ -0,0 +1,37 @@
+<?php
+
+
+/**
+ * Security enhancements.
+ *
+ * @author dankert
+ *
+ */
+class DBVersion000006 extends DbVersion
+{
+ public function update()
+ {
+ $not_nullable = false;
+ $nullable = true;
+
+ $this->addColumn('user','password_expires','INT',0,null,$nullable);
+
+ $this->addColumn('user','last_login' ,'INT',0,null,$nullable);
+
+ $this->addColumn('user','password_algo' ,'INT',0,2,$not_nullable);
+
+ // Setting Password algo. Passwords beginning with '$' are (old) MD5-hashes.
+
+ // SUBSTR(s,pos,length) is supported by MySql,Postgres,SQLite
+ // SUBSTRING(s FROM pos FOR length) is NOT supported by SQLite
+ $table = $this->getTableName('user');
+ $db = $this->getDb();
+ $updateAlgoStmt = $db->sql('UPDATE '.$table.
+ ' SET password_algo=1 WHERE SUBSTR(password_hash,1,1) = '."'$'".';'
+ );
+ $updateAlgoStmt->query();
+
+ }
+}
+
+?>+
\ No newline at end of file
diff --git a/db/update/DBVersion000007.class.php b/db/update/DBVersion000007.class.php
@@ -0,0 +1,38 @@
+<?php
+
+
+/**
+ * Security enhancements.
+ *
+ * @author dankert
+ *
+ */
+class DBVersion000007 extends DbVersion
+{
+ public function update()
+ {
+ $not_nullable = false;
+ $nullable = true;
+
+ $this->addColumn('user','otp_secret' ,'VARCHAR',255,null,$nullable );
+
+ $table = $this->getTableName('user');
+ $db = $this->getDb();
+ $stmt = $db->sql('SELECT id FROM '.$table);
+ foreach( $stmt->getCol() as $userid )
+ {
+ $secret = Password::randomHexString(64);
+ $stmt = $db->sql('UPDATE '.$table.' SET otp_secret={secret} WHERE id={id}');
+ $stmt->setString('secret',$secret);
+ $stmt->setInt('id',$userid);
+ $stmt->query();
+ }
+
+ $this->addColumn('user','totp' ,'INT' , 1, 0,$not_nullable);
+ $this->addColumn('user','hotp_counter','INT' , 0, 0,$not_nullable);
+ $this->addColumn('user','hotp' ,'INT' , 1, 0,$not_nullable);
+
+ }
+}
+
+?>+
\ No newline at end of file
diff --git a/util/Password.class.php b/util/Password.class.php
@@ -1,5 +1,9 @@
<?php
+define('OR_PASSWORD_ALGO_PLAIN',0);
+define('OR_PASSWORD_ALGO_CRYPT',1);
+define('OR_PASSWORD_ALGO_MD5' ,2);
+
/**
* Hashfunktion für Passwörter.
@@ -13,34 +17,59 @@
class Password
{
/**
- * Hashen eines Kennwortes mit Bcrypt (bzw. MD5).
- * @param $password
- * @param $cost Kostenfaktor: Eine Ganzzahl von 4 bis 31.
+ * Ermittelt den bestverfügbarsten hash-Algorhytmus.
*/
- static public function hash( $password,$cost=10 )
+ static public function bestAlgoAvailable()
{
if ( function_exists('crypt') && defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1 )
{
- $chars = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- $salt = '';
- for( $i = 1; $i <= 22; $i++ )
- $salt .= $chars[ rand(0,63) ];
-
- //
- if ( version_compare(PHP_VERSION, '5.3.7') >= 0 )
- $algo = '2y';
- else
- $algo = '2a';
-
- // Kostenfaktor muss zwischen '04' und '31' (jeweils einschließlich) liegen.
- $cost = max(min($cost,31),4);
- $cost = str_pad($cost, 2, '0', STR_PAD_LEFT);
-
- return crypt($password,'$'.$algo.'$'.$cost.'$'.$salt.'$');
+ return OR_PASSWORD_ALGO_CRYPT;
+ }
+ elseif ( function_exists('md5') )
+ {
+ return OR_PASSWORD_ALGO_MD5;
}
else
{
- return md5($password);
+ return OR_PASSWORD_ALGO_PLAIN;
+ }
+ }
+
+
+ /**
+ * Hashen eines Kennwortes mit Bcrypt (bzw. MD5).
+ * @param $password
+ * @param $algo Algo
+ * @param $cost Kostenfaktor: Eine Ganzzahl von 4 bis 31.
+ */
+ static public function hash( $password,$algo,$cost=10 )
+ {
+ switch( $algo )
+ {
+ case OR_PASSWORD_ALGO_CRYPT:
+
+ $chars = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ $salt = '';
+ for( $i = 1; $i <= 22; $i++ )
+ $salt .= $chars[ rand(0,63) ];
+
+ //
+ if ( version_compare(PHP_VERSION, '5.3.7') >= 0 )
+ $algo = '2y';
+ else
+ $algo = '2a';
+
+ // Kostenfaktor muss zwischen '04' und '31' (jeweils einschließlich) liegen.
+ $cost = max(min($cost,31),4);
+ $cost = str_pad($cost, 2, '0', STR_PAD_LEFT);
+
+ return crypt($password,'$'.$algo.'$'.$cost.'$'.$salt.'$');
+
+ case OR_PASSWORD_ALGO_MD5:
+ return md5($password); // ooold.
+
+ case OR_PASSWORD_ALGO_PLAIN:
+ return $password; // you want it, you get it.
}
}
@@ -51,24 +80,52 @@ class Password
* @param String $hash Hash
* @return boolean true, falls das Passwort dem Hashwert entspricht.
*/
- static public function check( $password,$hash )
+ static public function check( $password,$hash,$algo )
{
- if ( substr($hash,0,1) != '$' )
- // Wenn kein '$' voransteht, dann handelt es sich wohl um
- // einen alten MD5-Hash
- return $hash == md5($password);
+ switch( $algo )
+ {
+ case OR_PASSWORD_ALGO_MD5:
+ return $hash == md5($password);
+
+ case OR_PASSWORD_ALGO_CRYPT:
- elseif ( function_exists('crypt') )
+ if ( function_exists('crypt') )
+ {
+ // Workaround: Die Spalte 'password' ist z.Zt. nur 50 Stellen lang, daher
+ // wird der mit crypt() erzeugte Hash auf die Länge des gespeicherten Hashes
+ // gekürzt. Falls die Spalte später länger ist, wirkt automatisch die volle
+ // Hash-Länge.
+ return $hash == substr(crypt($password,$hash),0,strlen($hash));
+ }
+ else
+ {
+ throw new Exception("Modular crypt format is not supported by this PHP version (no function 'crypt()')");
+ }
+
+ case OR_PASSWORD_ALGO_PLAIN:
+ return $hash == $password;
+ }
+ }
+
+ static public function randomHexString( $bytesCount )
+ {
+ if ( function_exists('random_bytes') )
+ {
+ return bin2hex( random_bytes($bytesCount) );
+ }
+ elseif ( function_exists('openssl_random_pseudo_bytes') )
{
- // Workaround: Die Spalte 'password' ist z.Zt. nur 50 Stellen lang, daher
- // wird der mit crypt() erzeugte Hash auf die Länge des gespeicherten Hashes
- // gekürzt. Falls die Spalte später länger ist, wirkt automatisch die volle
- // Hash-Länge.
- return $hash == substr(crypt($password,$hash),0,strlen($hash));
+ return bin2hex( openssl_random_pseudo_bytes($bytesCount) );
}
else
{
- throw new Exception("Modular crypt format is not supported by this PHP version (no function 'crypt()')");
+ // This fallback is NOT cryptographic safe!
+ $buf = '';
+ for ($i = 0; $i < $length; ++$i)
+ {
+ $buf .= chr(mt_rand(0, 255));
+ }
+ return bin2hex($buf);
}
}
}