Skip to content
This repository
Browse code

Add bcrypt support to Security::hash()

  • Loading branch information...
commit 434d3a71377b52edf0af94d13e31594a785eac20 1 parent ed19821
sitedyno authored July 21, 2012 markstory committed July 22, 2012
56  lib/Cake/Test/Case/Utility/SecurityTest.php
@@ -68,6 +68,46 @@ public function testValidateAuthKey() {
68 68
 	}
69 69
 
70 70
 /**
  71
+ * testHashInvalidSalt method
  72
+ *
  73
+ * @expectedException PHPUnit_Framework_Error
  74
+ * @return void
  75
+ */
  76
+	public function testHashInvalidSalt() {
  77
+		$result = Security::hash('someKey', 'blowfish', true);
  78
+	}
  79
+
  80
+/**
  81
+ * testHashAnotherInvalidSalt
  82
+ *
  83
+ * @expectedException PHPUnit_Framework_Error
  84
+ * @return void
  85
+ */
  86
+	public function testHashAnotherInvalidSalt() {
  87
+		$result = Security::hash('someKey', 'blowfish', '$1$lksdjoijfaoijs');
  88
+	}
  89
+
  90
+/**
  91
+ * testHashYetAnotherInvalidSalt
  92
+ *
  93
+ * @expectedException PHPUnit_Framework_Error
  94
+ * @return void
  95
+ */
  96
+	public function testHashYetAnotherInvalidSalt() {
  97
+		$result = Security::hash('someKey', 'blowfish', '$2a$10$123');
  98
+	}
  99
+
  100
+/**
  101
+ * testHashInvalidCost method
  102
+ *
  103
+ * @expectedException PHPUnit_Framework_Error
  104
+ * @return void
  105
+ */
  106
+	public function testHashInvalidCost() {
  107
+		Security::setCost(1000);
  108
+		$result = Security::hash('somekey', 'blowfish', false);
  109
+	}
  110
+/**
71 111
  * testHash method
72 112
  *
73 113
  * @return void
@@ -112,6 +152,22 @@ public function testHash() {
112 152
 			$this->assertSame(strlen(Security::hash($key, 'sha256', true)), 64);
113 153
 		}
114 154
 
  155
+		$hashType = 'blowfish';
  156
+		Security::setHash($hashType);
  157
+		Security::setCost(10); // ensure default cost
  158
+		$this->assertSame(Security::$hashType, $hashType);
  159
+		$this->assertSame(strlen(Security::hash($key, null, false)), 60);
  160
+
  161
+		$password = $submittedPassword = $key;
  162
+		$storedPassword = Security::hash($password);
  163
+
  164
+		$hashedPassword = Security::hash($submittedPassword, null, $storedPassword);
  165
+		$this->assertSame($storedPassword, $hashedPassword);
  166
+
  167
+		$submittedPassword = 'someOtherKey';
  168
+		$hashedPassword = Security::hash($submittedPassword, null, $storedPassword);
  169
+		$this->assertNotSame($storedPassword, $hashedPassword);
  170
+
115 171
 		Security::setHash($_hashType);
116 172
 	}
117 173
 
111  lib/Cake/Utility/Security.php
@@ -34,6 +34,13 @@ class Security {
34 34
 	public static $hashType = null;
35 35
 
36 36
 /**
  37
+ * Default cost
  38
+ *
  39
+ * @var string
  40
+ */
  41
+	public static $hashCost = '10';
  42
+
  43
+/**
37 44
  * Get allowed minutes of inactivity based on security level.
38 45
  *
39 46
  * @return integer Allowed inactivity in minutes
@@ -76,14 +83,26 @@ public static function validateAuthKey($authKey) {
76 83
 /**
77 84
  * Create a hash from string using given method.
78 85
  * Fallback on next available method.
  86
+ * If you are using blowfish, for comparisons simply pass the originally hashed
  87
+ * string as the salt (the salt is prepended to the hash and php handles the
  88
+ * parsing automagically. Do NOT use a constant salt for blowfish.
79 89
  *
80 90
  * @param string $string String to hash
81 91
  * @param string $type Method to use (sha1/sha256/md5)
82  
- * @param boolean $salt If true, automatically appends the application's salt
83  
- *     value to $string (Security.salt)
  92
+ * @param mixed $salt If true, automatically appends the application's salt
  93
+ *     value to $string (Security.salt). If you are using blowfish the salt
  94
+ *     must be false or a previously generated salt.
84 95
  * @return string Hash
85 96
  */
