class PhpassHashedPasswordBase
Same name in other branches
- 11.x core/lib/Drupal/Core/Password/PhpassHashedPasswordBase.php \Drupal\Core\Password\PhpassHashedPasswordBase
Legacy password hashing framework.
@internal
Hierarchy
- class \Drupal\Core\Password\PhpassHashedPasswordBase implements \Drupal\Core\Password\PasswordInterface
Expanded class hierarchy of PhpassHashedPasswordBase
See also
https://www.drupal.org/node/3322420
1 file declares its use of PhpassHashedPasswordBase
- PhpassHashedPassword.php in core/
modules/ phpass/ src/ Password/ PhpassHashedPassword.php
File
-
core/
lib/ Drupal/ Core/ Password/ PhpassHashedPasswordBase.php, line 14
Namespace
Drupal\Core\PasswordView source
abstract class PhpassHashedPasswordBase implements PasswordInterface {
/**
* The minimum allowed log2 number of iterations for password stretching.
*/
const MIN_HASH_COUNT = 7;
/**
* The maximum allowed log2 number of iterations for password stretching.
*/
const MAX_HASH_COUNT = 30;
/**
* The expected (and maximum) number of characters in a hashed password.
*/
const HASH_LENGTH = 55;
/**
* Returns a string for mapping an int to the corresponding base 64 character.
*
* @var string
*/
// phpcs:ignore Drupal.NamingConventions.ValidVariableName.LowerCamelName
public static $ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
/**
* Password stretching iteration count.
*
* Specifies the number of times the hashing function will be applied when
* generating new password hashes. The number of times is calculated by
* raising 2 to the power of the given value.
*
* @var int
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0.
* No replacement.
*
* @see https://www.drupal.org/node/3443277
*/
protected $countLog2;
/**
* The core PHP password interface.
*/
protected ?PasswordInterface $corePassword;
/**
* Constructs a new password hashing instance.
*
* @param \Drupal\Core\Password\PasswordInterface|int $corePassword
* The core PHP password interface (or the countLog2 value for BC).
*/
public function __construct(PasswordInterface|int $corePassword) {
if ($corePassword instanceof PasswordInterface) {
// Note: If $corePassword is set, $countLog2 isn't used anywhere in the
// code path of this class. Still, set it to the default value for BC
// reasons.
// @phpstan-ignore-next-line
$this->countLog2 = 16;
$this->corePassword = $corePassword;
}
else {
$countLog2 = $corePassword;
@trigger_error('Calling ' . __METHOD__ . '() with numeric $countLog2 as the first parameter is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use PhpassHashedPasswordInterface::__construct() with $corePassword parameter set to an instance of Drupal\\Core\\Password\\PhpPassword instead. See https://www.drupal.org/node/3322420', E_USER_DEPRECATED);
// Ensure that $countLog2 is within set bounds.
// @phpstan-ignore-next-line
$this->countLog2 = $this->enforceLog2Boundaries($countLog2);
$this->corePassword = NULL;
}
}
/**
* Encodes bytes into printable base 64 using the *nix standard from crypt().
*
* @param string $input
* The string containing bytes to encode.
* @param int $count
* The number of characters (bytes) to encode.
*
* @return string
* Encoded string.
*/
protected function base64Encode($input, $count) {
$output = '';
$i = 0;
do {
$value = ord($input[$i++]);
$output .= static::$ITOA64[$value & 0x3f];
if ($i < $count) {
$value |= ord($input[$i]) << 8;
}
$output .= static::$ITOA64[$value >> 6 & 0x3f];
if ($i++ >= $count) {
break;
}
if ($i < $count) {
$value |= ord($input[$i]) << 16;
}
$output .= static::$ITOA64[$value >> 12 & 0x3f];
if ($i++ >= $count) {
break;
}
$output .= static::$ITOA64[$value >> 18 & 0x3f];
} while ($i < $count);
return $output;
}
/**
* Generates a random base 64-encoded salt prefixed with hash settings.
*
* Proper use of salts may defeat a number of attacks, including:
* - The ability to try candidate passwords against multiple hashes at once.
* - The ability to use pre-hashed lists of candidate passwords.
* - The ability to determine whether two users have the same (or different)
* password without actually having to guess one of the passwords.
*
* @return string
* A 12 character string containing the iteration count and a random salt.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0.
* No replacement.
*
* @see https://www.drupal.org/node/3443277
*/
protected function generateSalt() {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. No replacement. See https://www.drupal.org/node/3443277', E_USER_DEPRECATED);
$output = '$S$';
// We encode the final log2 iteration count in base 64.
$output .= static::$ITOA64[$this->countLog2];
// 6 bytes is the standard salt for a portable phpass hash.
$output .= $this->base64Encode(random_bytes(6), 6);
return $output;
}
/**
* Ensures that $count_log2 is within set bounds.
*
* @param int $count_log2
* Integer that determines the number of iterations used in the hashing
* process. A larger value is more secure, but takes more time to complete.
*
* @return int
* Integer within set bounds that is closest to $count_log2.
*/
protected function enforceLog2Boundaries($count_log2) {
if ($count_log2 < static::MIN_HASH_COUNT) {
return static::MIN_HASH_COUNT;
}
elseif ($count_log2 > static::MAX_HASH_COUNT) {
return static::MAX_HASH_COUNT;
}
return (int) $count_log2;
}
/**
* Hash a password using a secure stretched hash.
*
* By using a salt and repeated hashing the password is "stretched". Its
* security is increased because it becomes much more computationally costly
* for an attacker to try to break the hash by brute-force computation of the
* hashes of a large number of plain-text words or strings to find a match.
*
* @param string $algo
* The string name of a hashing algorithm usable by hash(), like 'sha256'.
* @param string $password
* Plain-text password up to 512 bytes (128 to 512 UTF-8 characters) to
* hash.
* @param string $setting
* An existing hash or the output of $this->generateSalt(). Must be at least
* 12 characters (the settings and salt).
*
* @return string
* A string containing the hashed password (and salt) or FALSE on failure.
* The return string will be truncated at HASH_LENGTH characters max.
*/
protected function crypt($algo, $password, $setting) {
// Prevent DoS attacks by refusing to hash large passwords.
if (strlen($password) > PasswordInterface::PASSWORD_MAX_LENGTH) {
return FALSE;
}
// The first 12 characters of an existing hash are its setting string.
$setting = substr($setting, 0, 12);
if ($setting[0] != '$' || $setting[2] != '$') {
return FALSE;
}
$count_log2 = $this->getCountLog2($setting);
// Stored hashes may have been encrypted with any iteration count. However
// we do not allow applying the algorithm for unreasonable low and high
// values respectively.
if ($count_log2 != $this->enforceLog2Boundaries($count_log2)) {
return FALSE;
}
$salt = substr($setting, 4, 8);
// Hashes must have an 8 character salt.
if (strlen($salt) != 8) {
return FALSE;
}
// Convert the base 2 logarithm into an integer.
$count = 1 << $count_log2;
$hash = hash($algo, $salt . $password, TRUE);
do {
$hash = hash($algo, $hash . $password, TRUE);
} while (--$count);
$len = strlen($hash);
$output = $setting . $this->base64Encode($hash, $len);
// $this->base64Encode() of a 16 byte MD5 will always be 22 characters.
// $this->base64Encode() of a 64 byte sha512 will always be 86 characters.
$expected = 12 + ceil(8 * $len / 6);
return strlen($output) == $expected ? substr($output, 0, static::HASH_LENGTH) : FALSE;
}
/**
* Parses the log2 iteration count from a stored hash or setting string.
*
* @param string $setting
* An existing hash or the output of $this->generateSalt(). Must be at least
* 12 characters (the settings and salt).
*
* @return int
* The log2 iteration count.
*/
public function getCountLog2($setting) {
return strpos(static::$ITOA64, $setting[3]);
}
/**
* {@inheritdoc}
*/
public function hash($password) {
if (isset($this->corePassword)) {
return $this->corePassword
->hash($password);
}
// @phpstan-ignore-next-line
return $this->crypt('sha512', $password, $this->generateSalt());
}
/**
* {@inheritdoc}
*/
public function check($password, $hash) {
// Newly created accounts may have empty passwords.
if ($hash === NULL || $hash === '') {
return FALSE;
}
if (str_starts_with($hash, 'U$')) {
// This may be an updated password from user_update_7000(). Such hashes
// have 'U' added as the first character and need an extra md5() (see the
// Drupal 7 documentation).
$stored_hash = substr($hash, 1);
$password = md5($password);
}
else {
$stored_hash = $hash;
}
$type = substr($stored_hash, 0, 3);
switch ($type) {
case '$S$':
// A normal Drupal 7 password using sha512.
$computed_hash = $this->crypt('sha512', $password, $stored_hash);
break;
case '$H$':
// phpBB3 uses "$H$" for the same thing as "$P$".
case '$P$':
// A phpass password generated using md5. This is an
// imported password or from an earlier Drupal version.
$computed_hash = $this->crypt('md5', $password, $stored_hash);
break;
default:
if (isset($this->corePassword)) {
return $this->corePassword
->check($password, $stored_hash);
}
return FALSE;
}
// Compare using hash_equals() instead of === to mitigate timing attacks.
return $computed_hash && hash_equals($stored_hash, $computed_hash);
}
/**
* {@inheritdoc}
*/
public function needsRehash($hash) {
if (isset($this->corePassword)) {
return $this->corePassword
->needsRehash($hash);
}
// Check whether this was an updated password.
if (!str_starts_with($hash, '$S$') || strlen($hash) != static::HASH_LENGTH) {
return TRUE;
}
// Ensure that $count_log2 is within set bounds.
// @phpstan-ignore-next-line
$count_log2 = $this->enforceLog2Boundaries($this->countLog2);
// Check whether the iteration count used differs from the standard number.
return $this->getCountLog2($hash) !== $count_log2;
}
}
Members
Title Sort descending | Deprecated | Modifiers | Object type | Summary |
---|---|---|---|---|
PhpassHashedPasswordBase::$corePassword | protected | property | The core PHP password interface. | |
PhpassHashedPasswordBase::$countLog2 | Deprecated | protected | property | Password stretching iteration count. |
PhpassHashedPasswordBase::$ITOA64 | public static | property | ||
PhpassHashedPasswordBase::base64Encode | protected | function | Encodes bytes into printable base 64 using the *nix standard from crypt(). | |
PhpassHashedPasswordBase::check | public | function | ||
PhpassHashedPasswordBase::crypt | protected | function | Hash a password using a secure stretched hash. | |
PhpassHashedPasswordBase::enforceLog2Boundaries | protected | function | Ensures that $count_log2 is within set bounds. | |
PhpassHashedPasswordBase::generateSalt | Deprecated | protected | function | Generates a random base 64-encoded salt prefixed with hash settings. |
PhpassHashedPasswordBase::getCountLog2 | public | function | Parses the log2 iteration count from a stored hash or setting string. | |
PhpassHashedPasswordBase::hash | public | function | ||
PhpassHashedPasswordBase::HASH_LENGTH | constant | The expected (and maximum) number of characters in a hashed password. | ||
PhpassHashedPasswordBase::MAX_HASH_COUNT | constant | The maximum allowed log2 number of iterations for password stretching. | ||
PhpassHashedPasswordBase::MIN_HASH_COUNT | constant | The minimum allowed log2 number of iterations for password stretching. | ||
PhpassHashedPasswordBase::needsRehash | public | function | ||
PhpassHashedPasswordBase::__construct | public | function | Constructs a new password hashing instance. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.