2009-11-06 03:23:21 +01:00
< ? php
/**
* Allows pluggable password encryption .
* By default , this might be PHP ' s integrated sha1 ()
* function , but could also be more sophisticated to facilitate
* password migrations from other systems .
* Use { @ link register ()} to add new implementations .
*
* Used in { @ link Security :: encrypt_password ()} .
*
2012-04-12 08:02:46 +02:00
* @ package framework
2009-11-06 03:23:21 +01:00
* @ subpackage security
*/
abstract class PasswordEncryptor {
/**
* @ var array
*/
protected static $encryptors = array ();
/**
* @ return Array Map of encryptor code to the used class .
*/
static function get_encryptors () {
2012-04-07 04:59:55 +02:00
return Config :: inst () -> get ( 'PasswordEncryptor' , 'encryptors' );
2009-11-06 03:23:21 +01:00
}
/**
* Add a new encryptor implementation .
*
* Note : Due to portability concerns , its not advisable to
* override an existing $code mapping with different behaviour .
*
* @ param String $code This value will be stored stored in the
* { @ link Member -> PasswordEncryption } property .
* @ param String $class Classname of a { @ link PasswordEncryptor } subclass
*/
static function register ( $code , $class ) {
2012-04-07 04:59:55 +02:00
Deprecation :: notice ( '3.0' , 'Use the Config system to register Password encryptors' );
2009-11-06 03:23:21 +01:00
self :: $encryptors [ $code ] = $class ;
}
/**
* @ param String $code Unique lookup .
*/
static function unregister ( $code ) {
2012-04-07 04:59:55 +02:00
Deprecation :: notice ( '3.0' , 'Use the Config system to unregister Password encryptors' );
2009-11-06 03:23:21 +01:00
if ( isset ( self :: $encryptors [ $code ])) unset ( self :: $encryptors [ $code ]);
}
/**
* @ param String $algorithm
2012-04-07 04:59:55 +02:00
* @ return PasswordEncryptor
* @ throws PasswordEncryptor_NotFoundException
2009-11-06 03:23:21 +01:00
*/
static function create_for_algorithm ( $algorithm ) {
2012-04-07 04:59:55 +02:00
$encryptors = self :: get_encryptors ();
if ( ! isset ( $encryptors [ $algorithm ])) {
2009-11-06 03:23:21 +01:00
throw new PasswordEncryptor_NotFoundException (
sprintf ( 'No implementation found for "%s"' , $algorithm )
);
}
2012-04-07 04:59:55 +02:00
$class = key ( $encryptors [ $algorithm ]);
2009-11-06 03:23:21 +01:00
if ( ! class_exists ( $class )) {
throw new PasswordEncryptor_NotFoundException (
sprintf ( 'No class found for "%s"' , $class )
);
2012-04-07 04:59:55 +02:00
}
$refClass = new ReflectionClass ( $class );
if ( ! $refClass -> getConstructor ()) {
return new $class ;
}
$arguments = $encryptors [ $algorithm ];
return ( $refClass -> newInstanceArgs ( $arguments ));
2009-11-06 03:23:21 +01:00
}
/**
* Return a string value stored in the { @ link Member -> Password } property .
* The password should be hashed with { @ link salt ()} if applicable .
*
* @ param String $password Cleartext password to be hashed
* @ param String $salt ( Optional )
* @ param Member $member ( Optional )
* @ return String Maximum of 512 characters .
*/
abstract function encrypt ( $password , $salt = null , $member = null );
/**
* Return a string value stored in the { @ link Member -> Salt } property .
*
2010-12-05 01:37:35 +01:00
* @ uses RandomGenerator
*
2009-11-06 03:23:21 +01:00
* @ param String $password Cleartext password
* @ param Member $member ( Optional )
* @ return String Maximum of 50 characters
*/
function salt ( $password , $member = null ) {
2010-12-05 01:37:35 +01:00
$generator = new RandomGenerator ();
return substr ( $generator -> generateHash ( 'sha1' ), 0 , 50 );
2009-11-06 03:23:21 +01:00
}
/**
* This usually just returns a strict string comparison ,
* but is necessary for { @ link PasswordEncryptor_LegacyPHPHash } .
*
* @ param String $hash1
* @ param String $hash2
* @ return boolean
2012-05-07 05:03:53 +02:00
*
* @ deprecated 3.0 - Use PasswordEncryptor :: check () instead .
2009-11-06 03:23:21 +01:00
*/
function compare ( $hash1 , $hash2 ) {
2012-05-07 05:03:53 +02:00
Deprecation :: notice ( '3.0.0' , 'PasswordEncryptor::compare() is deprecated, replaced by PasswordEncryptor::check().' );
2009-11-06 03:23:21 +01:00
return ( $hash1 === $hash2 );
}
2012-05-07 05:03:53 +02:00
/**
* This usually just returns a strict string comparison ,
* but is necessary for retain compatibility with password hashed
* with flawed algorithms - see { @ link PasswordEncryptor_LegacyPHPHash } and
* { @ link PasswordEncryptor_Blowfish }
*/
function check ( $hash , $password , $salt = null , $member = null ) {
return $hash === $this -> encrypt ( $password , $salt , $member );
}
2009-11-06 03:23:21 +01:00
}
/**
2012-05-02 03:51:29 +02:00
* Blowfish encryption - this is the default from SilverStripe 3.
* PHP 5.3 + will provide a php implementation if there is no system
* version available .
*
* @ package framework
* @ subpackage security
*/
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 .
2012-06-14 05:04:01 +02:00
* 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 .
2012-05-02 03:51:29 +02:00
*/
protected static $cost = 10 ;
2012-06-14 05:04:01 +02:00
/**
* 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
* @ return null
*/
public static function set_cost ( $cost ) {
self :: $cost = max ( min ( 31 , $cost ), 4 );
}
/**
* Gets the cost that is set for the blowfish algorithm
*
* @ param int $cost
* @ return null
*/
2012-06-15 02:13:33 +02:00
public static function get_cost () {
2012-06-14 05:04:01 +02:00
return self :: $cost ;
}
2012-05-02 03:51:29 +02:00
function encrypt ( $password , $salt = null , $member = null ) {
2012-05-07 05:04:09 +02:00
// 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.
2012-05-08 00:33:03 +02:00
$encryptedPassword = $this -> encryptY ( $password , $salt );
if ( ! $encryptedPassword ) {
$encryptedPassword = $this -> encryptA ( $password , $salt );
2012-05-07 05:04:09 +02:00
}
2012-05-08 00:33:03 +02:00
if ( ! $encryptedPassword ) {
$encryptedPassword = $this -> encryptX ( $password , $salt );
2012-05-07 05:04:09 +02:00
}
2012-05-02 03:51:29 +02:00
// We *never* want to generate blank passwords. If something
// goes wrong, throw an exception.
2012-05-08 00:33:03 +02:00
if ( strpos ( $encryptedPassword , '$2' ) === false ) {
2012-05-02 03:51:29 +02:00
throw new PasswordEncryptor_EncryptionFailed ( 'Blowfish password encryption failed.' );
2012-05-02 04:59:39 +02:00
}
2012-05-02 03:51:29 +02:00
2012-05-08 00:33:03 +02:00
return $encryptedPassword ;
2012-05-07 05:04:09 +02:00
}
2012-05-08 00:33:03 +02:00
function encryptX ( $password , $salt ) {
$methodAndSalt = '$2x$' . $salt ;
$encryptedPassword = crypt ( $password , $methodAndSalt );
2012-05-07 05:04:09 +02:00
2012-05-08 00:33:03 +02:00
if ( strpos ( $encryptedPassword , '$2x$' ) === 0 ) {
return $encryptedPassword ;
2012-05-07 05:04:09 +02:00
}
// Check if system a is actually x, and if available, use that.
2012-05-08 00:33:03 +02:00
if ( $this -> checkAEncryptionLevel () == 'x' ) {
$methodAndSalt = '$2a$' . $salt ;
$encryptedPassword = crypt ( $password , $methodAndSalt );
2012-05-07 05:04:09 +02:00
2012-05-08 00:33:03 +02:00
if ( strpos ( $encryptedPassword , '$2a$' ) === 0 ) {
$encryptedPassword = '$2x$' . substr ( $encryptedPassword , strlen ( '$2a$' ));
return $encryptedPassword ;
2012-05-07 05:04:09 +02:00
}
}
return false ;
}
2012-05-08 00:33:03 +02:00
function encryptY ( $password , $salt ) {
$methodAndSalt = '$2y$' . $salt ;
$encryptedPassword = crypt ( $password , $methodAndSalt );
2012-05-07 05:04:09 +02:00
2012-05-08 00:33:03 +02:00
if ( strpos ( $encryptedPassword , '$2y$' ) === 0 ) {
return $encryptedPassword ;
2012-05-07 05:04:09 +02:00
}
// Check if system a is actually y, and if available, use that.
2012-05-08 00:33:03 +02:00
if ( $this -> checkAEncryptionLevel () == 'y' ) {
$methodAndSalt = '$2a$' . $salt ;
$encryptedPassword = crypt ( $password , $methodAndSalt );
2012-05-07 05:04:09 +02:00
2012-05-08 00:33:03 +02:00
if ( strpos ( $encryptedPassword , '$2a$' ) === 0 ) {
$encryptedPassword = '$2y$' . substr ( $encryptedPassword , strlen ( '$2a$' ));
return $encryptedPassword ;
2012-05-07 05:04:09 +02:00
}
}
return false ;
}
2012-05-08 00:33:03 +02:00
function encryptA ( $password , $salt ) {
if ( $this -> checkAEncryptionLevel () == 'a' ) {
$methodAndSalt = '$2a$' . $salt ;
$encryptedPassword = crypt ( $password , $methodAndSalt );
2012-05-07 05:04:09 +02:00
2012-05-08 00:33:03 +02:00
if ( strpos ( $encryptedPassword , '$2a$' ) === 0 ) {
return $encryptedPassword ;
2012-05-07 05:04:09 +02:00
}
}
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 .
*/
2012-05-08 00:33:03 +02:00
function checkAEncryptionLevel () {
2012-05-07 05:04:09 +02:00
// 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
2012-05-16 06:40:12 +02:00
$xOrY = crypt ( " \xff \xa3 34 \xff \xff \xff \xa3 345 " , '$2a$05$/OK.fbVrR/bpIqNJ5ianF.o./n25XVfn6oAPaUvHe.Csk4zRfsYPi' ) == '$2a$05$/OK.fbVrR/bpIqNJ5ianF.o./n25XVfn6oAPaUvHe.Csk4zRfsYPi' ;
2012-05-08 00:33:03 +02:00
$yOrA = crypt ( " \xa3 " , '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq' ) == '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq' ;
2012-05-07 05:04:09 +02:00
2012-05-08 00:33:03 +02:00
if ( $xOrY && $yOrA ) {
2012-05-07 05:04:09 +02:00
return 'y' ;
2012-05-08 00:33:03 +02:00
} elseif ( $xOrY ) {
2012-05-07 05:04:09 +02:00
return 'x' ;
2012-05-08 00:33:03 +02:00
} elseif ( $yOrA ) {
2012-05-07 05:04:09 +02:00
return 'a' ;
}
return 'unknown' ;
2012-05-02 03:51:29 +02:00
}
2012-06-14 05:04:01 +02:00
/**
* self :: $cost param is forced to be two digits with leading zeroes for ints 4 - 9
*/
2012-05-07 05:04:09 +02:00
function salt ( $password , $member = null ) {
2012-05-02 03:51:29 +02:00
$generator = new RandomGenerator ();
2012-06-14 05:04:01 +02:00
return sprintf ( '%02d' , self :: $cost ) . '$' . substr ( $generator -> generateHash ( 'sha1' ), 0 , 22 );
2012-05-07 05:04:09 +02:00
}
function check ( $hash , $password , $salt = null , $member = null ) {
if ( strpos ( $hash , '$2y$' ) === 0 ) {
2012-05-08 00:33:03 +02:00
return $hash === $this -> encryptY ( $password , $salt );
2012-05-07 05:04:09 +02:00
} elseif ( strpos ( $hash , '$2a$' ) === 0 ) {
2012-05-08 00:33:03 +02:00
return $hash === $this -> encryptA ( $password , $salt );
2012-05-07 05:04:09 +02:00
} elseif ( strpos ( $hash , '$2x$' ) === 0 ) {
2012-05-08 00:33:03 +02:00
return $hash === $this -> encryptX ( $password , $salt );
2012-05-07 05:04:09 +02:00
}
return false ;
2012-05-02 03:51:29 +02:00
}
}
/**
* Encryption using built - in hash types in PHP .
2009-11-06 03:23:21 +01:00
* Please note that the implemented algorithms depend on the PHP
* distribution and architecture .
*
2012-04-12 08:02:46 +02:00
* @ package framework
2009-11-06 03:23:21 +01:00
* @ subpackage security
*/
class PasswordEncryptor_PHPHash extends PasswordEncryptor {
protected $algorithm = 'sha1' ;
/**
* @ param String $algorithm A PHP built - in hashing algorithm as defined by hash_algos ()
*/
function __construct ( $algorithm ) {
if ( ! in_array ( $algorithm , hash_algos ())) {
throw new Exception (
sprintf ( 'Hash algorithm "%s" not found in hash_algos()' , $algorithm )
);
}
$this -> algorithm = $algorithm ;
}
/**
* @ return string
*/
function getAlgorithm () {
return $this -> algorithm ;
}
function encrypt ( $password , $salt = null , $member = null ) {
2012-03-27 09:09:36 +02:00
return hash ( $this -> algorithm , $password . $salt );
2009-11-06 03:23:21 +01:00
}
}
/**
* Legacy implementation for SilverStripe 2.1 - 2.3 ,
* which had a design flaw in password hashing that caused
* the hashes to differ between architectures due to
* floating point precision problems in base_convert () .
* See http :// open . silverstripe . org / ticket / 3004
*
2012-04-12 08:02:46 +02:00
* @ package framework
2009-11-06 03:23:21 +01:00
* @ subpackage security
*/
class PasswordEncryptor_LegacyPHPHash extends PasswordEncryptor_PHPHash {
function encrypt ( $password , $salt = null , $member = null ) {
2010-04-12 07:01:31 +02:00
$password = parent :: encrypt ( $password , $salt , $member );
2009-11-06 03:23:21 +01:00
// Legacy fix: This shortening logic is producing unpredictable results.
//
// Convert the base of the hexadecimal password to 36 to make it shorter
// In that way we can store also a SHA256 encrypted password in just 64
// letters.
return substr ( base_convert ( $password , 16 , 36 ), 0 , 64 );
}
function compare ( $hash1 , $hash2 ) {
2012-05-07 05:03:53 +02:00
Deprecation :: notice ( '3.0.0' , 'PasswordEncryptor::compare() is deprecated, replaced by PasswordEncryptor::check().' );
2009-11-06 03:23:21 +01:00
// Due to flawed base_convert() floating poing precision,
// only the first 10 characters are consistently useful for comparisons.
return ( substr ( $hash1 , 0 , 10 ) === substr ( $hash2 , 0 , 10 ));
}
2012-05-07 05:03:53 +02:00
function check ( $hash , $password , $salt = null , $member = null ) {
// Due to flawed base_convert() floating poing precision,
// only the first 10 characters are consistently useful for comparisons.
return ( substr ( $hash , 0 , 10 ) === substr ( $this -> encrypt ( $password , $salt , $member ), 0 , 10 ));
}
2009-11-06 03:23:21 +01:00
}
/**
* Uses MySQL ' s PASSWORD encryption . Requires an active DB connection .
*
2012-04-12 08:02:46 +02:00
* @ package framework
2009-11-06 03:23:21 +01:00
* @ subpackage security
*/
class PasswordEncryptor_MySQLPassword extends PasswordEncryptor {
function encrypt ( $password , $salt = null , $member = null ) {
return DB :: query (
sprintf ( " SELECT PASSWORD('%s') " , Convert :: raw2sql ( $password ))
) -> value ();
}
function salt ( $password , $member = null ) {
return false ;
}
}
/**
* Uses MySQL ' s OLD_PASSWORD encyrption . Requires an active DB connection .
*
2012-04-12 08:02:46 +02:00
* @ package framework
2009-11-06 03:23:21 +01:00
* @ subpackage security
*/
class PasswordEncryptor_MySQLOldPassword extends PasswordEncryptor {
function encrypt ( $password , $salt = null , $member = null ) {
return DB :: query (
sprintf ( " SELECT OLD_PASSWORD('%s') " , Convert :: raw2sql ( $password ))
) -> value ();
}
function salt ( $password , $member = null ) {
return false ;
}
}
/**
* Cleartext passwords ( used in SilverStripe 2.1 ) .
* Also used when Security :: $encryptPasswords is set to FALSE .
* Not recommended .
*
2012-04-12 08:02:46 +02:00
* @ package framework
2009-11-06 03:23:21 +01:00
* @ subpackage security
*/
class PasswordEncryptor_None extends PasswordEncryptor {
function encrypt ( $password , $salt = null , $member = null ) {
return $password ;
}
function salt ( $password , $member = null ) {
return false ;
}
}
/**
2012-04-12 08:02:46 +02:00
* @ package framework
2009-11-06 03:23:21 +01:00
* @ subpackage security
*/
2012-03-09 03:06:12 +01:00
class PasswordEncryptor_NotFoundException extends Exception {}
2012-05-02 03:51:29 +02:00
/**
* @ package framework
* @ subpackage security
*/
class PasswordEncryptor_EncryptionFailed extends Exception {}