Completed
Pull Request — master (#5741)
by Damian
12:40
created

PasswordEncryptor_Blowfish::set_cost()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace SilverStripe\Security;
4
5
6
use SilverStripe\ORM\DB;
7
use Config;
8
use ReflectionClass;
9
use Exception;
10
11
/**
12
 * Allows pluggable password encryption.
13
 * By default, this might be PHP's integrated sha1()
14
 * function, but could also be more sophisticated to facilitate
15
 * password migrations from other systems.
16
 * Use {@link register()} to add new implementations.
17
 *
18
 * Used in {@link Security::encrypt_password()}.
19
 *
20
 * @package framework
21
 * @subpackage security
22
 */
23
abstract class PasswordEncryptor {
24
25
	/**
26
	 * @var array
27
	 * @config
28
	 */
29
	private static $encryptors = array();
30
31
	/**
32
	 * @return array Map of encryptor code to the used class.
33
	 */
34
	public static function get_encryptors() {
35
		return Config::inst()->get('SilverStripe\\Security\\PasswordEncryptor', 'encryptors');
36
	}
37
38
	/**
39
	 * @param String $algorithm
40
	 * @return PasswordEncryptor
41
	 * @throws PasswordEncryptor_NotFoundException
42
	 */
43
	public static function create_for_algorithm($algorithm) {
44
		$encryptors = self::get_encryptors();
45
		if(!isset($encryptors[$algorithm])) {
46
			throw new PasswordEncryptor_NotFoundException(
47
				sprintf('No implementation found for "%s"', $algorithm)
48
			);
49
		}
50
51
		$class=key($encryptors[$algorithm]);
52
		if(!class_exists($class)) {
53
			throw new PasswordEncryptor_NotFoundException(
54
				sprintf('No class found for "%s"', $class)
55
			);
56
57
		}
58
		$refClass = new ReflectionClass($class);
59
		if(!$refClass->getConstructor()) {
60
			return new $class;
61
		}
62
63
		$arguments = $encryptors[$algorithm];
64
		return($refClass->newInstanceArgs($arguments));
65
	}
66
67
	/**
68
	 * Return a string value stored in the {@link Member->Password} property.
69
	 * The password should be hashed with {@link salt()} if applicable.
70
	 *
71
	 * @param String $password Cleartext password to be hashed
72
	 * @param String $salt (Optional)
73
	 * @param Member $member (Optional)
74
	 * @return String Maximum of 512 characters.
75
	 */
76
	abstract public function encrypt($password, $salt = null, $member = null);
77
78
	/**
79
	 * Return a string value stored in the {@link Member->Salt} property.
80
	 *
81
	 * @uses RandomGenerator
82
	 *
83
	 * @param string $password Cleartext password
84
	 * @param Member $member (Optional)
85
	 * @return string Maximum of 50 characters
86
	 */
87
	public function salt($password, $member = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $password is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $member is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
88
		$generator = new RandomGenerator();
89
		return substr($generator->randomToken('sha1'), 0, 50);
90
	}
91
92
	/**
93
	 * This usually just returns a strict string comparison,
94
	 * but is necessary for retain compatibility with password hashed
95
	 * with flawed algorithms - see {@link PasswordEncryptor_LegacyPHPHash} and
96
	 * {@link PasswordEncryptor_Blowfish}
97
	 *
98
	 * @param string $hash
99
	 * @param string $password
100
	 * @param string $salt
101
	 * @param Member $member
102
	 * @return bool
103
	 */
104
	public function check($hash, $password, $salt = null, $member = null) {
105
		return $hash === $this->encrypt($password, $salt, $member);
106
	}
107
}
108
109
/**
110
 * Blowfish encryption - this is the default from SilverStripe 3.
111
 * PHP 5.3+ will provide a php implementation if there is no system
112
 * version available.
113
 *
114
 * @package framework
115
 * @subpackage security
116
 */
117
class PasswordEncryptor_Blowfish extends PasswordEncryptor {
118
	/**
119
	 * Cost of encryption.
120
	 * Higher costs will increase security, but also increase server load.
121
	 * If you are using basic auth, you may need to decrease this as encryption
122
	 * will be run on every request.
123
	 * The two digit cost parameter is the base-2 logarithm of the iteration
124
	 * count for the underlying Blowfish-based hashing algorithmeter and must
125
	 * be in range 04-31, values outside this range will cause crypt() to fail.
126
	 */
127
	protected static $cost = 10;
128
129
	/**
130
	 * Sets the cost of the blowfish algorithm.
131
	 * See {@link PasswordEncryptor_Blowfish::$cost}
132
	 * Cost is set as an integer but
133
	 * Ensure that set values are from 4-31
134
	 *
135
	 * @param int $cost range 4-31
136
	 * @return null
137
	 */
138
	public static function set_cost($cost) {
139
		self::$cost = max(min(31, $cost), 4);
140
	}
141
142
	/**
143
	 * Gets the cost that is set for the blowfish algorithm
144
	 *
145
	 * @return int
146
	 */
147
	public static function get_cost() {
148
		return self::$cost;
149
	}
150
151
	public function encrypt($password, $salt = null, $member = null) {
152
		// See: http://nz.php.net/security/crypt_blowfish.php
153
		// There are three version of the algorithm - y, a and x, in order
154
		// of decreasing security. Attempt to use the strongest version.
155
		$encryptedPassword = $this->encryptY($password, $salt);
156
		if(!$encryptedPassword) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $encryptedPassword of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
157
			$encryptedPassword = $this->encryptA($password, $salt);
158
		}
159
		if(!$encryptedPassword) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $encryptedPassword of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
160
			$encryptedPassword = $this->encryptX($password, $salt);
161
		}
162
163
		// We *never* want to generate blank passwords. If something
164
		// goes wrong, throw an exception.
165
		if(strpos($encryptedPassword, '$2') === false) {
166
			throw new PasswordEncryptor_EncryptionFailed('Blowfish password encryption failed.');
167
		}
168
169
		return $encryptedPassword;
170
	}
