silverstripe-framework/src/Security/PasswordEncryptor_Blowfish.php

181 lines
6.0 KiB
PHP

<?php
namespace SilverStripe\Security;
/**
* Blowfish encryption - this is the default from SilverStripe 3.
* PHP 5.3+ will provide a php implementation if there is no system
* version available.
*/
class PasswordEncryptor_Blowfish extends PasswordEncryptor
{
/**
* Cost of encryption.
* Higher costs will increase security, but also increase server load.
* If you are using basic auth, you may need to decrease this as encryption
* will be run on every request.
* The two digit cost parameter is the base-2 logarithm of the iteration
* count for the underlying Blowfish-based hashing algorithmeter and must
* be in range 04-31, values outside this range will cause crypt() to fail.
*/
protected static $cost = 10;
/**
* Sets the cost of the blowfish algorithm.
* See {@link PasswordEncryptor_Blowfish::$cost}
* Cost is set as an integer but
* Ensure that set values are from 4-31
*
* @param int $cost range 4-31
*/
public static function set_cost($cost)
{
self::$cost = max(min(31, $cost), 4);
}
/**
* Gets the cost that is set for the blowfish algorithm
*
* @return int
*/
public static function get_cost()
{
return self::$cost;
}
public function encrypt($password, $salt = null, $member = null)
{
// See: http://nz.php.net/security/crypt_blowfish.php
// There are three version of the algorithm - y, a and x, in order
// of decreasing security. Attempt to use the strongest version.
$encryptedPassword = $this->encryptY($password, $salt);
if (!$encryptedPassword) {
$encryptedPassword = $this->encryptA($password, $salt);
}
if (!$encryptedPassword) {
$encryptedPassword = $this->encryptX($password, $salt);
}
// We *never* want to generate blank passwords. If something
// goes wrong, throw an exception.
if (strpos($encryptedPassword ?? '', '$2') === false) {
throw new PasswordEncryptor_EncryptionFailed('Blowfish password encryption failed.');
}
return $encryptedPassword;
}
public function encryptX($password, $salt)
{
$methodAndSalt = '$2x$' . $salt;
$encryptedPassword = crypt($password ?? '', $methodAndSalt ?? '');
if (strpos($encryptedPassword ?? '', '$2x$') === 0) {
return $encryptedPassword;
}
// Check if system a is actually x, and if available, use that.
if ($this->checkAEncryptionLevel() == 'x') {
$methodAndSalt = '$2a$' . $salt;
$encryptedPassword = crypt($password ?? '', $methodAndSalt ?? '');
if (strpos($encryptedPassword ?? '', '$2a$') === 0) {
$encryptedPassword = '$2x$' . substr($encryptedPassword ?? '', strlen('$2a$'));
return $encryptedPassword;
}
}
return false;
}
public function encryptY($password, $salt)
{
$methodAndSalt = '$2y$' . $salt;
$encryptedPassword = crypt($password ?? '', $methodAndSalt ?? '');
if (strpos($encryptedPassword ?? '', '$2y$') === 0) {
return $encryptedPassword;
}
// Check if system a is actually y, and if available, use that.
if ($this->checkAEncryptionLevel() == 'y') {
$methodAndSalt = '$2a$' . $salt;
$encryptedPassword = crypt($password ?? '', $methodAndSalt ?? '');
if (strpos($encryptedPassword ?? '', '$2a$') === 0) {
$encryptedPassword = '$2y$' . substr($encryptedPassword ?? '', strlen('$2a$'));
return $encryptedPassword;
}
}
return false;
}
public function encryptA($password, $salt)
{
if ($this->checkAEncryptionLevel() == 'a') {
$methodAndSalt = '$2a$' . $salt;
$encryptedPassword = crypt($password ?? '', $methodAndSalt ?? '');
if (strpos($encryptedPassword ?? '', '$2a$') === 0) {
return $encryptedPassword;
}
}
return false;
}
/**
* The algorithm returned by using '$2a$' is not consistent -
* it might be either the correct (y), incorrect (x) or mostly-correct (a)
* version, depending on the version of PHP and the operating system,
* so we need to test it.
*/
public function checkAEncryptionLevel()
{
// Test hashes taken from
// http://cvsweb.openwall.com/cgi/cvsweb.cgi/~checkout~/Owl/packages/glibc
// /crypt_blowfish/wrapper.c?rev=1.9.2.1;content-type=text%2Fplain
$xOrY = crypt("\xff\xa334\xff\xff\xff\xa3345", '$2a$05$/OK.fbVrR/bpIqNJ5ianF.o./n25XVfn6oAPaUvHe.Csk4zRfsYPi')
== '$2a$05$/OK.fbVrR/bpIqNJ5ianF.o./n25XVfn6oAPaUvHe.Csk4zRfsYPi';
$yOrA = crypt("\xa3", '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq')
== '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq';
if ($xOrY && $yOrA) {
return 'y';
} elseif ($xOrY) {
return 'x';
} elseif ($yOrA) {
return 'a';
}
return 'unknown';
}
/**
* self::$cost param is forced to be two digits with leading zeroes for ints 4-9
*
* @param string $password
* @param Member $member
* @return string
*/
public function salt($password, $member = null)
{
$generator = new RandomGenerator();
return sprintf('%02d', self::$cost) . '$' . substr($generator->randomToken('sha1') ?? '', 0, 22);
}
public function check($hash, $password, $salt = null, $member = null)
{
if (strpos($hash ?? '', '$2y$') === 0) {
return $hash === $this->encryptY($password, $salt);
} elseif (strpos($hash ?? '', '$2a$') === 0) {
return $hash === $this->encryptA($password, $salt);
} elseif (strpos($hash ?? '', '$2x$') === 0) {
return $hash === $this->encryptX($password, $salt);
}
return false;
}
}