Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add Bcrypt and SHA512 crypt support to Security class #697

Closed
wants to merge 1 commit into from

2 participants

@sitedyno

Ready and willing to modify/update as needed :)

@markstory
Owner

Thanks for sending this as a pull request :). I wonder if the Security class' API wouldn't be simpler if the proposed crypt() method was folded into the existing Security::hash(). As it stands there would be two methods that handle one-way hashing, which will end up being confusing for some developers.

The usage could be something like

<?php
// Create a bcrypt hash
Security::setHash('blowfish');
$hash = Security::hash('content');

Security::setHash('sha256');
$hash = Security::hash('content');

// One off type change
$hash = Security::hash('content', 'blowfish');

The $salt param would only be supported for the older sha1, sha256, and md5 types. Those types would also directly use the Security.salt configure variable as they do now, while newer ciphers would use the proposed salt method.

@sitedyno

Default behavior is current behavior?

@markstory
Owner

Yeah, I don't think it would be wise to switch from sha1 to bcrypt as the default for a minor release. However, for 3.0, I'm all in favor of switching to bcrypt as the default.

@sitedyno

I'm thinking leave crypt() as is, or change it to a protected function. Then add minimal logic to hash() to call crypt() if type is blowfish or sha512. Sound good?

Also you used sha256 in the example, do you want to add the sha512 (with cost) or not?

Additionally I'm thinking about adding setCost() as function to set the cost for crypt, or would you prefer a hardcoded default?

@markstory
Owner

I think making crypt() into _crypt() makes sense. I think having it as a public method could be confusing, as I think most people associate encryption with 2 way encryption, which is not what this does.

I guess we could add sha512 as well. I also think having a setCost() method would make sense, as it saves having to repeat yourself each time you generate a hash.

@sitedyno

I'll do another pull request to 2.3 once i finish this up.

@sitedyno sitedyno closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 22, 2012
  1. @sitedyno
