From 2c5c2c101bee0d24e3c2cecb19aefd42fa4d79c8 Mon Sep 17 00:00:00 2001 From: tom-- Date: Sat, 26 Jul 2014 03:29:30 +0400 Subject: [PATCH 1/2] Fixes #4131: Security adjustments --- apps/advanced/common/models/User.php | 4 +- .../common/tests/templates/fixtures/user.php | 4 +- docs/guide/security-authentication.md | 9 +- docs/guide/security-passwords.md | 2 +- extensions/faker/README.md | 2 +- framework/CHANGELOG.md | 5 + framework/base/Security.php | 504 +++++++++++++-------- framework/web/Request.php | 2 +- tests/unit/framework/base/SecurityTest.php | 259 +++++++++-- 9 files changed, 538 insertions(+), 253 deletions(-) diff --git a/apps/advanced/common/models/User.php b/apps/advanced/common/models/User.php index 45afd6f..76c0926 100644 --- a/apps/advanced/common/models/User.php +++ b/apps/advanced/common/models/User.php @@ -159,7 +159,7 @@ class User extends ActiveRecord implements IdentityInterface */ public function generateAuthKey() { - $this->auth_key = Yii::$app->security->generateRandomKey(); + $this->auth_key = Yii::$app->security->generateRandomString(); } /** @@ -167,7 +167,7 @@ class User extends ActiveRecord implements IdentityInterface */ public function generatePasswordResetToken() { - $this->password_reset_token = Yii::$app->security->generateRandomKey() . '_' . time(); + $this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time(); } /** diff --git a/apps/advanced/common/tests/templates/fixtures/user.php b/apps/advanced/common/tests/templates/fixtures/user.php index b91ba40..87d382d 100644 --- a/apps/advanced/common/tests/templates/fixtures/user.php +++ b/apps/advanced/common/tests/templates/fixtures/user.php @@ -3,7 +3,7 @@ return [ 'username' => 'userName', 'auth_key' => function ($fixture, $faker, $index) { - $fixture['auth_key'] = Yii::$app->getSecurity()->generateRandomKey(); + $fixture['auth_key'] = Yii::$app->getSecurity()->generateRandomString(); return $fixture; }, @@ -13,7 +13,7 @@ return [ return $fixture; }, 'password_reset_token' => function ($fixture, $faker, $index) { - $fixture['password_reset_token'] = Yii::$app->getSecurity()->generateRandomKey() . '_' . time(); + $fixture['password_reset_token'] = Yii::$app->getSecurity()->generateRandomString() . '_' . time(); return $fixture; }, diff --git a/docs/guide/security-authentication.md b/docs/guide/security-authentication.md index 2c5d3f8..27f0f8f 100644 --- a/docs/guide/security-authentication.md +++ b/docs/guide/security-authentication.md @@ -64,15 +64,18 @@ class User extends ActiveRecord implements IdentityInterface } ``` -Two of the outlined methods are simple: `findIdentity` is provided with an ID value and returns a model instance associated with that ID. The `getId` method returns the ID itself. -Two of the other methods--`getAuthKey` and `validateAuthKey`--are used to provide extra security to the "remember me" cookie. The `getAuthKey` method should return a string that is unique for each user. You can create reliably create a unique string using `Yii::$app->getSecurity()->generateRandomKey()`. It's a good idea to also save this as part of the user's record: +Two of the outlined methods are simple: `findIdentity` is provided with an ID value and returns a model instance +associated with that ID. The `getId` method returns the ID itself. Two of the other methods – `getAuthKey` and +`validateAuthKey` – are used to provide extra security to the "remember me" cookie. The `getAuthKey` method should +return a string that is unique for each user. You can reliably create a unique string using +`Yii::$app->getSecurity()->generateRandomString()`. It's a good idea to also save this as part of the user's record: ```php public function beforeSave($insert) { if (parent::beforeSave($insert)) { if ($this->isNewRecord) { - $this->auth_key = Yii::$app->getSecurity()->generateRandomKey(); + $this->auth_key = Yii::$app->getSecurity()->generateRandomString(); } return true; } diff --git a/docs/guide/security-passwords.md b/docs/guide/security-passwords.md index b2fbb38..19796b6 100644 --- a/docs/guide/security-passwords.md +++ b/docs/guide/security-passwords.md @@ -42,7 +42,7 @@ Yii security helper makes generating pseudorandom data simple: ```php -$key = Yii::$app->getSecurity()->generateRandomKey(); +$key = Yii::$app->getSecurity()->generateRandomString(); ``` Note that you need to have the `openssl` extension installed in order to generate cryptographically secure random data. diff --git a/extensions/faker/README.md b/extensions/faker/README.md index 6a7f330..fc14819 100644 --- a/extensions/faker/README.md +++ b/extensions/faker/README.md @@ -78,7 +78,7 @@ return [ return $fixture; }, 'auth_key' => function ($fixture, $faker, $index) { - $fixture['auth_key'] = Yii::$app->getSecurity()->generateRandomKey(); + $fixture['auth_key'] = Yii::$app->getSecurity()->generateRandomString(); return $fixture; }, ]; diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index fd777f1..249c65a 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -149,6 +149,11 @@ Yii Framework 2 Change Log - Enh #4080: Added proper handling and support of the symlinked directories in `FileHelper`, added $options parameter in `FileHelper::removeDirectory()` (resurtm) - Enh #4086: changedAttributes of afterSave Event now contain old values (dizews) - Enh #4114: Added `Security::generateRandomBytes()`, improved tests (samdark) +- Enh #4131: Security adjustments (tom--) + - Added HKDF to `yii\base\Security`. + - Reverted auto fallback to PHP PBKDF2. + - Fixed PBKDF2 key truncation. + - Adjusted API. - Enh #4209: Added `beforeCopy`, `afterCopy`, `forceCopy` properties to AssetManager (cebe) - Enh #4297: Added check for DOM extension to requirements (samdark) - Enh #4317: Added `absoluteAuthTimeout` to yii\web\User (ivokund, nkovacs) diff --git a/framework/base/Security.php b/framework/base/Security.php index 5f69d00..0b594fa 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -15,21 +15,11 @@ use Yii; * * In particular, Security supports the following features: * - * - Encryption/decryption: [[encrypt()]] and [[decrypt()]] + * - Encryption/decryption: [[encryptByKey()]], [[decryptByKey()]], [[encryptByPassword()]] and [[decryptByPassword()]] + * - Key derivation using standard algorithms: [[pbkdf2()]] and [[hkdf()]] * - Data tampering prevention: [[hashData()]] and [[validateData()]] * - Password validation: [[generatePasswordHash()]] and [[validatePassword()]] * - * Additionally, Security provides [[getSecretKey()]] to support generating - * named secret keys. These secret keys, once generated, will be stored in a file - * and made available in future requests. - * - * This component provides several configuration parameters, which allow tuning your own balance - * between high security and high performance. - * - * > Tip: you may add several `Security` components with different configurations to your application, - * this allows usage of different encryption strategies for different use cases or migrate encrypted data - * from outdated strategy to the new one. - * * > Note: this class requires 'mcrypt' PHP extension. For the highest security level PHP version >= 5.5.0 is recommended. * * @author Qiang Xue @@ -40,114 +30,208 @@ use Yii; class Security extends Component { /** - * @var integer crypt block size in bytes. - * For AES-128, AES-192, block size is 128-bit (16 bytes). - * For AES-256, block size is 256-bit (32 bytes). - * Recommended value: 32 - */ - public $cryptBlockSize = 32; - /** - * @var integer crypt key size in bytes. - * For AES-192, key size is 192-bit (24 bytes). - * For AES-256, key size is 256-bit (32 bytes). - * Recommended value: 32 + * @var integer derivation iterations count. + * Only used when @see $deriveKeyStrategy = 'password'. Set as + * high as possible to hinder dictionary password attacks. */ - public $cryptKeySize = 32; + public $derivationIterations = 100000; /** - * @var string derivation hash algorithm name. - * Recommended value: 'sha256' + * @var string strategy, which should be used to generate password hash. + * Available strategies: + * - 'password_hash' - use of PHP `password_hash()` function with PASSWORD_DEFAULT algorithm. + * This option is recommended, but it requires PHP version >= 5.5.0 + * - 'crypt' - use PHP `crypt()` function. */ - public $derivationHash = 'sha256'; + public $passwordHashStrategy = 'crypt'; + + // AES has 128-bit block size and three key sizes: 128, 192 and 256 bits. + // mcrypt offers the Rijndael cipher with block sizes of 128, 192 and 256 + // bits but only the 128-bit Rijndael is standardized in AES. + // So to use AES in mycrypt, specify `'rijndael-128'` cipher and mcrypt + // chooses the appropriate AES based on the length of the supplied key. + const MCRYPT_CIPHER = 'rijndael-128'; + const MCRYPT_MODE = 'cbc'; + // Same size for encryption keys, auth keys and KDF salt + const KEY_SIZE = 16; + // Hash algorithm for key derivation. + const KDF_HASH = 'sha256'; + // Hash algorithm for authentication. + const MAC_HASH = 'sha256'; + // HKDF info value for auth keys + const AUTH_KEY_INFO = 'AuthorizationKey'; + + private $_cryptModule; + /** - * @var integer derivation iterations count. - * Recommended value: 1000000 + * Encrypts data using a password. + * Derives keys for encryption and authentication from the password using PBKDF2 and a random salt, + * which is deliberately slow to protect against dictionary attacks. Use [[encryptByKey()]] to + * encrypt fast using a cryptographic key rather than a password. Key derivation time is + * determined by [[$derivationIterations]], which should be set as high as possible. + * The encrypted data includes a keyed message authentication code (MAC) so there is no need + * to hash input or output data. + * > Note: Avoid encrypting with passwords wherever possible. Nothing can protect against + * poor-quality or compromosed passwords. + * @param string $data the data to encrypt + * @param string $password the password to use for encryption + * @return string the encrypted data + * @see decryptByPassword() + * @see encryptByKey() */ - public $derivationIterations = 1000000; + public function encryptByPassword($data, $password) + { + return $this->encrypt($data, true, $password, null); + } + /** - * @var string strategy, which should be used to derive a key for encryption. - * Available strategies: - * - 'pbkdf2' - PBKDF2 key derivation. This option is recommended, but it requires PHP version >= 5.5.0 - * - 'hmac' - HMAC hash key derivation. + * Encrypts data using a cyptograhic key. + * Derives keys for encryption and authentication from the input key using HKDF and a random salt, + * which is very fast relative to [[encryptByPassword()]]. The input key must be properly + * random -- use [[generateRandomKey()]] to generate keys. + * The encrypted data includes a keyed message authentication code (MAC) so there is no need + * to hash input or output data. + * @param string $data the data to encrypt + * @param string $inputKey the input to use for encryption and authentication + * @param string $info optional context and application specific information, see [[hkdf()]] + * @return string the encrypted data + * @see decryptByPassword() + * @see encryptByKey() */ - public $deriveKeyStrategy = 'hmac'; + public function encryptByKey($data, $inputKey, $info = null) + { + return $this->encrypt($data, false, $inputKey, $info); + } + /** - * @var string strategy, which should be used to generate password hash. - * Available strategies: - * - 'password_hash' - use of PHP `password_hash()` function with PASSWORD_DEFAULT algorithm. This option is recommended, - * but it requires PHP version >= 5.5.0 - * - 'crypt' - use PHP `crypt()` function. + * Verifies and decrypts data encrypted with [[encryptByPassword()]]. + * @param string $data the encrypted data to decrypt + * @param string $password the password to use for decryption + * @return bool|string the decrypted data or false on authentication failure + * @see encryptByPassword() */ - public $passwordHashStrategy = 'crypt'; + public function decryptByPassword($data, $password) + { + return $this->decrypt($data, true, $password, null); + } + /** - * @var boolean whether to generate unique salt while deriving encryption key. - * If enabled (recommended) this option increases encrypted text length, but provide more security. - * If disabled this option reduces encrypted text length, but also reduces security. + * Verifies and decrypts data encrypted with [[encryptByPassword()]]. + * @param string $data the encrypted data to decrypt + * @param string $inputKey the input to use for encryption and authentication + * @param string $info optional context and application specific information, see [[hkdf()]] + * @return bool|string the decrypted data or false on authentication failure + * @see encryptByKey() */ - public $useDeriveKeyUniqueSalt = true; + public function decryptByKey($data, $inputKey, $info = null) + { + return $this->decrypt($data, false, $inputKey, $info); + } + /** - * @var string the path or alias of a file that stores the secret keys automatically generated by [[getSecretKey()]]. - * The file must be writable by Web server process. It contains a JSON hash of key names and key values. + * Initializes the mcrypt module. + * @return resource the mcrypt module handle. + * @throws InvalidConfigException if mcrypt extension is not installed + * @throws Exception if mcrypt initialization fails */ - public $secretKeyFile = '@runtime/keys.json'; + protected function getCryptModule() + { + if ($this->_cryptModule === null) { + if (!extension_loaded('mcrypt')) { + throw new InvalidConfigException('The mcrypt PHP extension is not installed.'); + } + + $this->_cryptModule = @mcrypt_module_open(self::MCRYPT_CIPHER, '', self::MCRYPT_MODE, ''); + if ($this->_cryptModule === false) { + $this->_cryptModule = null; + throw new Exception('Failed to initialize the mcrypt module.'); + } + } + + return $this->_cryptModule; + } + + public function __destruct() + { + if ($this->_cryptModule !== null) { + mcrypt_module_close($this->_cryptModule); + $this->_cryptModule = null; + } + } /** * Encrypts data. - * @param string $data data to be encrypted. - * @param string $password the encryption password + * @param string $data data to be encrypted + * @param bool $passwordBased set true to use password-based key derivation + * @param string $secret the encryption password or key + * @param string $info context/application specific information, e.g. a user ID + * Only used when $deriveKeyStrategy = 'key'. + * See RFC 5869 Section 3.2 @link https://tools.ietf.org/html/rfc5869 * @return string the encrypted data * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized * @see decrypt() */ - public function encrypt($data, $password) + protected function encrypt($data, $passwordBased, $secret, $info) { - $module = $this->openCryptModule(); + $module = $this->getCryptModule(); + + $keySalt = $this->generateRandomKey(self::KEY_SIZE); + if ($passwordBased) { + $key = $this->pbkdf2(self::KDF_HASH, $secret, $keySalt, $this->derivationIterations, self::KEY_SIZE); + } else { + $key = $this->hkdf(self::KDF_HASH, $secret, $keySalt, $info, self::KEY_SIZE); + } + $data = $this->addPadding($data); $ivSize = mcrypt_enc_get_iv_size($module); $iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM); - if ($this->useDeriveKeyUniqueSalt) { - $keySalt = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM); - $encrypted = $keySalt; - } else { - $keySalt = $iv; - $encrypted = ''; - } - $key = $this->deriveKey($password, $keySalt); mcrypt_generic_init($module, $key, $iv); - $encrypted .= $iv . mcrypt_generic($module, $data); + $encrypted = mcrypt_generic($module, $data); mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return $encrypted; + $authKey = $this->hkdf(self::KDF_HASH, $key, null, self::AUTH_KEY_INFO, self::KEY_SIZE); + $hashed = $this->hashData($iv . $encrypted, $authKey); + + /* + * Output: [keySalt][MAC][IV][ciphertext] + * - keySalt is KEY_SIZE bytes long + * - MAC: message authentication code, length same as the output of MAC_HASH + * - IV: initialization vector, length set by CRYPT_CIPHER and CRYPT_MODE, mcrypt_enc_get_iv_size() + */ + return $keySalt . $hashed; } /** - * Decrypts data - * @param string $data data to be decrypted. - * @param string $password the decryption password - * @return string the decrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * Decrypts data. + * @param string $data encrypted data to be decrypted. + * @param bool $passwordBased set true to use password-based key derivation + * @param string $secret the decryption password or key + * @param string $info context/application specific information, @see encrypt() + * @return bool|string the decrypted data or false on authentication failure * @see encrypt() */ - public function decrypt($data, $password) + protected function decrypt($data, $passwordBased, $secret, $info) { - if ($data === null) { - return null; + $keySalt = StringHelper::byteSubstr($data, 0, self::KEY_SIZE); + if ($passwordBased) { + $key = $this->pbkdf2(self::KDF_HASH, $secret, $keySalt, $this->derivationIterations, self::KEY_SIZE); + } else { + $key = $this->hkdf(self::KDF_HASH, $secret, $keySalt, $info, self::KEY_SIZE); } - $module = $this->openCryptModule(); + + $authKey = $this->hkdf(self::KDF_HASH, $key, null, self::AUTH_KEY_INFO, self::KEY_SIZE); + $data = $this->validateData(StringHelper::byteSubstr($data, self::KEY_SIZE, null), $authKey); + if ($data === false) { + return false; + } + + $module = $this->getCryptModule(); $ivSize = mcrypt_enc_get_iv_size($module); $iv = StringHelper::byteSubstr($data, 0, $ivSize); - $keySalt = $iv; - $encrypted = StringHelper::byteSubstr($data, $ivSize, StringHelper::byteLength($data)); - if ($this->useDeriveKeyUniqueSalt) { - $iv = StringHelper::byteSubstr($encrypted, 0, $ivSize); - $encrypted = StringHelper::byteSubstr($encrypted, $ivSize, StringHelper::byteLength($encrypted)); - } - $key = $this->deriveKey($password, $keySalt); + $encrypted = StringHelper::byteSubstr($data, $ivSize, null); mcrypt_generic_init($module, $key, $iv); $decrypted = mdecrypt_generic($module, $encrypted); mcrypt_generic_deinit($module); - mcrypt_module_close($module); return $this->stripPadding($decrypted); } @@ -159,7 +243,9 @@ class Security extends Component */ protected function addPadding($data) { - $pad = $this->cryptBlockSize - (StringHelper::byteLength($data) % $this->cryptBlockSize); + $module = $this->getCryptModule(); + $blockSize = mcrypt_enc_get_block_size($module); + $pad = $blockSize - (StringHelper::byteLength($data) % $blockSize); return $data . str_repeat(chr($pad), $pad); } @@ -182,70 +268,135 @@ class Security extends Component } /** - * Derives a key from the given password (PBKDF2). - * @param string $password the source password + * Derives a key from the given input key using the standard HKDF algorithm. + * Implements HKDF spcified in [RFC 5869](https://tools.ietf.org/html/rfc5869). + * Recommend use one of the SHA-2 hash algorithms: sha224, sha256, sha384 or sha512. + * @param string $algo a hash algorithm supported by `hash_hmac()`, e.g. 'SHA-256' + * @param string $inputKey the source key * @param string $salt the random salt - * @throws InvalidConfigException if unsupported derive key strategy is configured. + * @param string $info optional info to bind the derived key material to application- + * and context-specific information, e.g. a user ID or API version, see + * [RFC 5869](https://tools.ietf.org/html/rfc5869) + * @param int $length length of the output key in bytes. If 0, the output key is + * the length of the hash algorithm output. + * @throws InvalidParamException * @return string the derived key */ - protected function deriveKey($password, $salt) + public function hkdf($algo, $inputKey, $salt = null, $info = null, $length = 0) { - switch ($this->deriveKeyStrategy) { - case 'pbkdf2': - return $this->deriveKeyPbkdf2($password, $salt); - case 'hmac': - return $this->deriveKeyHmac($password, $salt); - default: - throw new InvalidConfigException("Unknown derive key strategy '{$this->deriveKeyStrategy}'"); + $test = @hash_hmac($algo, '', '', true); + if (!$test) { + throw new InvalidParamException('Failed to generate HMAC with hash algorithm: ' . $algo); + } + $hashLength = StringHelper::byteLength($test); + if (is_string($length) && preg_match('{^\d{1,16}$}', $length)) { + $length = (int) $length; + } + if (!is_integer($length) || $length < 0 || $length > 255 * $hashLength) { + throw new InvalidParamException('Invalid length'); + } + $blocks = $length !== 0 ? ceil($length / $hashLength) : 1; + + if ($salt === null) { + $salt = str_repeat("\0", $hashLength); + } + $prKey = hash_hmac($algo, $inputKey, $salt, true); + + $hmac = ''; + $outputKey = ''; + for ($i = 1; $i <= $blocks; $i++) { + $hmac = hash_hmac($algo, $hmac . $info . chr($i), $prKey, true); + $outputKey .= $hmac; } + + if ($length !== 0) { + $outputKey = StringHelper::byteSubstr($outputKey, 0, $length); + } + return $outputKey; } /** - * Derives a key from the given password using PBKDF2. + * Derives a key from the given password using the standard PBKDF2 algorithm. + * Implements HKDF2 specified in [RFC 2898](http://tools.ietf.org/html/rfc2898#section-5.2) + * Recommend use one of the SHA-2 hash algorithms: sha224, sha256, sha384 or sha512. + * @param string $algo a hash algorithm supported by `hash_hmac()`, e.g. 'SHA-256' * @param string $password the source password * @param string $salt the random salt - * @throws InvalidConfigException if environment does not allows PBKDF2. + * @param int $iterations the number of iterations of the hash algorithm. Set as high as + * possible to hinder dictionary password attacks. + * @param int $length length of the output key in bytes. If 0, the output key is + * the length of the hash algorithm output. + * @throws InvalidParamException * @return string the derived key */ - protected function deriveKeyPbkdf2($password, $salt) + public function pbkdf2($algo, $password, $salt, $iterations, $length = 0) { if (function_exists('hash_pbkdf2')) { - return hash_pbkdf2($this->derivationHash, $password, $salt, $this->derivationIterations, $this->cryptKeySize, true); - } else { - throw new InvalidConfigException('Security::$deriveKeyStrategy is set to "pbkdf2", which requires PHP >= 5.5.0. Either upgrade your run-time environment or use another strategy.'); + $outputKey = hash_pbkdf2($algo, $password, $salt, $iterations, $length, true); + if ($outputKey === false) { + throw new InvalidParamException('Invalid parameters to hash_pbkdf2()'); + } + return $outputKey; } - } - /** - * Derives a key from the given password using HMAC. - * @param string $password the source password - * @param string $salt the random salt - * @return string the derived key - */ - protected function deriveKeyHmac($password, $salt) - { - $hmac = hash_hmac($this->derivationHash, $salt . pack('N', 1), $password, true); - $xorsum = $hmac; - for ($i = 1; $i < $this->derivationIterations; $i++) { - $hmac = hash_hmac($this->derivationHash, $hmac, $password, true); - $xorsum ^= $hmac; + // todo: is there a nice way to reduce the code repetition in hkdf() and pbkdf2()? + $test = @hash_hmac($algo, '', '', true); + if (!$test) { + throw new InvalidParamException('Failed to generate HMAC with hash algorithm: ' . $algo); + } + if (is_string($iterations) && preg_match('{^\d{1,16}$}', $iterations)) { + $iterations = (int) $iterations; + } + if (!is_integer($iterations) || $iterations < 1) { + throw new InvalidParamException('Invalid iterations'); + } + if (is_string($length) && preg_match('{^\d{1,16}$}', $length)) { + $length = (int) $length; + } + if (!is_integer($length) || $length < 0) { + throw new InvalidParamException('Invalid length'); } - return substr($xorsum, 0, $this->cryptKeySize); + $hashLength = StringHelper::byteLength($test); + $blocks = $length !== 0 ? ceil($length / $hashLength) : 1; + + $outputKey = ''; + for ($j = 1; $j <= $blocks; $j++) { + $hmac = hash_hmac($algo, $salt . pack('N', $j), $password, true); + $xorsum = $hmac; + for ($i = 1; $i < $iterations; $i++) { + $hmac = hash_hmac($algo, $hmac, $password, true); + $xorsum ^= $hmac; + } + $outputKey .= $xorsum; + } + + if ($length !== 0) { + $outputKey = StringHelper::byteSubstr($outputKey, 0, $length); + } + return $outputKey; } /** * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. + * There is no need to hash inputs or outputs of [[encryptByKey()]] or [[encryptByPassword()]] + * as those methods perform the task. * @param string $data the data to be protected - * @param string $key the secret key to be used for generating hash - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. + * @param string $key the secret key to be used for generating hash. Should be a secure + * cryptographic key. + * @throws InvalidConfigException * @return string the data prefixed with the keyed hash * @see validateData() - * @see getSecretKey() + * @see generateRandomKey() + * @see hkdf() + * @see pbkdf2() */ - public function hashData($data, $key, $algorithm = 'sha256') + public function hashData($data, $key) { - return hash_hmac($algorithm, $data, $key) . $data; + $hash = hash_hmac(self::MAC_HASH, $data, $key, true); + if (!$hash) { + throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . self::MAC_HASH); + } + return $hash . $data; } /** @@ -253,21 +404,24 @@ class Security extends Component * @param string $data the data to be validated. The data must be previously * generated by [[hashData()]]. * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" * function to see the supported hashing algorithms on your system. This must be the same * as the value passed to [[hashData()]] when generating the hash for the data. + * @throws InvalidConfigException * @return string the real data with the hash stripped off. False if the data is tampered. * @see hashData() */ - public function validateData($data, $key, $algorithm = 'sha256') + public function validateData($data, $key) { - $hashSize = StringHelper::byteLength(hash_hmac($algorithm, 'test', $key)); - $n = StringHelper::byteLength($data); - if ($n >= $hashSize) { - $hash = StringHelper::byteSubstr($data, 0, $hashSize); - $pureData = StringHelper::byteSubstr($data, $hashSize, $n - $hashSize); + $test = @hash_hmac(self::MAC_HASH, '', '', true); + if (!$test) { + throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . self::MAC_HASH); + } + $hashLength = StringHelper::byteLength($test); + if (StringHelper::byteLength($data) >= $hashLength) { + $hash = StringHelper::byteSubstr($data, 0, $hashLength); + $pureData = StringHelper::byteSubstr($data, $hashLength, null); - $calculatedHash = hash_hmac($algorithm, $pureData, $key); + $calculatedHash = hash_hmac(self::MAC_HASH, $pureData, $key, true); if ($this->compareString($hash, $calculatedHash)) { return $pureData; @@ -279,32 +433,6 @@ class Security extends Component } } - private $_keys; - - /** - * Returns a secret key associated with the specified name. - * If the secret key does not exist, it will be automatically generated and saved in [[secretKeyFile]]. - * @param string $name the name that is associated with the secret key - * @param integer $length the length of the key that should be generated if not exists - * @param boolean $regenerate whether to regenerate a secret if it already exists - * @return string the secret key associated with the specified name - */ - public function getSecretKey($name, $length = 32, $regenerate = false) - { - $keyFile = Yii::getAlias($this->secretKeyFile); - - if ($this->_keys === null) { - $this->_keys = is_file($keyFile) ? json_decode(file_get_contents($keyFile), true) : []; - } - - if (!isset($this->_keys[$name]) || $regenerate) { - $this->_keys[$name] = $this->generateRandomKey($length); - file_put_contents($keyFile, json_encode($this->_keys)); - } - - return $this->_keys[$name]; - } - /** * Generates specified number of random bytes. * Note that output may not be ASCII. @@ -314,7 +442,7 @@ class Security extends Component * @throws Exception on failure. * @return string the generated random bytes */ - public function generateRandomBytes($length = 32) + public function generateRandomKey($length = 32) { if (!extension_loaded('mcrypt')) { throw new InvalidConfigException('The mcrypt PHP extension is not installed.'); @@ -328,43 +456,24 @@ class Security extends Component /** * Generates a random string of specified length. - * The string generated matches [A-Za-z0-9_.-]+ + * The string generated matches [A-Za-z0-9_-]+ and is transparent to URL-encoding. * * @param integer $length the length of the key in characters * @throws Exception Exception on failure. * @return string the generated random key */ - public function generateRandomKey($length = 32) + public function generateRandomString($length = 32) { - $bytes = $this->generateRandomBytes($length); - return strtr(StringHelper::byteSubstr(base64_encode($bytes), 0, $length), '+/=', '_-.'); - } - - /** - * Opens the mcrypt module. - * @return resource the mcrypt module handle. - * @throws InvalidConfigException if mcrypt extension is not installed - * @throws Exception if mcrypt initialization fails - */ - protected function openCryptModule() - { - if (!extension_loaded('mcrypt')) { - throw new InvalidConfigException('The mcrypt PHP extension is not installed.'); - } - // AES version depending on crypt block size - $algorithmName = 'rijndael-' . ($this->cryptBlockSize * 8); - $module = @mcrypt_module_open($algorithmName, '', 'cbc', ''); - if ($module === false) { - throw new Exception('Failed to initialize the mcrypt module.'); - } - - return $module; + $bytes = $this->generateRandomKey($length); + // '=' character(s) returned by base64_encode() are always discarded because + // they are guaranteed to be after position $length in the base64_encode() output. + return strtr(substr(base64_encode($bytes), 0, $length), '+/', '_-'); } /** * Generates a secure hash from a password and a random salt. * - * The generated hash can be stored in database (e.g. `CHAR(64) CHARACTER SET latin1` on MySQL). + * The generated hash can be stored in database. * Later when a password needs to be validated, the hash can be fetched and passed * to [[validatePassword()]]. For example, * @@ -387,11 +496,12 @@ class Security extends Component * the longer it takes to generate the hash and to verify a password against it. Higher cost * therefore slows down a brute-force attack. For best protection against brute for attacks, * set it to the highest value that is tolerable on production servers. The time taken to - * compute the hash doubles for every increment by one of $cost. So, for example, if the - * hash takes 1 second to compute when $cost is 14 then then the compute time varies as - * 2^($cost - 14) seconds. + * compute the hash doubles for every increment by one of $cost. * @throws Exception on bad password parameter or cost parameter - * @return string The password hash string, ASCII and not longer than 64 characters. + * @throws InvalidConfigException + * @return string The password hash string. When [[passwordHashStrategy]] is set to 'crypt', + * the output is alwaus 60 ASCII characters, when set to 'password_hash' the output length + * might increase in future versions of PHP (http://php.net/manual/en/function.password-hash.php) * @see validatePassword() */ public function generatePasswordHash($password, $cost = 13) @@ -401,12 +511,13 @@ class Security extends Component if (!function_exists('password_hash')) { throw new InvalidConfigException('Password hash key strategy "password_hash" requires PHP >= 5.5.0, either upgrade your environment or use another strategy.'); } + /** @noinspection PhpUndefinedConstantInspection */ return password_hash($password, PASSWORD_DEFAULT, ['cost' => $cost]); case 'crypt': $salt = $this->generateSalt($cost); $hash = crypt($password, $salt); - - if (!is_string($hash) || strlen($hash) < 32) { + // strlen() is safe since crypt() returns only ascii + if (!is_string($hash) || strlen($hash) !== 60) { throw new Exception('Unknown error occurred while generating hash.'); } return $hash; @@ -443,7 +554,7 @@ class Security extends Component case 'crypt': $test = crypt($password, $hash); $n = strlen($test); - if ($n < 32 || $n !== strlen($hash)) { + if ($n !== 60) { return false; } return $this->compareString($test, $hash); @@ -471,14 +582,9 @@ class Security extends Component throw new InvalidParamException('Cost must be between 4 and 31.'); } - // Get 20 * 8bits of random entropy - $rand = $this->generateRandomBytes(20); - - // Add the microtime for a little more entropy. - $rand .= microtime(true); - // Mix the bits cryptographically into a 20-byte binary string. - $rand = sha1($rand, true); - // Form the prefix that specifies Blowfish algorithm and cost parameter. + // Get a 20-byte random string + $rand = $this->generateRandomKey(20); + // Form the prefix that specifies Blowfish (bcrypt) algorithm and cost parameter. $salt = sprintf("$2y$%02d$", $cost); // Append the random salt data in the required base64 format. $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); @@ -496,10 +602,14 @@ class Security extends Component protected function compareString($expected, $actual) { // timing attack resistant approach: + $length = StringHelper::byteLength($expected); + if ($length !== StringHelper::byteLength($actual)) { + return false; + } $diff = 0; - for ($i = 0, $length = StringHelper::byteLength($actual); $i < $length; $i++) { + for ($i = 0; $i < $length; $i++) { $diff |= (ord($actual[$i]) ^ ord($expected[$i])); } return $diff === 0; } -} +} \ No newline at end of file diff --git a/framework/web/Request.php b/framework/web/Request.php index 4d4ae15..ee9ef43 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -1303,7 +1303,7 @@ class Request extends \yii\base\Request { $options = $this->csrfCookie; $options['name'] = $this->csrfParam; - $options['value'] = Yii::$app->getSecurity()->generateRandomKey(); + $options['value'] = Yii::$app->getSecurity()->generateRandomString(); return new Cookie($options); } diff --git a/tests/unit/framework/base/SecurityTest.php b/tests/unit/framework/base/SecurityTest.php index ff23083..c9f7b6f 100644 --- a/tests/unit/framework/base/SecurityTest.php +++ b/tests/unit/framework/base/SecurityTest.php @@ -24,7 +24,7 @@ class SecurityTest extends TestCase { parent::setUp(); $this->security = new Security(); - $this->security->derivationIterations = 100; // speed up test running + $this->security->derivationIterations = 1000; // speed up test running } // Tests : @@ -78,72 +78,239 @@ class SecurityTest extends TestCase $this->assertFalse($this->security->validatePassword('test', $hash)); } - /** - * Data provider for [[testEncrypt()]] - * @return array test data - */ - public function dataProviderEncrypt() + public function testEncryptByPassword() + { + $data = 'known data'; + $key = 'secret'; + + $encryptedData = $this->security->encryptByPassword($data, $key); + $this->assertFalse($data === $encryptedData); + $decryptedData = $this->security->decryptByPassword($encryptedData, $key); + $this->assertEquals($data, $decryptedData); + + $tampered = $encryptedData; + $tampered[20] = ~$tampered[20]; + $decryptedData = $this->security->decryptByPassword($tampered, $key); + $this->assertTrue(false === $decryptedData); + } + + public function testEncryptByKey() + { + $data = 'known data'; + $key = $this->security->generateRandomKey(80); + + $encryptedData = $this->security->encryptByKey($data, $key); + $this->assertFalse($data === $encryptedData); + $decryptedData = $this->security->decryptByKey($encryptedData, $key); + $this->assertEquals($data, $decryptedData); + + $encryptedData = $this->security->encryptByKey($data, $key, $key); + $decryptedData = $this->security->decryptByKey($encryptedData, $key, $key); + $this->assertEquals($data, $decryptedData); + + $tampered = $encryptedData; + $tampered[20] = ~$tampered[20]; + $decryptedData = $this->security->decryptByKey($tampered, $key); + $this->assertTrue(false === $decryptedData); + + $decryptedData = $this->security->decryptByKey($encryptedData, $key, $key . "\0"); + $this->assertTrue(false === $decryptedData); + } + + public function testGenerateRandomKey() + { + $length = 21; + $key = $this->security->generateRandomKey($length); + $this->assertEquals($length, strlen($key)); + } + + public function testGenerateRandomString() + { + $length = 21; + $key = $this->security->generateRandomString($length); + $this->assertEquals($length, strlen($key)); + $this->assertEquals(1, preg_match('/[A-Za-z0-9_-]+/', $key)); + } + + public function dataProviderPbkdf2() { return [ [ - 'hmac', - true, - false, + 'sha1', + 'password', + 'salt', + 1, + 20, + '0c60c80f961f0e71f3a9b524af6012062fe037a6' + ], + [ + 'sha1', + 'password', + 'salt', + 2, + 20, + 'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957' + ], + [ + 'sha1', + 'password', + 'salt', + 4096, + 20, + '4b007901b765489abead49d926f721d065a429c1' ], [ - 'hmac', - false, - false, + 'sha1', + 'password', + 'salt', + 16777216, + 20, + 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984' ], [ - 'pbkdf2', - true, - !function_exists('hash_pbkdf2') + 'sha1', + 'passwordPASSWORDpassword', + 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, + 25, + '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038' ], [ - 'pbkdf2', - false, - !function_exists('hash_pbkdf2') + 'sha1', + "pass\0word", + "sa\0lt", + 4096, + 16, + '56fa6aa75548099dcc37d7f03425e0c3' + ], + [ + 'sha256', + 'password', + 'salt', + 1, + 20, + '120fb6cffcf8b32c43e7225256c4f837a86548c9' + ], + [ + 'sha256', + "pass\0word", + "sa\0lt", + 4096, + 32, + '89b69d0516f829893c696226650a86878c029ac13ee276509d5ae58b6466a724' + ], + [ + 'sha256', + 'passwordPASSWORDpassword', + 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, + 40, + '348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9' ], ]; } /** - * @dataProvider dataProviderEncrypt + * @dataProvider dataProviderPbkdf2 * - * @param string $deriveKeyStrategy - * @param boolean $useDeriveKeyUniqueSalt - * @param boolean $isSkipped + * @param string $hash + * @param string $password + * @param string $salt + * @param int $iterations + * @param int $length + * @param string $okm */ - public function testEncrypt($deriveKeyStrategy, $useDeriveKeyUniqueSalt, $isSkipped) + public function testPbkdf2($hash, $password, $salt, $iterations, $length, $okm) { - if ($isSkipped) { - $this->markTestSkipped("Unable to test '{$deriveKeyStrategy}' derive key strategy"); - return; - } - $this->security->deriveKeyStrategy = $deriveKeyStrategy; - $this->security->useDeriveKeyUniqueSalt = $useDeriveKeyUniqueSalt; - - $data = 'known data'; - $key = 'secret'; - $encryptedData = $this->security->encrypt($data, $key); - $this->assertFalse($data === $encryptedData); - $decryptedData = $this->security->decrypt($encryptedData, $key); - $this->assertEquals($data, $decryptedData); + $this->security->derivationIterations = $iterations; + $DK = $this->security->pbkdf2($hash, $password, $salt, $iterations, $length); + $this->assertEquals($okm, bin2hex($DK)); } - public function testGenerateRandomBytes() + public function dataProviderDeriveKey() { - $length = 21; - $key = $this->security->generateRandomBytes($length); - $this->assertEquals($length, strlen($key)); + // See Appendix A in https://tools.ietf.org/html/rfc5869 + return [ + [ + 'Hash' => 'sha256', + 'IKM' => '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', + 'salt' => '000102030405060708090a0b0c', + 'info' => 'f0f1f2f3f4f5f6f7f8f9', + 'L' => 42, + 'PRK' => '077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5', + 'OKM' => '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865', + ], + [ + 'Hash' => 'sha256', + 'IKM' => '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f', + 'salt' => '606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf', + 'info' => 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', + 'L' => 82, + 'PRK' => '06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244', + 'OKM' => 'b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87', + ], + [ + 'Hash' => 'sha256', + 'IKM' => '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', + 'salt' => '', + 'info' => '', + 'L' => 42, + 'PRK' => '19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04', + 'OKM' => '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8', + ], + [ + 'Hash' => 'sha1', + 'IKM' => '0b0b0b0b0b0b0b0b0b0b0b', + 'salt' => '000102030405060708090a0b0c', + 'info' => 'f0f1f2f3f4f5f6f7f8f9', + 'L' => 42, + 'PRK' => '9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243', + 'OKM' => '085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896', + ], + [ + 'Hash' => 'sha1', + 'IKM' => '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f', + 'salt' => '606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf', + 'info' => 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', + 'L' => 82, + 'PRK' => '8adae09a2a307059478d309b26c4115a224cfaf6', + 'OKM' => '0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4', + ], + [ + 'Hash' => 'sha1', + 'IKM' => '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', + 'salt' => '', + 'info' => '', + 'L' => 42, + 'PRK' => 'da8c8a73c7fa77288ec6f5e7c297786aa0d32d01', + 'OKM' => '0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918', + ], + [ + 'Hash' => 'sha1', + 'IKM' => '0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c', + 'salt' => null, + 'info' => '', + 'L' => 42, + 'PRK' => '2adccada18779e7c2077ad2eb19d3f3e731385dd', + 'OKM' => '2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48', + ] + ]; } - public function testGenerateRandomKey() + /** + * @dataProvider dataProviderDeriveKey + * + * @param string $hash + * @param string $ikm + * @param string $salt + * @param string $info + * @param int $l + * @param string $prk + * @param string $okm + */ + public function testHkdf($hash, $ikm, $salt, $info, $l, $prk, $okm) { - $length = 21; - $key = $this->security->generateRandomKey($length); - $this->assertEquals($length, strlen($key)); - $this->assertEquals(1, preg_match('/[A-Za-z0-9_.-]+/', $key)); + $dk = $this->security->hkdf($hash, hex2bin($ikm), hex2bin($salt), hex2bin($info), $l); + $this->assertEquals($okm, bin2hex($dk)); } -} +} \ No newline at end of file From c5a3cd511eb86106a57a1fecdf348c150f1ce7fb Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 26 Jul 2014 14:09:38 +0400 Subject: [PATCH 2/2] Security component adjustments: fixed comment style, hkdf() and pbkdf2() are now protected, compareString() is now public --- framework/base/Security.php | 34 +++++++++++++++++---------- tests/unit/framework/base/ExposedSecurity.php | 27 +++++++++++++++++++++ tests/unit/framework/base/SecurityTest.php | 5 ++-- 3 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 tests/unit/framework/base/ExposedSecurity.php diff --git a/framework/base/Security.php b/framework/base/Security.php index 0b594fa..0c955f4 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -44,20 +44,30 @@ class Security extends Component */ public $passwordHashStrategy = 'crypt'; - // AES has 128-bit block size and three key sizes: 128, 192 and 256 bits. - // mcrypt offers the Rijndael cipher with block sizes of 128, 192 and 256 - // bits but only the 128-bit Rijndael is standardized in AES. - // So to use AES in mycrypt, specify `'rijndael-128'` cipher and mcrypt - // chooses the appropriate AES based on the length of the supplied key. + /** + * AES has 128-bit block size and three key sizes: 128, 192 and 256 bits. + * mcrypt offers the Rijndael cipher with block sizes of 128, 192 and 256 + * bits but only the 128-bit Rijndael is standardized in AES. + * So to use AES in mycrypt, specify `'rijndael-128'` cipher and mcrypt + * chooses the appropriate AES based on the length of the supplied key. + */ const MCRYPT_CIPHER = 'rijndael-128'; const MCRYPT_MODE = 'cbc'; - // Same size for encryption keys, auth keys and KDF salt + /** + * Same size for encryption keys, auth keys and KDF salt + */ const KEY_SIZE = 16; - // Hash algorithm for key derivation. + /** + * Hash algorithm for key derivation. + */ const KDF_HASH = 'sha256'; - // Hash algorithm for authentication. + /** + * Hash algorithm for authentication. + */ const MAC_HASH = 'sha256'; - // HKDF info value for auth keys + /** + * HKDF info value for auth keys + */ const AUTH_KEY_INFO = 'AuthorizationKey'; private $_cryptModule; @@ -282,7 +292,7 @@ class Security extends Component * @throws InvalidParamException * @return string the derived key */ - public function hkdf($algo, $inputKey, $salt = null, $info = null, $length = 0) + protected function hkdf($algo, $inputKey, $salt = null, $info = null, $length = 0) { $test = @hash_hmac($algo, '', '', true); if (!$test) { @@ -329,7 +339,7 @@ class Security extends Component * @throws InvalidParamException * @return string the derived key */ - public function pbkdf2($algo, $password, $salt, $iterations, $length = 0) + protected function pbkdf2($algo, $password, $salt, $iterations, $length = 0) { if (function_exists('hash_pbkdf2')) { $outputKey = hash_pbkdf2($algo, $password, $salt, $iterations, $length, true); @@ -599,7 +609,7 @@ class Security extends Component * @param string $actual string to compare. * @return boolean whether strings are equal. */ - protected function compareString($expected, $actual) + public function compareString($expected, $actual) { // timing attack resistant approach: $length = StringHelper::byteLength($expected); diff --git a/tests/unit/framework/base/ExposedSecurity.php b/tests/unit/framework/base/ExposedSecurity.php new file mode 100644 index 0000000..7909057 --- /dev/null +++ b/tests/unit/framework/base/ExposedSecurity.php @@ -0,0 +1,27 @@ +security = new Security(); + $this->security = new ExposedSecurity(); $this->security->derivationIterations = 1000; // speed up test running }