171
172
	public function encryptX($password, $salt) {
173
		$methodAndSalt = '$2x$' . $salt;
174
		$encryptedPassword = crypt($password, $methodAndSalt);
175
176
		if(strpos($encryptedPassword, '$2x$') === 0) {
177
			return $encryptedPassword;
178
		}
179
180
		// Check if system a is actually x, and if available, use that.
181
		if($this->checkAEncryptionLevel() == 'x') {
182
			$methodAndSalt = '$2a$' . $salt;
183
			$encryptedPassword = crypt($password, $methodAndSalt);
184
185
			if(strpos($encryptedPassword, '$2a$') === 0) {
186
				$encryptedPassword = '$2x$' . substr($encryptedPassword, strlen('$2a$'));
187
				return $encryptedPassword;
188
			}
189
		}
190
191
		return false;
192
	}
193
194
	public function encryptY($password, $salt) {
195
		$methodAndSalt = '$2y$' . $salt;
196
		$encryptedPassword = crypt($password, $methodAndSalt);
197
198
		if(strpos($encryptedPassword, '$2y$') === 0) {
199
			return $encryptedPassword;
200
		}
201
202
		// Check if system a is actually y, and if available, use that.
203
		if($this->checkAEncryptionLevel() == 'y') {
204
			$methodAndSalt = '$2a$' . $salt;
205
			$encryptedPassword = crypt($password, $methodAndSalt);
206
207
			if(strpos($encryptedPassword, '$2a$') === 0) {
208
				$encryptedPassword = '$2y$' . substr($encryptedPassword, strlen('$2a$'));
209
				return $encryptedPassword;
210
			}
211
		}
212
213
		return false;
214
	}
215
216
	public function encryptA($password, $salt) {
217
		if($this->checkAEncryptionLevel() == 'a') {
218
			$methodAndSalt = '$2a$' . $salt;
219
			$encryptedPassword = crypt($password, $methodAndSalt);
220
221
			if(strpos($encryptedPassword, '$2a$') === 0) {
222
				return $encryptedPassword;
223
			}
224
		}
225
226
		return false;
227
	}
228
229
	/**
230
	 * The algorithm returned by using '$2a$' is not consistent -
231
	 * it might be either the correct (y), incorrect (x) or mostly-correct (a)
232
	 * version, depending on the version of PHP and the operating system,
233
	 * so we need to test it.
234
	 */
235
	public function checkAEncryptionLevel() {
236
		// Test hashes taken from
237
		// http://cvsweb.openwall.com/cgi/cvsweb.cgi/~checkout~/Owl/packages/glibc
238
		//    /crypt_blowfish/wrapper.c?rev=1.9.2.1;content-type=text%2Fplain
239
		$xOrY = crypt("\xff\xa334\xff\xff\xff\xa3345", '$2a$05$/OK.fbVrR/bpIqNJ5ianF.o./n25XVfn6oAPaUvHe.Csk4zRfsYPi')
240
			== '$2a$05$/OK.fbVrR/bpIqNJ5ianF.o./n25XVfn6oAPaUvHe.Csk4zRfsYPi';
241
		$yOrA = crypt("\xa3", '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq')
242
			== '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq';
243
244
		if($xOrY && $yOrA) {
245
			return 'y';
246
		} elseif($xOrY) {
247
			return 'x';
248
		} elseif($yOrA) {
249
			return 'a';
250
		}
251
252
		return 'unknown';
253
	}
254
255
	/**
256
	 * self::$cost param is forced to be two digits with leading zeroes for ints 4-9
257
	 *
258
	 * @param string $password
259
	 * @param Member $member
260
	 * @return string
261
	 */
