123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- <?php
- /*
- * Unbiased random password generator.
- * Copyright (c) 2015, Taylor Hornby
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
- class ExtremelyUnlikelyRandomnessException extends Exception {
-
- }
- class PasswordGenerator {
- public static function getASCIIPassword($length) {
- $printable = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
- return self::getCustomPassword(str_split($printable), $length);
- }
- public static function getAlphaNumericPassword($length) {
- $alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- return self::getCustomPassword(str_split($alphanum), $length);
- }
- public static function getHexPassword($length) {
- $hex = "0123456789ABCDEF";
- return self::getCustomPassword(str_split($hex), $length);
- }
- /*
- * Create a random password composed of a custom character set.
- * $characterSet - An *array* of strings the password can be composed of.
- * $length - The number of random strings (in $characterSet) to include in the password.
- * Returns false on error (always check!).
- */
- public static function getCustomPassword($characterSet, $length) {
- if ($length < 1 || !is_array($characterSet))
- return false;
- $charSetLen = count($characterSet);
- if ($charSetLen == 0)
- return false;
- $random = self::getRandomInts($length * 2);
- $mask = self::getMinimalBitMask($charSetLen - 1);
- $password = "";
- // To generate the password, we repeatedly try random integers and use the ones within the range
- // 0 to $charSetLen - 1 to select an index into the character set. This is the only known way to
- // make a truly unbiased random selection from a set using random binary data.
- // A poorly implemented or malicious RNG could cause an infinite loop, leading to a denial of service.
- // We need to guarantee termination, so $iterLimit holds the number of further iterations we will allow.
- // It is extremely unlikely (about 2^-64) that more than $length*64 random ints are needed.
- $iterLimit = max($length, $length * 64); // If length is close to PHP_INT_MAX we don't want to overflow.
- $randIdx = 0;
- while (self::safeStrlen($password) < $length) {
- if ($randIdx >= count($random)) {
- $random = self::getRandomInts(2 * ($length - strlen($password)));
- $randIdx = 0;
- }
- // This is wasteful, but RNGs are fast and doing otherwise adds complexity and bias.
- $c = $random[$randIdx++] & $mask;
- // Only use the random number if it is in range, otherwise try another (next iteration).
- if ($c < $charSetLen)
- $password .= self::sidechannel_safe_array_index($characterSet, $c);
- // FIXME: check the return value
- // Guarantee termination
- $iterLimit--;
- if ($iterLimit <= 0) {
- throw new ExtremelyUnlikelyRandomnessException("Hit iteration limit when generating password.");
- }
- }
- return $password;
- }
- // FIXME: This function needs unit tests!
- public static function getRandomInt($min_inclusive, $max_inclusive) {
- if ($min_inclusive > $max_inclusive) {
- throw new InvalidArgumentException("min is greater than max.");
- }
- if ($min_inclusive == $max_inclusive) {
- return $min_inclusive;
- }
- $range = $max_inclusive - $min_inclusive;
- $mask = self::getMinimalBitMask($range);
- $random = self::getRandomInts(10);
- $randIdx = 0;
- $iterLimit = 100;
- while (1) {
- /* Replenish the random integers if we ran out. */
- if ($randIdx >= count($random)) {
- $random = self::getRandomInts(10);
- $randIdx = 0;
- }
- $offset = $random[$randIdx++] & $mask;
- if ($offset <= $range) {
- return $min_inclusive + $offset;
- }
- $iterLimit--;
- if ($iterLimit <= 0) {
- throw new ExtremelyUnlikelyRandomnessException("Hit iteration limit when generating random integer.");
- }
- }
- }
- // Returns the character at index $index in $string in constant time.
- private static function sidechannel_safe_array_index($string, $index) {
- // FIXME: Make the const-time hack below work for all integer sizes, or
- // check it properly.
- if (count($string) > 65535 || $index > count($string)) {
- return false;
- }
- $character = 0;
- for ($i = 0; $i < count($string); $i++) {
- $x = $i ^ $index;
- $mask = (((($x | ($x >> 16)) & 0xFFFF) + 0xFFFF) >> 16) - 1;
- $character |= ord($string[$i]) & $mask;
- }
- return chr($character);
- }
- // Returns the smallest bit mask of all 1s such that ($toRepresent & mask) = $toRepresent.
- // $toRepresent must be an integer greater than or equal to 1.
- private static function getMinimalBitMask($toRepresent) {
- if ($toRepresent < 1) {
- throw new InvalidArgumentException("Non-positive integer passed to getMinimalBitMask.");
- }
- $mask = 0x1;
- while ($mask < $toRepresent) {
- $mask = ($mask << 1) | 1;
- }
- return $mask;
- }
- // Returns an array of $numInts random integers between 0 and PHP_INT_MAX
- public static function getRandomInts($numInts) {
- $ints = array();
- if ($numInts <= 0) {
- return $ints;
- }
- $rawBinary = mcrypt_create_iv($numInts * PHP_INT_SIZE, MCRYPT_DEV_URANDOM);
- for ($i = 0; $i < $numInts; ++$i) {
- $thisInt = 0;
- for ($j = 0; $j < PHP_INT_SIZE; ++$j) {
- $thisInt = ($thisInt << 8) | (ord($rawBinary[$i * PHP_INT_SIZE + $j]) & 0xFF);
- }
- // Absolute value in two's compliment (with min int going to zero)
- $thisInt = $thisInt & PHP_INT_MAX;
- $ints[] = $thisInt;
- }
- return $ints;
- }
- public static function safeStrlen($str) {
- if (function_exists('mb_strlen')) {
- return mb_strlen($str, '8bit');
- }
- return strlen($str);
- }
- }
|