File modules/security/Password.class.php

Last commit: Fri Apr 15 20:45:19 2022 +0200	dankert	Refactoring: Code cleanup.
1 <?php 2 namespace security; 3 4 5 6 use cms\base\Configuration; 7 8 /** 9 * Security functions for passwords. 10 * 11 * @author Jan dankert 12 * 13 */ 14 class Password 15 { 16 /** 17 * yes, we are supporting PLAIN passwords. Why? 18 * Normally, there are not used, but in developing situations this is useful. 19 */ 20 const ALGO_PLAIN = 0; 21 const ALGO_CRYPT = 1; 22 const ALGO_MD5 = 2; 23 const ALGO_PHP_PASSWORD_HASH = 3; 24 const ALGO_SHA1 = 4; 25 26 /** 27 * Detects the best available algorhythm for password hashing. 28 */ 29 static public function bestAlgoAvailable() 30 { 31 if ( function_exists('password_hash') ) 32 { 33 // Use BCRYPT, this is available since PHP 5.5 and is safe for now. 34 return self::ALGO_PHP_PASSWORD_HASH; 35 } 36 elseif ( function_exists('crypt') && defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1 ) 37 { 38 // see https://en.wikipedia.org/wiki/Blowfish_(cipher) 39 // BLOWFISH 40 return self::ALGO_CRYPT; 41 } 42 elseif ( function_exists('sha1') ) 43 { 44 // see https://en.wikipedia.org/wiki/SHA-1 45 // should not be used because of some security issues. 46 return self::ALGO_SHA1; 47 } 48 elseif ( function_exists('md5') ) 49 { 50 // see https://en.wikipedia.org/wiki/MD5 51 // should not be used because of some security issues. 52 return self::ALGO_MD5; 53 } 54 else 55 { 56 // This should never happen ;) 57 return self::ALGO_PLAIN; 58 } 59 } 60 61 62 63 /** 64 * Hash the password. 65 * 66 * @param $password string The password to hash 67 * @param $algo int Hashing algorhythm 68 * @param $cost cost factor: An integer between 4 and 31. 69 */ 70 static public function hash( $password,$algo,$cost=10 ) 71 { 72 switch( $algo ) 73 { 74 case self::ALGO_PHP_PASSWORD_HASH: 75 76 return password_hash( $password, PASSWORD_BCRYPT,array('cost'=>$cost) ); 77 78 case self::ALGO_CRYPT: 79 80 $salt = Password::randomHexString(10); // this should be cryptographically safe. 81 82 // see https://www.php.net/security/crypt_blowfish.php 83 if ( version_compare(PHP_VERSION, '5.3.7') >= 0 ) 84 $algo = '2y'; // BLOWFISH 85 else 86 $algo = '2a'; // "old" BLOWFISH, but no problem if using PHP >= 5.3.7 87 88 // cost factor should be between '04' and '31'. 89 $cost = max(min($cost,31),4); 90 $cost = str_pad($cost, 2, '0', STR_PAD_LEFT); 91 92 return crypt($password,'$'.$algo.'$'.$cost.'$'.$salt.'$'); 93 94 case self::ALGO_MD5: 95 return md5($password); // ooold. 96 97 case self::ALGO_SHA1: 98 return sha1($password); // 99 100 case self::ALGO_PLAIN: 101 return $password; // you want it, you get it. 102 } 103 } 104 105 /** 106 * Prüft das Kennwort gegen einen gespeicherten Hashwert. 107 * 108 * @param String $password Passwort 109 * @param String $hash Hash 110 * @return boolean true, falls das Passwort dem Hashwert entspricht. 111 */ 112 static public function check( $password,$hash,$algo ) 113 { 114 switch( $algo ) 115 { 116 case self::ALGO_PHP_PASSWORD_HASH: 117 // This is 'timing attack safe' as the documentation says. 118 return password_verify($password,$hash); 119 120 case self::ALGO_CRYPT: 121 122 if ( function_exists('crypt') ) 123 { 124 // Workaround: Die Spalte 'password' war frueher nur 50 Stellen lang, daher 125 // wird der mit crypt() erzeugte Hash auf die Länge des gespeicherten Hashes 126 // gekürzt. Mit aktuellen Versionen gespeicherte Hashes haben die volle Länge. 127 return Password::equals( $hash, substr(crypt($password,$hash),0,strlen($hash)) ); 128 } 129 else 130 { 131 throw new LogicException("Modular crypt format is not supported by this PHP ".PHP_VERSION." (no function 'crypt()')"); 132 } 133 134 case self::ALGO_SHA1: 135 return Password::equals( $hash, sha1($password) ); 136 137 case self::ALGO_MD5: 138 return Password::equals( $hash, md5($password) ); 139 140 case self::ALGO_PLAIN: 141 return Password::equals( $hash, $password ); 142 } 143 } 144 145 146 /** 147 * Creates cryptographic safe random bytes in HEX format. 148 * @param int $bytesCount 149 * @return string HEX 150 */ 151 static public function randomHexString( $bytesCount ) 152 { 153 return bin2hex( Password::randomBytes($bytesCount) ); 154 } 155 156 157 /** 158 * Creates a cryptographic safe number. 159 * @param int $bytesCount 160 * @return int 161 */ 162 static public function randomNumber( $bytesCount ) 163 { 164 // With '@' we reject the message "Invalid characters passed" since PHP 7.4 165 return @bindec( Password::randomBytes($bytesCount) ); 166 } 167 168 169 /** 170 * Creates cryptographic safe random bytes. 171 * @param int $bytesCount 172 * @return string Binary bytes 173 */ 174 static public function randomBytes( $bytesCount ) 175 { 176 if ( function_exists('random_bytes') ) 177 { 178 return random_bytes($bytesCount); 179 } 180 elseif ( function_exists('openssl_random_pseudo_bytes') ) 181 { 182 return openssl_random_pseudo_bytes($bytesCount); 183 } 184 else 185 { 186 // This fallback is NOT cryptographic safe! 187 $buf = ''; 188 for ($i = 0; $i < $length; ++$i) 189 { 190 $buf .= chr(mt_rand(0, 255)); 191 } 192 return $buf; 193 } 194 } 195 196 197 /** 198 * Time-safe Compare of 2 strings. 199 * 200 * @param String $known 201 * @param String $user 202 * @return boolean true, if equal 203 */ 204 static private function equals( $known, $user ) 205 { 206 if( function_exists('hash_equals')) 207 { 208 return hash_equals($known, $user); 209 } 210 else 211 { 212 if ( strlen($known) != strlen($user) ) 213 { 214 return false; 215 } 216 else 217 { 218 $res = $known ^ $user; 219 $ret = 0; 220 for($i = strlen($res) - 1; $i >= 0; $i--) $ret |= ord($res[$i]); 221 return !$ret; 222 } 223 } 224 } 225 226 227 /** 228 * Cryptographic delay of execution. 229 * Delay is from 0 to 168 milliseconds, Steps of 10 nanoseconds(!), which would be very heavy to attack over a network. 230 */ 231 static public function delay() 232 { 233 time_nanosleep(0, Password::randomNumber(3)*10); // delay: 0-167772150ns (= 0-~168ms) 234 } 235 236 237 /** 238 * Creates a new, pronounceable password. 239 * 240 * Inspired by http://www.phpbuilder.com/annotate/message.php3?id=1014451 241 * 242 * @return String a random password 243 */ 244 public static function createPassword() 245 { 246 $passwordConfig = Configuration::subset('security')->subset('password'); 247 248 $pw = ''; 249 $c = 'bcdfghjklmnprstvwz'; // consonants except hard to speak ones 250 $v = 'aeiou'; // vowels 251 $a = $c.$v.'123456789'; // both (plus numbers except zero) 252 253 //use two syllables... 254 for ( $i=0; $i < intval($passwordConfig->get('generated_length',16))/3; $i++ ) 255 { 256 $pw .= $c[rand(0, strlen($c)-1)]; 257 $pw .= $v[rand(0, strlen($v)-1)]; 258 $pw .= $a[rand(0, strlen($a)-1)]; 259 } 260 261 return $pw; 262 } 263 264 265 266 /** 267 * Pepper the password. 268 * 269 * Siehe http://de.wikipedia.org/wiki/Salt_%28Kryptologie%29#Pfeffer 270 * für weitere Informationen. 271 * 272 * @param $pass string password 273 * @return string peppered password 274 */ 275 public static function pepperPassword( $pass ) 276 { 277 $salt = Configuration::Conf()->subset('security')->subset('password')->get('pepper'); 278 279 return $salt.$pass; 280 } 281 282 283 284 }
Download modules/security/Password.class.php
History Fri, 15 Apr 2022 20:45:19 +0200 dankert Refactoring: Code cleanup. Wed, 23 Feb 2022 00:38:24 +0100 dankert New: Enable HOTP with counter synchronization; New: TOTP of the last period are valid too. Wed, 7 Oct 2020 23:28:52 +0200 Jan Dankert Fix: bindec() fails on PHP 7.4 if there a faulty bytes in a string. Sat, 1 Jun 2019 01:25:02 +0200 Jan Dankert Kommentare... Wed, 8 May 2019 21:30:20 +0200 Jan Dankert Statt globalen Konstanten Klassenkonstanten verwenden. Wed, 6 Dec 2017 23:53:14 +0100 Jan Dankert Eigener Namespace für Security-Klassen. Sun, 3 Dec 2017 03:33:57 +0100 Jan Dankert Refactoring: Security-Funktionen in ein eigenes "Modul" ausgelagert.