262
	public function salt($password, $member = null) {
263
		$generator = new RandomGenerator();
264
		return sprintf('%02d', self::$cost) . '$' . substr($generator->randomToken('sha1'), 0, 22);
265
	}
266
267
	public function check($hash, $password, $salt = null, $member = null) {
268
		if(strpos($hash, '$2y$') === 0) {
269
			return $hash === $this->encryptY($password, $salt);
270
		} elseif(strpos($hash, '$2a$') === 0) {
271
			return $hash === $this->encryptA($password, $salt);
272
		} elseif(strpos($hash, '$2x$') === 0) {
273
			return $hash === $this->encryptX($password, $salt);
274
		}
275
276
		return false;
277
	}
278
}
279
280
/**
281
 * Encryption using built-in hash types in PHP.
282
 * Please note that the implemented algorithms depend on the PHP
283
 * distribution and architecture.
284
 *
285
 * @package framework
286
 * @subpackage security
287
 */
288
class PasswordEncryptor_PHPHash extends PasswordEncryptor {
289
290
	protected $algorithm = 'sha1';
291
292
	/**
293
	 * @param string $algorithm A PHP built-in hashing algorithm as defined by hash_algos()
294
	 * @throws Exception
295
	 */
296
	public function __construct($algorithm) {
297
		if(!in_array($algorithm, hash_algos())) {
298
			throw new Exception(
299
				sprintf('Hash algorithm "%s" not found in hash_algos()', $algorithm)
300
			);
301
		}
302
303
		$this->algorithm = $algorithm;
304
	}
305
306
	/**
307
	 * @return string
308
	 */
309
	public function getAlgorithm() {
310
		return $this->algorithm;
311
	}
312
313
	public function encrypt($password, $salt = null, $member = null) {
314
		return hash($this->algorithm, $password . $salt);
315
	}
316
}
317
318
/**
319
 * Legacy implementation for SilverStripe 2.1 - 2.3,
320
 * which had a design flaw in password hashing that caused
321
 * the hashes to differ between architectures due to
322
 * floating point precision problems in base_convert().
323
 * See http://open.silverstripe.org/ticket/3004
324
 *
325
 * @package framework
326
 * @subpackage security
327
 */
328
class PasswordEncryptor_LegacyPHPHash extends PasswordEncryptor_PHPHash {
329
	public function encrypt($password, $salt = null, $member = null) {
330
		$password = parent::encrypt($password, $salt, $member);
331
332
		// Legacy fix: This shortening logic is producing unpredictable results.
333
		//
334
		// Convert the base of the hexadecimal password to 36 to make it shorter
335
		// In that way we can store also a SHA256 encrypted password in just 64
336
		// letters.
337
		return substr(base_convert($password, 16, 36), 0, 64);
338
	}
339
340
	public function check($hash, $password, $salt = null, $member = null) {
341
		// Due to flawed base_convert() floating poing precision,
342
		// only the first 10 characters are consistently useful for comparisons.
343
		return (substr($hash, 0, 10) === substr($this->encrypt($password, $salt, $member), 0, 10));
344
	}
345
}
346
347
/**
348
 * Uses MySQL's PASSWORD encryption. Requires an active DB connection.
349
 *
350
 * @package framework
351
 * @subpackage security
352
 */
353
class PasswordEncryptor_MySQLPassword extends PasswordEncryptor {
354
	public function encrypt($password, $salt = null, $member = null) {
355
		return DB::prepared_query("SELECT PASSWORD(?)", array($password))->value();
356
	}
357
358
	public function salt($password, $member = null) {
359
		return false;
360
	}
361
}
362
363
/**
364
 * Uses MySQL's OLD_PASSWORD encyrption. Requires an active DB connection.
365
 *
366
 * @package framework
367
 * @subpackage security
368
 */
369
class PasswordEncryptor_MySQLOldPassword extends PasswordEncryptor {
370
	public function encrypt($password, $salt = null, $member = null) {
371
		return DB::prepared_query("SELECT OLD_PASSWORD(?)", array($password))->value();
372
	}
373
374
	public function salt($password, $member = null) {
375
		return false;
376
	}
377
}
378
379
/**
380
 * Cleartext passwords (used in SilverStripe 2.1).
381
 * Also used when Security::$encryptPasswords is set to FALSE.
382
 * Not recommended.
383
 *
384
 * @package framework
385
 * @subpackage security
386
 */
387
class PasswordEncryptor_None extends PasswordEncryptor {
388
	public function encrypt($password, $salt = null, $member = null) {
389
		return $password;
390
	}
391
392
	public function salt($password, $member = null) {
393
		return false;
394
	}
395
}
396
397
/**
398
 * @package framework
399
 * @subpackage security
400
 */
401
class PasswordEncryptor_NotFoundException extends Exception {}
402
403
/**
404
 * @package framework
405
 * @subpackage security
406
 */
407
class PasswordEncryptor_EncryptionFailed extends Exception {}
408