364 lines
9.9 KiB
PHP
364 lines
9.9 KiB
PHP
<?php namespace ProcessWire;
|
|
/**
|
|
* ProcessWire Password Fieldtype
|
|
*
|
|
* Class to hold combined password/salt info. Uses Blowfish when possible.
|
|
* Specially used by FieldtypePassword.
|
|
*
|
|
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
|
* https://processwire.com
|
|
*
|
|
* @method setPass($value) Protected internal use method
|
|
* @property string $salt
|
|
* @property string $hash
|
|
* @property-write string $pass
|
|
*
|
|
*/
|
|
|
|
class Password extends Wire {
|
|
|
|
/**
|
|
* @var array
|
|
*
|
|
*/
|
|
protected $data = array(
|
|
'salt' => '',
|
|
'hash' => '',
|
|
);
|
|
|
|
/**
|
|
* @var WireRandom|null
|
|
*
|
|
*/
|
|
protected $random = null;
|
|
|
|
/**
|
|
* Does this Password match the given string?
|
|
*
|
|
* @param string $pass Password to compare
|
|
* @return bool
|
|
*
|
|
*/
|
|
public function matches($pass) {
|
|
|
|
if(!strlen($pass)) return false;
|
|
$hash = $this->hash($pass);
|
|
if(!strlen($hash)) return false;
|
|
$updateNotify = false;
|
|
|
|
if($this->isBlowfish($hash)) {
|
|
$hash = substr($hash, 29);
|
|
|
|
} else if($this->supportsBlowfish()) {
|
|
// notify user they may want to change their password
|
|
// to take advantage of blowfish hashing
|
|
$updateNotify = true;
|
|
}
|
|
|
|
if(strlen($hash) < 29) return false;
|
|
|
|
if(function_exists("\\hash_equals")) {
|
|
$matches = hash_equals($this->data['hash'], $hash);
|
|
} else {
|
|
$matches = ($hash === $this->data['hash']);
|
|
}
|
|
|
|
if($matches && $updateNotify) {
|
|
$this->message($this->_('The password system has recently been updated. Please change your password to complete the update for your account.'));
|
|
}
|
|
|
|
return $matches;
|
|
}
|
|
|
|
/**
|
|
* Get a property via direct access ('salt' or 'hash')
|
|
*
|
|
* #pw-group-internal
|
|
*
|
|
* @param string $name
|
|
* @return mixed
|
|
*
|
|
*/
|
|
public function __get($name) {
|
|
if($name === 'salt' && empty($this->data['salt'])) $this->data['salt'] = $this->salt();
|
|
return isset($this->data[$name]) ? $this->data[$name] : null;
|
|
}
|
|
|
|
/**
|
|
* Set a property
|
|
*
|
|
* #pw-group-internal
|
|
*
|
|
* @param string $key
|
|
* @param mixed $value
|
|
*
|
|
*/
|
|
public function __set($key, $value) {
|
|
|
|
if($key === 'pass') {
|
|
// setting the password
|
|
$this->setPass($value);
|
|
|
|
} else if(array_key_exists($key, $this->data)) {
|
|
// something other than pass
|
|
$this->data[$key] = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the 'pass' to the given value
|
|
*
|
|
* @param string $value
|
|
* @throws WireException if given invalid $value
|
|
*
|
|
*/
|
|
protected function ___setPass($value) {
|
|
|
|
// if nothing supplied, then don't continue
|
|
if(!strlen($value)) return;
|
|
if(!is_string($value)) throw new WireException("Password must be a string");
|
|
|
|
// first check to see if it actually changed
|
|
if($this->data['salt'] && $this->data['hash']) {
|
|
$hash = $this->hash($value);
|
|
if($this->isBlowfish($hash)) $hash = substr($hash, 29);
|
|
// if no change then return now
|
|
if($hash === $this->data['hash']) return;
|
|
}
|
|
|
|
// password has changed
|
|
$this->trackChange('pass');
|
|
|
|
// force reset by clearing out the salt, hash() will gen a new salt
|
|
$this->data['salt'] = '';
|
|
|
|
// generate the new hash
|
|
$hash = $this->hash($value);
|
|
|
|
// if it's a blowfish hash, separate the salt from the hash
|
|
if($this->isBlowfish($hash)) {
|
|
$this->data['salt'] = substr($hash, 0, 29); // previously 28
|
|
$this->data['hash'] = substr($hash, 29);
|
|
} else {
|
|
$this->data['hash'] = $hash;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a random salt for the given hashType
|
|
*
|
|
* @return string
|
|
*
|
|
*/
|
|
protected function salt() {
|
|
|
|
// if system doesn't support blowfish, return old style salt
|
|
if(!$this->supportsBlowfish()) return md5($this->randomBase64String(44));
|
|
|
|
// blowfish assumed from this point forward
|
|
// use stronger blowfish mode if PHP version supports it
|
|
$salt = (version_compare(PHP_VERSION, '5.3.7') >= 0) ? '$2y' : '$2a';
|
|
|
|
// cost parameter (04-31)
|
|
$salt .= '$11$';
|
|
// 22 random base64 characters
|
|
$salt .= $this->randomBase64String(22);
|
|
// plus trailing $
|
|
$salt .= '$';
|
|
|
|
return $salt;
|
|
}
|
|
|
|
/**
|
|
* Generate a truly random base64 string of a certain length
|
|
*
|
|
* See WireRandom::base64() for details
|
|
*
|
|
* @param int $requiredLength Length of string you want returned (default=22)
|
|
* @param array|bool $options Specify array of options or boolean to specify only `fast` option.
|
|
* - `fast` (bool): Use fastest, not cryptographically secure method (default=false).
|
|
* - `test` (bool|array): Return tests in a string (bool true), or specify array(true) to return tests array (default=false).
|
|
* Note that if the test option is used, then the fast option is disabled.
|
|
* @return string|array Returns only array if you specify array for $test argument, otherwise returns string
|
|
*
|
|
*/
|
|
public function randomBase64String($requiredLength = 22, $options = array()) {
|
|
return $this->random()->base64($requiredLength, $options);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given string is blowfish hashed
|
|
*
|
|
* @param string $str
|
|
* @return bool
|
|
*
|
|
*/
|
|
public function isBlowfish($str = '') {
|
|
if(!strlen($str)) $str = $this->data['salt'];
|
|
$prefix = substr($str, 0, 3);
|
|
return $prefix === '$2a' || $prefix === '$2x' || $prefix === '$2y';
|
|
}
|
|
|
|
/**
|
|
* Returns whether the current system supports Blowfish
|
|
*
|
|
* @return bool
|
|
*
|
|
*/
|
|
public function supportsBlowfish() {
|
|
return version_compare(PHP_VERSION, '5.3.0') >= 0 && defined("CRYPT_BLOWFISH") && CRYPT_BLOWFISH;
|
|
}
|
|
|
|
/**
|
|
* Given an unhashed password, generate a hash of the password for database storage and comparison
|
|
*
|
|
* Note: When blowfish, returns the entire blowfish string which has the salt as the first 28 characters.
|
|
*
|
|
* @param string $pass Raw password
|
|
* @return string
|
|
* @throws WireException
|
|
*
|
|
*/
|
|
protected function hash($pass) {
|
|
|
|
$config = $this->wire()->config;
|
|
|
|
// if there is no salt yet, make one (for new pass or reset pass)
|
|
if(strlen($this->data['salt']) < 28) $this->data['salt'] = $this->salt();
|
|
|
|
// if system doesn't support blowfish, but has a blowfish salt, then reset it
|
|
if(!$this->supportsBlowfish() && $this->isBlowfish($this->data['salt'])) $this->data['salt'] = $this->salt();
|
|
|
|
// salt we made (the one ultimately stored in DB)
|
|
$salt1 = $this->data['salt'];
|
|
|
|
// static salt stored in config.php
|
|
$salt2 = (string) $config->userAuthSalt;
|
|
|
|
// auto-detect the hash type based on the format of the salt
|
|
$hashType = $this->isBlowfish($salt1) ? 'blowfish' : $config->userAuthHashType;
|
|
|
|
if(!$hashType) {
|
|
// If there is no defined hash type, and the system doesn't support blowfish, then just use md5 (ancient backwards compatibility)
|
|
$hash = md5($pass);
|
|
|
|
} else if($hashType == 'blowfish') {
|
|
if(!$this->supportsBlowfish()) {
|
|
throw new WireException("This version of PHP is not compatible with the passwords. Did passwords originate on a newer version of PHP?");
|
|
}
|
|
// our preferred method
|
|
$hash = crypt($pass . $salt2, $salt1);
|
|
|
|
} else {
|
|
// older style, non-blowfish support
|
|
// split the password in two
|
|
$splitPass = str_split($pass, (int) (strlen($pass) / 2) + 1);
|
|
// generate the hash
|
|
$hash = hash($hashType, $salt1 . $splitPass[0] . $salt2 . $splitPass[1], false);
|
|
}
|
|
|
|
if(!is_string($hash) || strlen($hash) <= 13) throw new WireException("Unable to generate password hash");
|
|
|
|
return $hash;
|
|
}
|
|
|
|
/**
|
|
* Return a pseudo-random alpha or alphanumeric character
|
|
*
|
|
* This method may be deprecated at some point, so it is preferable to use the
|
|
* `randomLetters()` or `randomAlnum()` methods instead, when you can count on
|
|
* the PW version being 3.0.109 or higher.
|
|
*
|
|
* @param int $qty Number of random characters requested
|
|
* @param bool $alphanumeric Specify true to allow digits in return value
|
|
* @param array $disallow Characters that may not be used in return value
|
|
* @return string
|
|
* @deprecated use WireRandom::alpha() instead
|
|
*
|
|
*/
|
|
public function randomAlpha($qty = 1, $alphanumeric = false, $disallow = array()) {
|
|
$letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
$digits = '0123456789';
|
|
if($alphanumeric) $letters .= $digits;
|
|
if($alphanumeric === 1) $letters = $digits; // digits only
|
|
foreach($disallow as $c) {
|
|
$letters = str_replace($c, '', $letters);
|
|
}
|
|
$value = '';
|
|
for($x = 0; $x < $qty; $x++) {
|
|
$n = mt_rand(0, strlen($letters) - 1);
|
|
$value .= $letters[$n];
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Return cryptographically secure random alphanumeric, alpha or numeric string
|
|
*
|
|
* @param int $length Required length of string, or 0 for random length
|
|
* @param array $options See WireRandom::alphanumeric() for options
|
|
* @return string
|
|
* @throws WireException
|
|
* @since 3.0.109
|
|
* @deprecated use WireRandom::alphanumeric() instead
|
|
*
|
|
*/
|
|
public function randomAlnum($length = 0, array $options = array()) {
|
|
return $this->random()->alphanumeric($length, $options);
|
|
}
|
|
|
|
/**
|
|
* Return string of random letters
|
|
*
|
|
* @param int $length Required length of string or 0 for random length
|
|
* @param array $options See options for randomAlnum() method
|
|
* @return string
|
|
* @since 3.0.109
|
|
* @deprecated use WireRandom::alpha() instead.
|
|
*
|
|
*/
|
|
public function randomLetters($length = 0, array $options = array()) {
|
|
return $this->random()->alpha($length, $options);
|
|
}
|
|
|
|
/**
|
|
* Return string of random digits
|
|
*
|
|
* @param int $length Required length of string or 0 for random length
|
|
* @param array $options See WireRandom::numeric() method
|
|
* @return string
|
|
* @since 3.0.109
|
|
* @deprecated Use WireRandom::numeric() instead
|
|
*
|
|
*/
|
|
public function randomDigits($length = 0, array $options = array()) {
|
|
return $this->random()->numeric($length, $options);
|
|
}
|
|
|
|
/**
|
|
* Generate and return a random password
|
|
*
|
|
* See WireRandom::pass() method for details.
|
|
*
|
|
* @param array $options See WireRandom::pass() for options
|
|
* @return string
|
|
*
|
|
*/
|
|
public function randomPass(array $options = array()) {
|
|
return $this->random()->pass($options);
|
|
}
|
|
|
|
/**
|
|
* @return WireRandom
|
|
*
|
|
*/
|
|
protected function random() {
|
|
if($this->random === null) $this->random = $this->wire(new WireRandom());
|
|
return $this->random;
|
|
}
|
|
|
|
public function __toString() {
|
|
return (string) $this->data['hash'];
|
|
}
|
|
|
|
}
|