├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── test.php └── PasswordGenerator.php /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | [._]*.s[a-w][a-z] 3 | [._]s[a-w][a-z] 4 | /.idea 5 | /composer.lock 6 | /vendor/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - "7.1" 4 | - "7.0" 5 | - "5.6" 6 | - "5.5" 7 | - "5.4" 8 | - "5.3" 9 | # Versions below here are not installed on travis-ci 10 | # - "5.1" 11 | # - "5.0" 12 | # - "4.4" 13 | # - "4.3" 14 | # - "4.2" 15 | # - "4.1" 16 | # - "4.0" 17 | script: php test.php 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | php-passgen 2 | ============ 3 | 4 | [![Build Status](https://travis-ci.org/defuse/php-passgen.svg?branch=master)](https://travis-ci.org/defuse/php-passgen) 5 | 6 | A MIT-licensed library for generating cryptographically secure passwords in PHP. 7 | 8 | Audit Status 9 | ------------- 10 | 11 | Although it has been developed with security as the top priority, and with 12 | secure coding practices in mind, this code has not received any professional 13 | review. 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "defuse/php-passgen", 3 | "description": "PHP Random Password Generator Library", 4 | "license": "MIT", 5 | "keywords": ["security", "passwords", "random", "generate"], 6 | "authors": [ 7 | { 8 | "name": "Taylor Hornby", 9 | "email": "havoc@defuse.ca" 10 | } 11 | ], 12 | "autoload": { 13 | "classmap": ["PasswordGenerator.php"] 14 | }, 15 | "require": { 16 | "php": ">=5.3.0", 17 | "ext-mcrypt": "*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | 107 | -------------------------------------------------------------------------------- /PasswordGenerator.php: -------------------------------------------------------------------------------- 1 | ?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; 38 | return self::getCustomPassword(str_split($printable), $length); 39 | } 40 | 41 | public static function getAlphaNumericPassword($length) 42 | { 43 | $alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 44 | return self::getCustomPassword(str_split($alphanum), $length); 45 | } 46 | 47 | public static function getHexPassword($length) 48 | { 49 | $hex = "0123456789ABCDEF"; 50 | return self::getCustomPassword(str_split($hex), $length); 51 | } 52 | 53 | /* 54 | * Create a random password composed of a custom character set. 55 | * $characterSet - An *array* of strings the password can be composed of. 56 | * $length - The number of random strings (in $characterSet) to include in the password. 57 | * Returns false on error (always check!). 58 | */ 59 | public static function getCustomPassword($characterSet, $length) 60 | { 61 | if($length < 1 || !is_array($characterSet)) 62 | return false; 63 | 64 | $charSetLen = count($characterSet); 65 | if($charSetLen == 0) 66 | return false; 67 | 68 | $random = self::getRandomInts($length * 2); 69 | $mask = self::getMinimalBitMask($charSetLen - 1); 70 | 71 | $password = ""; 72 | 73 | // To generate the password, we repeatedly try random integers and use the ones within the range 74 | // 0 to $charSetLen - 1 to select an index into the character set. This is the only known way to 75 | // make a truly unbiased random selection from a set using random binary data. 76 | 77 | // A poorly implemented or malicious RNG could cause an infinite loop, leading to a denial of service. 78 | // We need to guarantee termination, so $iterLimit holds the number of further iterations we will allow. 79 | // It is extremely unlikely (about 2^-64) that more than $length*64 random ints are needed. 80 | $iterLimit = max($length, $length * 64); // If length is close to PHP_INT_MAX we don't want to overflow. 81 | $randIdx = 0; 82 | while(self::safeStrlen($password) < $length) 83 | { 84 | if($randIdx >= count($random)) 85 | { 86 | $random = self::getRandomInts(2*($length - strlen($password))); 87 | $randIdx = 0; 88 | } 89 | 90 | // This is wasteful, but RNGs are fast and doing otherwise adds complexity and bias. 91 | $c = $random[$randIdx++] & $mask; 92 | // Only use the random number if it is in range, otherwise try another (next iteration). 93 | if($c < $charSetLen) 94 | $password .= self::sidechannel_safe_array_index($characterSet, $c); 95 | // FIXME: check the return value 96 | 97 | // Guarantee termination 98 | $iterLimit--; 99 | if($iterLimit <= 0) { 100 | throw new ExtremelyUnlikelyRandomnessException("Hit iteration limit when generating password."); 101 | } 102 | } 103 | 104 | return $password; 105 | } 106 | 107 | // FIXME: This function needs unit tests! 108 | public static function getRandomInt($min_inclusive, $max_inclusive) 109 | { 110 | if ($min_inclusive > $max_inclusive) { 111 | throw new InvalidArgumentException("min is greater than max."); 112 | } 113 | 114 | if ($min_inclusive == $max_inclusive) { 115 | return $min_inclusive; 116 | } 117 | 118 | $range = $max_inclusive - $min_inclusive; 119 | $mask = self::getMinimalBitMask($range); 120 | 121 | $random = self::getRandomInts(10); 122 | $randIdx = 0; 123 | 124 | $iterLimit = 100; 125 | while (1) { 126 | /* Replenish the random integers if we ran out. */ 127 | if ($randIdx >= count($random)) { 128 | $random = self::getRandomInts(10); 129 | $randIdx = 0; 130 | } 131 | 132 | $offset = $random[$randIdx++] & $mask; 133 | if ($offset <= $range) { 134 | return $min_inclusive + $offset; 135 | } 136 | 137 | $iterLimit--; 138 | if ($iterLimit <= 0) { 139 | throw new ExtremelyUnlikelyRandomnessException("Hit iteration limit when generating random integer."); 140 | } 141 | } 142 | } 143 | 144 | // Returns the character at index $index in $string in constant time. 145 | private static function sidechannel_safe_array_index($string, $index) 146 | { 147 | // FIXME: Make the const-time hack below work for all integer sizes, or 148 | // check it properly. 149 | if (count($string) > 65535 || $index > count($string)) { 150 | return false; 151 | } 152 | $character = 0; 153 | for ($i = 0; $i < count($string); $i++) { 154 | $x = $i ^ $index; 155 | $mask = (((($x | ($x >> 16)) & 0xFFFF) + 0xFFFF) >> 16) - 1; 156 | $character |= ord($string[$i]) & $mask; 157 | } 158 | return chr($character); 159 | } 160 | 161 | // Returns the smallest bit mask of all 1s such that ($toRepresent & mask) = $toRepresent. 162 | // $toRepresent must be an integer greater than or equal to 1. 163 | private static function getMinimalBitMask($toRepresent) 164 | { 165 | if($toRepresent < 1) { 166 | throw new InvalidArgumentException("Non-positive integer passed to getMinimalBitMask."); 167 | } 168 | $mask = 0x1; 169 | while($mask < $toRepresent) { 170 | $mask = ($mask << 1) | 1; 171 | } 172 | return $mask; 173 | } 174 | 175 | // Returns an array of $numInts random integers between 0 and PHP_INT_MAX 176 | public static function getRandomInts($numInts) 177 | { 178 | $ints = array(); 179 | if ($numInts <= 0) { 180 | return $ints; 181 | } 182 | $rawBinary = mcrypt_create_iv($numInts * PHP_INT_SIZE, MCRYPT_DEV_URANDOM); 183 | for($i = 0; $i < $numInts; ++$i) 184 | { 185 | $thisInt = 0; 186 | for($j = 0; $j < PHP_INT_SIZE; ++$j) 187 | { 188 | $thisInt = ($thisInt << 8) | (ord($rawBinary[$i * PHP_INT_SIZE + $j]) & 0xFF); 189 | } 190 | // Absolute value in two's compliment (with min int going to zero) 191 | $thisInt = $thisInt & PHP_INT_MAX; 192 | $ints[] = $thisInt; 193 | } 194 | return $ints; 195 | } 196 | 197 | public static function safeStrlen($str) 198 | { 199 | if (function_exists('mb_strlen')) { 200 | return mb_strlen($str, '8bit'); 201 | } 202 | return strlen($str); 203 | } 204 | 205 | } 206 | --------------------------------------------------------------------------------