openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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:
db/update/DBVersion000004.class.php | 27+++++++++++++++++++++++++++
db/update/DBVersion000005.class.php | 34++++++++++++++++++++++++++++++++++
db/update/DBVersion000006.class.php | 38++++++++++++++++++++++++++++++++++++++
db/update/DBVersion000007.class.php | 39+++++++++++++++++++++++++++++++++++++++
util/Password.class.php | 123++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
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); } } }