86 97
 	public static function hash($string, $type = null, $salt = false) {
  98
+		if (empty($type)) {
  99
+			$type = self::$hashType;
  100
+		}
  101
+		$type = strtolower($type);
  102
+
  103
+		if ($type === 'blowfish') {
  104
+			return self::_crypt($string, $type, $salt);
  105
+		}
87 106
 		if ($salt) {
88 107
 			if (is_string($salt)) {
89 108
 				$string = $salt . $string;
@@ -92,11 +111,6 @@ public static function hash($string, $type = null, $salt = false) {
92 111
 			}
93 112
 		}
94 113
 
95  
-		if (empty($type)) {
96  
-			$type = self::$hashType;
97  
-		}
98  
-		$type = strtolower($type);
99  
-
100 114
 		if ($type == 'sha1' || $type == null) {
101 115
 			if (function_exists('sha1')) {
102 116
 				$return = sha1($string);
@@ -119,7 +133,7 @@ public static function hash($string, $type = null, $salt = false) {
119 133
  * Sets the default hash method for the Security object.  This affects all objects using
120 134
  * Security::hash().
121 135
  *
122  
- * @param string $hash Method to use (sha1/sha256/md5)
  136
+ * @param string $hash Method to use (sha1/sha256/md5/blowfish)
123 137
  * @return void
124 138
  * @see Security::hash()
125 139
  */
@@ -128,6 +142,16 @@ public static function setHash($hash) {
128 142
 	}
129 143
 
130 144
 /**
  145
+ * Sets the cost for they blowfish hash method.
  146
+ *
  147
+ * @param integer $cost Valid values are 4-31
  148
+ * @return void
  149
+ */
  150
+	public static function setCost($cost) {
  151
+		self::$hashCost = $cost;
  152
+	}
  153
+
  154
+/**
131 155
  * Encrypts/Decrypts a text using the given key.
132 156
  *
133 157
  * @param string $text Encrypted string to decrypt, normal string to encrypt
@@ -189,4 +213,75 @@ public static function rijndael($text, $key, $operation) {
189 213
 		return $out;
190 214
 	}
191 215
 
  216
+/**
  217
+ * Generates a pseudo random salt suitable for use with php's crypt() function.
  218
+ * The salt length should not exceed 27. The salt will be composed of
  219
+ * [./0-9A-Za-z]{$length}.
  220
+ *
  221
+ * @param integer $length The length of the returned salt
  222
+ * @return string The generated salt
  223
+ */
  224
+	public static function salt($length = 22) {
  225
+		return substr(str_replace('+', '.', base64_encode(sha1(uniqid(Configure::read('Security.salt'), true), true))), 0, $length);
  226
+	}
  227
+
  228
+/**
  229
+ * One way encryption using php's crypt() function.
  230
+ *
  231
+ * @param string $password The string to be encrypted.
  232
+ * @param string $type The encryption method to use (blowfish)
  233
+ * @param mixed $salt false to generate a new salt or an existing salt.
  234
+ */
  235
+	protected static function _crypt($password, $type = null, $salt = false) {
  236
+		$options = array(
  237
+			'saltFormat' => array(
  238
+				'blowfish' => '$2a$%s$%s',
  239
+			),
  240
+			'saltLength' => array(
  241
+				'blowfish' => 22,
  242
+			),
  243
+			'costLimits' => array(
  244
+				'blowfish' => array(4, 31),
  245
+			)
  246
+		);
  247
+		extract($options);
  248
+		if ($type === null) {
  249
+			$hashType = self::$hashType;
  250
+		} else {
  251
+			$hashType = $type;
  252
+		}
  253
+		$cost = self::$hashCost;
  254
+		if ($salt === false) {
  255
+			if (isset($costLimits[$hashType]) && ($cost < $costLimits[$hashType][0] || $cost > $costLimits[$hashType][1])) {
  256
+				trigger_error(__d(
  257
+					'cake_dev',
  258
+					'When using %s you must specify a cost between %s and %s',
  259
+					array(
  260
+						$hashType,
  261
+						$costLimits[$hashType][0],
  262
+						$costLimits[$hashType][1]
  263
+					)
  264
+				), E_USER_WARNING);
  265
+				return '';
  266
+			}
  267
+			$vspArgs = array();
  268
+			$salt = self::salt($saltLength[$hashType]);
  269
+			if ($hashType === 'blowfish') {
  270
+				$bfCost = chr(ord('0') + $cost / 10);
  271
+				$bfCost .= chr(ord('0') + $cost % 10);
  272
+				$vspArgs[] = $bfCost;
  273
+			}
  274
+			$vspArgs[] = $salt;
  275
+			$salt = vsprintf($saltFormat[$hashType], $vspArgs);
  276
+		} elseif ($salt === true || strpos($salt, '$2a$') !== 0 || strlen($salt) < 29) {
  277
+			trigger_error(__d(
  278
+				'cake_dev',
  279
+				'Invalid salt: %s for %s Please visit http://www.php.net/crypt and read the appropriate section for building %s salts.',
  280
+				array($salt, $hashType, $hashType)
  281
+			), E_USER_WARNING);
  282
+			return '';
  283
+		}
  284
+		return crypt($password, $salt);
  285
+	}
  286
+
192 287
 }

0 notes on commit 434d3a7

Thiago Belem

What about the == or = padding from base64? http://en.wikipedia.org/wiki/Base64#Padding

The = isn't an allowed character to the blowfish salt.

Mark Story

Good point, I'll update that.

sitedyno

The original generates salts up to 27 characters w/o any "=" (this is specified in the comments). The blowfish salt is 22 characters and all the other salts used by crypt are shorter.

Please sign in to comment.
Something went wrong with that request. Please try again.