PasswordGenerator.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. <?php
  2. /*
  3. * Unbiased random password generator.
  4. * Copyright (c) 2015, Taylor Hornby
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. *
  10. * 1. Redistributions of source code must retain the above copyright notice,
  11. * this list of conditions and the following disclaimer.
  12. *
  13. * 2. Redistributions in binary form must reproduce the above copyright notice,
  14. * this list of conditions and the following disclaimer in the documentation
  15. * and/or other materials provided with the distribution.
  16. *
  17. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  18. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  19. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  20. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  21. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  22. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  23. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  24. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  25. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  26. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  27. * POSSIBILITY OF SUCH DAMAGE.
  28. */
  29. class ExtremelyUnlikelyRandomnessException extends Exception {
  30. }
  31. class PasswordGenerator {
  32. public static function getASCIIPassword($length) {
  33. $printable = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
  34. return self::getCustomPassword(str_split($printable), $length);
  35. }
  36. public static function getAlphaNumericPassword($length) {
  37. $alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  38. return self::getCustomPassword(str_split($alphanum), $length);
  39. }
  40. public static function getHexPassword($length) {
  41. $hex = "0123456789ABCDEF";
  42. return self::getCustomPassword(str_split($hex), $length);
  43. }
  44. /*
  45. * Create a random password composed of a custom character set.
  46. * $characterSet - An *array* of strings the password can be composed of.
  47. * $length - The number of random strings (in $characterSet) to include in the password.
  48. * Returns false on error (always check!).
  49. */
  50. public static function getCustomPassword($characterSet, $length) {
  51. if ($length < 1 || !is_array($characterSet))
  52. return false;
  53. $charSetLen = count($characterSet);
  54. if ($charSetLen == 0)
  55. return false;
  56. $random = self::getRandomInts($length * 2);
  57. $mask = self::getMinimalBitMask($charSetLen - 1);
  58. $password = "";
  59. // To generate the password, we repeatedly try random integers and use the ones within the range
  60. // 0 to $charSetLen - 1 to select an index into the character set. This is the only known way to
  61. // make a truly unbiased random selection from a set using random binary data.
  62. // A poorly implemented or malicious RNG could cause an infinite loop, leading to a denial of service.
  63. // We need to guarantee termination, so $iterLimit holds the number of further iterations we will allow.
  64. // It is extremely unlikely (about 2^-64) that more than $length*64 random ints are needed.
  65. $iterLimit = max($length, $length * 64); // If length is close to PHP_INT_MAX we don't want to overflow.
  66. $randIdx = 0;
  67. while (self::safeStrlen($password) < $length) {
  68. if ($randIdx >= count($random)) {
  69. $random = self::getRandomInts(2 * ($length - strlen($password)));
  70. $randIdx = 0;
  71. }
  72. // This is wasteful, but RNGs are fast and doing otherwise adds complexity and bias.
  73. $c = $random[$randIdx++] & $mask;
  74. // Only use the random number if it is in range, otherwise try another (next iteration).
  75. if ($c < $charSetLen)
  76. $password .= self::sidechannel_safe_array_index($characterSet, $c);
  77. // FIXME: check the return value
  78. // Guarantee termination
  79. $iterLimit--;
  80. if ($iterLimit <= 0) {
  81. throw new ExtremelyUnlikelyRandomnessException("Hit iteration limit when generating password.");
  82. }
  83. }
  84. return $password;
  85. }
  86. // FIXME: This function needs unit tests!
  87. public static function getRandomInt($min_inclusive, $max_inclusive) {
  88. if ($min_inclusive > $max_inclusive) {
  89. throw new InvalidArgumentException("min is greater than max.");
  90. }
  91. if ($min_inclusive == $max_inclusive) {
  92. return $min_inclusive;
  93. }
  94. $range = $max_inclusive - $min_inclusive;
  95. $mask = self::getMinimalBitMask($range);
  96. $random = self::getRandomInts(10);
  97. $randIdx = 0;
  98. $iterLimit = 100;
  99. while (1) {
  100. /* Replenish the random integers if we ran out. */
  101. if ($randIdx >= count($random)) {
  102. $random = self::getRandomInts(10);
  103. $randIdx = 0;
  104. }
  105. $offset = $random[$randIdx++] & $mask;
  106. if ($offset <= $range) {
  107. return $min_inclusive + $offset;
  108. }
  109. $iterLimit--;
  110. if ($iterLimit <= 0) {
  111. throw new ExtremelyUnlikelyRandomnessException("Hit iteration limit when generating random integer.");
  112. }
  113. }
  114. }
  115. // Returns the character at index $index in $string in constant time.
  116. private static function sidechannel_safe_array_index($string, $index) {
  117. // FIXME: Make the const-time hack below work for all integer sizes, or
  118. // check it properly.
  119. if (count($string) > 65535 || $index > count($string)) {
  120. return false;
  121. }
  122. $character = 0;
  123. for ($i = 0; $i < count($string); $i++) {
  124. $x = $i ^ $index;
  125. $mask = (((($x | ($x >> 16)) & 0xFFFF) + 0xFFFF) >> 16) - 1;
  126. $character |= ord($string[$i]) & $mask;
  127. }
  128. return chr($character);
  129. }
  130. // Returns the smallest bit mask of all 1s such that ($toRepresent & mask) = $toRepresent.
  131. // $toRepresent must be an integer greater than or equal to 1.
  132. private static function getMinimalBitMask($toRepresent) {
  133. if ($toRepresent < 1) {
  134. throw new InvalidArgumentException("Non-positive integer passed to getMinimalBitMask.");
  135. }
  136. $mask = 0x1;
  137. while ($mask < $toRepresent) {
  138. $mask = ($mask << 1) | 1;
  139. }
  140. return $mask;
  141. }
  142. // Returns an array of $numInts random integers between 0 and PHP_INT_MAX
  143. public static function getRandomInts($numInts) {
  144. $ints = array();
  145. if ($numInts <= 0) {
  146. return $ints;
  147. }
  148. $rawBinary = mcrypt_create_iv($numInts * PHP_INT_SIZE, MCRYPT_DEV_URANDOM);
  149. for ($i = 0; $i < $numInts; ++$i) {
  150. $thisInt = 0;
  151. for ($j = 0; $j < PHP_INT_SIZE; ++$j) {
  152. $thisInt = ($thisInt << 8) | (ord($rawBinary[$i * PHP_INT_SIZE + $j]) & 0xFF);
  153. }
  154. // Absolute value in two's compliment (with min int going to zero)
  155. $thisInt = $thisInt & PHP_INT_MAX;
  156. $ints[] = $thisInt;
  157. }
  158. return $ints;
  159. }
  160. public static function safeStrlen($str) {
  161. if (function_exists('mb_strlen')) {
  162. return mb_strlen($str, '8bit');
  163. }
  164. return strlen($str);
  165. }
  166. }