mlanthaler: Added support for password encryption (http://support.silverstripe.com/gsoc/ticket/34).

See http://www.silverstripe.com/google-summer-of-code-forum/flat/2417 for more information.  
(merged from branches/gsoc)


git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@41959 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2007-09-15 21:51:37 +00:00
parent 1119948ec4
commit 7be8460917
4 changed files with 333 additions and 8 deletions

View File

@ -66,4 +66,20 @@ Authenticator::registerAuthenticator('OpenIDAuthenticator');
define('MCE_ROOT', 'jsparty/tiny_mce2/');
/**
* Should passwords be encrypted (TRUE) or stored in clear text (FALSE)?
*/
Security::encrypt_passwords(true);
/**
* Which algorithm should be used to encrypt? Should a salt be used to
* increase the security?
*
* You can get a list of supported algorithms by calling
* {@link Security::get_encryption_algorithms()}
*/
Security::set_password_encryption_algorithm('sha1', true);
?>

View File

@ -5,7 +5,7 @@ class Member extends DataObject {
'FirstName' => "Varchar",
'Surname' => "Varchar",
'Email' => "Varchar",
'Password' => "Varchar",
'Password' => "Varchar(64)", // support for up to SHA256!
'RememberLoginToken' => "Varchar(50)",
'NumVisit' => "Int",
'LastVisited' => 'Datetime',
@ -13,7 +13,10 @@ class Member extends DataObject {
'AutoLoginHash' => 'Varchar(10)',
'AutoLoginExpired' => 'Datetime',
'BlacklistedEmail' => 'Boolean',
'PasswordEncryption' => "Enum('none', 'none')",
'Salt' => "Varchar(50)"
);
static $belongs_many_many = array(
"Groups" => "Group",
@ -30,6 +33,26 @@ class Member extends DataObject {
);
/**
* This method is used to initialize the static database members
*
* Since PHP doesn't support any expressions for the initialization of
* static member variables we need a method that does that.
*
* This method adds all supported encryption algorithms to the
* PasswordEncryption Enum field.
*
* @todo Maybe it would be useful to define this in DataObject and call
* it automatically?
*/
public static function init_db_fields() {
self::$db['PasswordEncryption'] = "Enum(array('none', '" .
implode("', '", array_map("addslashes",
Security::get_encryption_algorithms())) .
"'), 'none')";
}
/**
* Logs this member in
*
@ -280,6 +303,17 @@ class Member extends DataObject {
* found update this record to merge with that member.
*/
function onBeforeWrite() {
if(isset($this->changed['Password']) && $this->changed['Password']) {
// Password was changed: encrypt the password according the settings
$encryption_details = Security::encrypt_password($this->Password);
$this->Password = $encryption_details['password'];
$this->Salt = $encryption_details['salt'];
$this->PasswordEncryption = $encryption_details['algorithm'];
$this->changed['Salt'] = true;
$this->changed['PasswordEncryption'] = true;
}
if($this->Email) {
if($this->ID) {
$idClause = "AND `Member`.ID <> $this->ID";
@ -1087,5 +1121,8 @@ class Member_Validator extends RequiredFields {
}
}
// Initialize the static DB variables to add the supported encryption
// algorithms to the PasswordEncryption Enum field
Member::init_db_fields();
?>
?>

View File

@ -27,10 +27,20 @@ class MemberAuthenticator extends Authenticator {
*/
public function authenticate(array $RAW_data, Form $form = null) {
$SQL_user = Convert::raw2sql($RAW_data['Email']);
$SQL_password = Convert::raw2sql($RAW_data['Password']);
$member = DataObject::get_one(
"Member", "Email = '$SQL_user' AND Password = '$SQL_password'");
$member = DataObject::get_one("Member",
"Email = '$SQL_user' AND Password IS NOT NULL");
if($member) {
$encryption_details =
Security::encrypt_password($RAW_data['Password'], $member->Salt,
$member->PasswordEncryption);
// Check if the entered password is valid
if(($member->Password != $encryption_details['password']))
$member = null;
}
if($member) {
Session::clear("BackURL");

View File

@ -15,8 +15,37 @@ class Security extends Controller {
*/
protected static $password;
/**
* If set to TRUE to prevent sharing of the session across several sites
* in the domain.
*
* @var bool
*/
protected static $strictPathChecking = false;
/**
* Should passwords be stored encrypted?
*
* @var bool
*/
protected static $encryptPasswords = true;
/**
* The password encryption algorithm to use if {@link $encryptPasswords}
* is set to TRUE.
*
* @var string
*/
protected static $encryptionAlgorithm = 'sha1';
/**
* Should a salt be used for the password encryption?
*
* @var bool
*/
protected static $useSalt = true;
/**
* Register that we've had a permission failure trying to view the given page
*
@ -45,9 +74,9 @@ class Security extends Controller {
// Prepare the messageSet provided
if(!$messageSet) {
$messageSet = array(
'default' => "That page is secured. Enter your email address and password and we will send you right along.",
'alreadyLoggedIn' => "You don't have access to this page. If you have another password that can access that page, you can log in below.",
'logInAgain' => "You have been logged out. If you would like to log in again, enter a username and password below.",
'default' => "That page is secured. Enter your credentials below and we will send you right along.",
'alreadyLoggedIn' => "You don't have access to this page. If you have another account that can access that page, you can log in below.",
'logInAgain' => "You have been logged out. If you would like to log in again, enter your credentials below.",
);
} else if(!is_array($messageSet)) {
$messageSet = array('default' => $messageSet);
@ -388,6 +417,239 @@ class Security extends Controller {
static function getStrictPathChecking() {
return self::$strictPathChecking;
}
/**
* Set if passwords should be encrypted or not
*
* @param bool $encrypt Set to TRUE if you want that all (new) passwords
* will be stored encrypted, FALSE if you want to
* store the passwords in clear text.
*/
public static function encrypt_passwords($encrypt) {
self::$encryptPasswords = (bool)$encrypt;
}
/**
* Get a list of all available encryption algorithms
*
* This method tries to use PHP's hash_algos() function. If it is not
* supported or it returns no algorithms, as a failback mechanism it tries
* to use the md5() and sha1() function and returns them.
*
* @return array Returns an array of strings containing all supported
* encryption algorithms.
*/
public static function get_encryption_algorithms() {
$result = function_exists('hash_algos')
? hash_algos()
: array();
if(count($result) == 0) {
if(function_exists('md5'))
$result[] = 'md5';
if(function_exists('sha1'))
$result[] = 'sha1';
}
return $result;
}
/**
* Set the password encryption algorithm
*
* @param string $algorithm One of the available password encryption
* algorithms determined by
* {@link Security::get_encryption_algorithms()}
* @param bool $use_salt Set to TRUE if a random salt should be used to
* encrypt the passwords, otherwise FALSE
* @return bool Returns TRUE if the passed algorithm was valid, otherwise
* FALSE.
*/
public static function set_password_encryption_algorithm($algorithm,
$use_salt) {
if(in_array($algorithm, self::get_encryption_algorithms()) == false)
return false;
self::$encryptionAlgorithm = $algorithm;
self::$useSalt = (bool)$use_salt;
return true;
}
/**
* Get the the password encryption details
*
* The return value is an array of the following form:
* <code>
* array('encrypt_passwords' => bool,
* 'algorithm' => string,
* 'use_salt' => bool)
* </code>
*
* @return array Returns an associative array containing all the
* password encryption relevant information.
*/
public static function get_password_encryption_details() {
return array('encrypt_passwords' => self::$encryptPasswords,
'algorithm' => self::$encryptionAlgorithm,
'use_salt' => self::$useSalt);
}
/**
* Encrypt a password
*
* Encrypt a password according to the current password encryption
* settings.
* Use {@link Security::get_password_encryption_details()} to retrieve the
* current settings.
* If the settings are so that passwords shouldn't be encrypted, the
* result is simple the clear text password with an empty salt except when
* a custom algorithm ($algorithm parameter) was passed.
*
* @param string $password The password to encrypt
* @param string $salt Optional: The salt to use. If it is not passed, but
* needed, the method will automatically create a
* random salt that will then be returned as return
* value.
* @pram strin $algorithm Optional: Use another algorithm to encrypt the
* password (so that the encryption algorithm can
* be changed over the time).
* @return mixed Returns an associative array containing the encrypted
* password and the used salt in the form
* <i>array('encrypted_password' => string, 'salt' =>
* string, 'algorithm' => string)</i>.
* If the passed algorithm is invalid, FALSE will be
* returned.
*
* @see encrypt_passwords()
* @see set_password_encryption_algorithm()
* @see get_password_encryption_details()
*/
public static function encrypt_password($password, $salt = null,
$algorithm = null) {
if(strlen(trim($password)) == 0) {
// An empty password was passed, return an empty password an salt!
return array('password' => null,
'salt' => null,
'algorithm' => 'none');
} elseif((self::$encryptPasswords == false) || ($algorithm == 'none')) {
// The password should not be encrypted
return array('password' => substr($password, 0, 64),
'salt' => null,
'algorithm' => 'none');
} elseif(strlen(trim($algorithm)) != 0) {
// A custom encryption algorithm was passed, check if we can use it
if(in_array($algorithm, self::get_encryption_algorithms()) == false)
return false;
} else {
// Just use the default encryption algorithm
$algorithm = self::$encryptionAlgorithm;
}
// If no salt was provided but we need one we just generate a random one
if(strlen(trim($salt)) == 0)
$salt = null;
if((self::$useSalt == true) && is_null($salt)) {
$salt = sha1(mt_rand()) . time();
$salt = substr(base_convert($salt, 16, 36), 0, 50);
}
// Encrypt the password
if(function_exists('hash')) {
$password = hash($algorithm, $password . $salt);
} else {
$password = call_user_func($algorithm, $password . $salt);
}
// 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.
$password = substr(base_convert($password, 16, 36), 0, 64);
return array('password' => $password,
'salt' => $salt,
'algorithm' => $algorithm);
}
/**
* Encrypt all passwords
*
* Action to encrypt all *clear text* passwords in the database according
* to the current settings.
* If the current settings are so that passwords shouldn't be encrypted,
* an explanation will be printed out.
*
* To run this action, the user needs to have administrator rights!
*/
function encryptallpasswords() {
// Only administrators can run this method
if(!Member::currentUser() || !Member::currentUser()->isAdmin()) {
Security::permissionFailure($this,
"This page is secured and you need administrator rights to access it. " .
"Enter your credentials below and we will send you right along.");
return;
}
if(self::$encryptPasswords == false) {
print "<h1>Password encryption disabled!</h1>\n";
print "<p>To encrypt your passwords change your password settings by adding\n";
print "<pre>Security::encrypt_passwords(true);</pre>\nto mysite/_config.php</p>";
return;
}
// Are there members with a clear text password?
$members = DataObject::get("Member",
"PasswordEncryption = 'none' AND Password IS NOT NULL");
if(!$members) {
print "<h1>No passwords to encrypt</h1>\n";
print "<p>There are no members with a clear text password that could be encrypted!</p>\n";
return;
}
// Encrypt the passwords...
print "<h1>Encrypting all passwords</h1>";
print '<p>The passwords will be encrypted using the &quot;' .
htmlentities(self::$encryptionAlgorithm) . '&quot; algorithm ';
print (self::$useSalt)
? "with a salt to increase the security.</p>\n"
: "without using a salt to increase the security.</p><p>\n";
foreach($members as $member) {
// Force the update of the member record, as new passwords get
// automatically encrypted according to the settings, this will do all
// the work for us
$member->forceChange();
$member->write();
print " Encrypted credentials for member &quot;";
print htmlentities($member->getTitle()) . '&quot; (ID: ' . $member->ID .
'; E-Mail: ' . htmlentities($member->Email) . ")<br />\n";
}
print '</p>';
}
}
?>