This page is out of date. Refresh to see the latest.
View
97 lib/Cake/Test/Case/Utility/SecurityTest.php
@@ -204,4 +204,101 @@ public function testRijndaelInvalidKey() {
$result = Security::rijndael($txt, $key, 'encrypt');
}
+/**
+ * testSalt method
+ *
+ * @return void
+ */
+ public function testSalt() {
+ $regex = "/[\.\/0-9A-Za-z]";
+
+ $length = 22;
+ $result = Security::salt($length);
+ $this->assertEquals($length, strlen($result));
+ $regexLen = $regex . "{" . $length . "}/";
+ $this->assertEquals(1, preg_match($regexLen, $result));
+
+ $length = 16;
+ $result = Security::salt($length);
+ $this->assertEquals($length, strlen($result));
+ $regexLen = $regex . "{" . $length . "}/";
+ $this->assertEquals(1, preg_match($regexLen, $result));
+ }
+
+/**
+ * testInvalidBlowfishCost setting
+ *
+ * @expectedException PHPUnit_Framework_Error
+ * @return void
+ */
+ public function testInvalidBlowfishCost() {
+ $options = array(
+ 'cryptType' => 'CRYPT_BLOWFISH',
+ 'cost' => 1,
+ );
+ $result = Security::crypt('somepassword', false, $options);
+ }
+
+/**
+ * testInvalidSha512Cost setting
+ *
+ * @expectedException PHPUnit_Framework_Error
+ * @return void
+ */
+ public function testInvalidSha512Cost() {
+ $options = array(
+ 'cryptType' => 'CRYPT_SHA512',
+ 'cost' => 1,
+ );
+ $result = Security::crypt('somepassword', false, $options);
+ }
+
+/**
+ * testCrypt method
+ *
+ * @return void
+ */
+ public function testCrypt() {
+ $pass = '!@#$%^&*()-_=+,./;\':"[]\\{}|';
+ $result = Security::crypt($pass, false);
+ $this->assertEquals($result, Security::crypt($pass, $result));
+
+ $pass = '123456789 123456789 123456789 123456789 123456789 123456789 123456789';
+ $result = Security::crypt($pass, false);
+ $this->assertEquals($result, Security::crypt($pass, $result));
+
+ $pass = 'パスワード';
+ $result = Security::crypt($pass, false);
+ $this->assertEquals($result, Security::crypt($pass, $result));
+
+ $pass = 'كلمة السر';
+ $result = Security::crypt($pass, false);
+ $this->assertEquals($result, Security::crypt($pass, $result));
+ }
+
+/**
+ * testCryptSHA512 method
+ *
+ * @return void
+ */
+ public function testCryptSHA512() {
+ $options = array(
+ 'cryptType' => 'CRYPT_SHA512'
+ );
+ $pass = '!@#$%^&*()-_=+,./;\':"[]\\{}|';
+ $result = Security::crypt($pass, false, $options);
+ $this->assertEquals($result, Security::crypt($pass, $result, $options));
+
+ $pass = '123456789 123456789 123456789 123456789 123456789 123456789 123456789';
+ $result = Security::crypt($pass, false, $options);
+ $this->assertEquals($result, Security::crypt($pass, $result, $options));
+
+ $pass = 'パスワード';
+ $result = Security::crypt($pass, false, $options);
+ $this->assertEquals($result, Security::crypt($pass, $result, $options));
+
+ $pass = 'كلمة السر';
+ $result = Security::crypt($pass, false, $options);
+ $this->assertEquals($result, Security::crypt($pass, $result, $options));
+ }
}
View
109 lib/Cake/Utility/Security.php
@@ -34,6 +34,50 @@ class Security {
public static $hashType = null;
/**
+ * BLOWFISH encryption method
+ *
+ * @const BLOWFISH
+ */
+ const BLOWFISH = 'CRYPT_BLOWFISH';
+
+/**
+ * SHA512 encryption method
+ *
+ * @const SHA512
+ */
+ const SHA512 = 'CRYPT_SHA512';
+
+/**
+ * Crypt settings.
+ *
+ * @var array
+ */
+ public static $cryptSettings = array(
+ 'cryptType' => self::BLOWFISH,
+ 'cost' => 10,
+ );
+
+/**
+ * Salt properties.
+ *
+ * @var array
+ */
+ protected static $_saltProperties = array(
+ 'saltFormat' => array(
+ self::BLOWFISH => '$2a$%s$%s$',
+ self::SHA512 => '$6$rounds=%s$%s$'
+ ),
+ 'saltLength' => array(
+ self::BLOWFISH => 22,
+ self::SHA512 => 16
+ ),
+ 'costLimits' => array(
+ self::BLOWFISH => array(4, 31),
+ self::SHA512 => array(1000, 999999999)
+ )
+ );
+
+/**
* Get allowed minutes of inactivity based on security level.
*
* @return integer Allowed inactivity in minutes
@@ -189,4 +233,69 @@ public static function rijndael($text, $key, $operation) {
return $out;
}
+/**
+ * Generates a psuedo random salt suitable for use with php's crypt() function.
+ * The salt length should not exceed 27. The salt will be composed of
+ * [./0-9A-Za-z]{$length}.
+ *
+ * @param integer $length The length of the returned salt
+ * @return string The generated salt
+ */
+ public static function salt($length = 22) {
+ return substr(str_replace('+', '.', base64_encode(sha1(uniqid(Configure::read('Security.salt'), true), true))), 0, $length);
+ }
+
+/**
+ * One way encryption using php's crypt() function.
+ *
+ * ### Options that can be passed to $options.
+ *
+ * - cryptType - The encryption type to use for this function call,
+ * CRYPT_BLOWFISH or CRYPT_SHA512.
+ * - cost - The cost to compute the hash. Higher cost increases security but
+ * also increases time taken to complete the encryption.
+ *
+ * @param string $password The string to be encrypted.
+ * @param string $salt False to generate new salt. Optionally pass the complete
+ * (from db etc) encrpted password as salt.
+ * @param array $options Array of options to override default settings, see above.
+ */
+ public static function crypt($password, $salt = false, $options = array()) {
+ if (isset($options['cryptType']) && $options['cryptType'] === self::SHA512 && !isset($options['cost'])) {
+ $options['cost'] = 5000;
+ }
+ $options = array_merge(self::$cryptSettings, $options, self::$_saltProperties);
+ extract($options);
+ if ($salt === false) {
+ if (isset($costLimits[$cryptType]) && ($cost < $costLimits[$cryptType][0] || $cost > $costLimits[$cryptType][1])) {
+ trigger_error(sprintf(
+ __d(
+ 'cake_dev',
+ 'When using %s you must specify a cost between %s and %s',
+ array(
+ $cryptType,
+ $costLimits[$cryptType][0],
+ $costLimits[$cryptType][1]
+ )
+ ),
+ E_USER_WARNING
+ ));
+ return '';
+ }
+ $vspArgs = array();
+ $salt = self::salt($saltLength[$cryptType]);
+ if ($cryptType === self::BLOWFISH) {
+ $bfCost = chr(ord('0') + $cost / 10);
+ $bfCost .= chr(ord('0') + $cost % 10);
+ $vspArgs[] = $bfCost;
+ }
+ if ($cryptType === self::SHA512) {
+ $vspArgs[] = $cost;
+ }
+ $vspArgs[] = $salt;
+ $salt = vsprintf($saltFormat[$cryptType], $vspArgs);
+ }
+ return crypt($password, $salt);
+ }
+
}
Something went wrong with that request. Please try again.