From db0beb6b6bc9e62161976d04f24bdd6a636ec5ab Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Fri, 27 Jun 2014 14:24:58 +0300 Subject: [PATCH 01/19] 'yii\base\Security' component created. --- framework/base/Security.php | 369 +++++++++++++++++++++++++++++ tests/unit/framework/base/SecurityTest.php | 57 +++++ 2 files changed, 426 insertions(+) create mode 100644 framework/base/Security.php create mode 100644 tests/unit/framework/base/SecurityTest.php diff --git a/framework/base/Security.php b/framework/base/Security.php new file mode 100644 index 0000000..bb02474 --- /dev/null +++ b/framework/base/Security.php @@ -0,0 +1,369 @@ + + * @link http://www.zfort.com/ + * @copyright Copyright © 2000-2014 Zfort Group + * @license http://www.zfort.com/terms-of-use + */ + +namespace yii\base; +use yii\helpers\StringHelper; +use Yii; + + +/** + * Security provides a set of methods to handle common security-related tasks. + * + * In particular, Security supports the following features: + * + * - Encryption/decryption: [[encrypt()]] and [[decrypt()]] + * - 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. + * + * @author Qiang Xue + * @author Tom Worster + * @author Klimov Paul + * @since 2.0 + */ +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). + */ + 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). + */ + public $cryptKeySize = 32; + /** + * @var string derivation hash algorithm name. + */ + public $derivationHash = 'sha256'; + /** + * @var integer derivation iterations count. + */ + public $derivationIterations = 1000000; + + /** + * Encrypts data. + * @param string $data data to be encrypted. + * @param string $password the encryption password + * @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) + { + $module = $this->openCryptModule(); + $data = $this->addPadding($data); + $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_DEV_URANDOM); + $key = $this->deriveKey($password, $iv); + mcrypt_generic_init($module, $key, $iv); + $encrypted = $iv . mcrypt_generic($module, $data); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + + return $encrypted; + } + + /** + * 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 + * @see encrypt() + */ + public function decrypt($data, $password) + { + if ($data === null) { + return null; + } + $module = $this->openCryptModule(); + $ivSize = mcrypt_enc_get_iv_size($module); + $iv = StringHelper::byteSubstr($data, 0, $ivSize); + $key = $this->deriveKey($password, $iv); + mcrypt_generic_init($module, $key, $iv); + $decrypted = mdecrypt_generic($module, StringHelper::byteSubstr($data, $ivSize, StringHelper::byteLength($data))); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + + return $this->stripPadding($decrypted); + } + + /** + * Adds a padding to the given data (PKCS #7). + * @param string $data the data to pad + * @return string the padded data + */ + protected function addPadding($data) + { + $pad = $this->cryptBlockSize - (StringHelper::byteLength($data) % $this->cryptBlockSize); + + return $data . str_repeat(chr($pad), $pad); + } + + /** + * Strips the padding from the given data. + * @param string $data the data to trim + * @return string the trimmed data + */ + protected function stripPadding($data) + { + $end = StringHelper::byteSubstr($data, -1, null); + $last = ord($end); + $n = StringHelper::byteLength($data) - $last; + if (StringHelper::byteSubstr($data, $n, null) == str_repeat($end, $last)) { + return StringHelper::byteSubstr($data, 0, $n); + } + + return false; + } + + /** + * Derives a key from the given password (PBKDF2). + * @param string $password the source password + * @param string $salt the random salt + * @return string the derived key + */ + protected function deriveKey($password, $salt) + { + if (function_exists('hash_pbkdf2')) { + return hash_pbkdf2($this->derivationHash, $password, $salt, $this->derivationIterations, $this->cryptKeySize, true); + } + $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; + } + + return substr($xorsum, 0, $this->cryptKeySize); + } + + /** + * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. + * @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. + * @return string the data prefixed with the keyed hash + * @see validateData() + * @see getSecretKey() + */ + public function hashData($data, $key, $algorithm = 'sha256') + { + return hash_hmac($algorithm, $data, $key) . $data; + } + + /** + * Validates if the given data is tampered. + * @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. + * @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') + { + $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); + + $calculatedHash = hash_hmac($algorithm, $pureData, $key); + + // timing attack resistant approach: + $diff = 0; + for ($i = 0; $i < StringHelper::byteLength($calculatedHash); $i++) { + $diff |= (ord($calculatedHash[$i]) ^ ord($hash[$i])); + } + return $diff === 0 ? $pureData : false; + } else { + return false; + } + } + + /** + * Returns a secret key associated with the specified name. + * If the secret key does not exist, a random key will be generated + * and saved in the file "keys.json" under the application's runtime directory + * so that the same secret key can be returned in future requests. + * @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 + * @return string the secret key associated with the specified name + */ + public function getSecretKey($name, $length = 32) + { + static $keys; + $keyFile = Yii::$app->getRuntimePath() . '/keys.json'; + if ($keys === null) { + $keys = []; + if (is_file($keyFile)) { + $keys = json_decode(file_get_contents($keyFile), true); + } + } + if (!isset($keys[$name])) { + $keys[$name] = $this->generateRandomKey($length); + file_put_contents($keyFile, json_encode($keys)); + } + + return $keys[$name]; + } + + /** + * Generates a random key. The key may contain uppercase and lowercase latin letters, digits, underscore, dash and dot. + * @param integer $length the length of the key that should be generated + * @return string the generated random key + */ + public function generateRandomKey($length = 32) + { + return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + } + + /** + * 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; + } + + /** + * 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). + * Later when a password needs to be validated, the hash can be fetched and passed + * to [[validatePassword()]]. For example, + * + * ~~~ + * // generates the hash (usually done during user registration or when the password is changed) + * $hash = Yii::$app->getSecurity()->generatePasswordHash($password); + * // ...save $hash in database... + * + * // during login, validate if the password entered is correct using $hash fetched from database + * if (Yii::$app->getSecurity()->validatePassword($password, $hash) { + * // password is good + * } else { + * // password is bad + * } + * ~~~ + * + * @param string $password The password to be hashed. + * @param integer $cost Cost parameter used by the Blowfish hash algorithm. + * The higher the value of cost, + * 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. + * @throws Exception on bad password parameter or cost parameter + * @return string The password hash string, ASCII and not longer than 64 characters. + * @see validatePassword() + */ + public function generatePasswordHash($password, $cost = 13) + { + $salt = $this->generateSalt($cost); + $hash = crypt($password, $salt); + + if (!is_string($hash) || strlen($hash) < 32) { + throw new Exception('Unknown error occurred while generating hash.'); + } + + return $hash; + } + + /** + * Verifies a password against a hash. + * @param string $password The password to verify. + * @param string $hash The hash to verify the password against. + * @return boolean whether the password is correct. + * @throws InvalidParamException on bad password or hash parameters or if crypt() with Blowfish hash is not available. + * @see generatePasswordHash() + */ + public function validatePassword($password, $hash) + { + if (!is_string($password) || $password === '') { + throw new InvalidParamException('Password must be a string and cannot be empty.'); + } + + if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) { + throw new InvalidParamException('Hash is invalid.'); + } + + $test = crypt($password, $hash); + $n = strlen($test); + if ($n < 32 || $n !== strlen($hash)) { + return false; + } + + // Use a for-loop to compare two strings to prevent timing attacks. See: + // http://codereview.stackexchange.com/questions/13512 + $check = 0; + for ($i = 0; $i < $n; ++$i) { + $check |= (ord($test[$i]) ^ ord($hash[$i])); + } + + return $check === 0; + } + + /** + * Generates a salt that can be used to generate a password hash. + * + * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function + * requires, for the Blowfish hash algorithm, a salt string in a specific format: + * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters + * from the alphabet "./0-9A-Za-z". + * + * @param integer $cost the cost parameter + * @return string the random salt value. + * @throws InvalidParamException if the cost parameter is not between 4 and 31 + */ + protected function generateSalt($cost = 13) + { + $cost = (int) $cost; + if ($cost < 4 || $cost > 31) { + throw new InvalidParamException('Cost must be between 4 and 31.'); + } + + // Get 20 * 8bits of random entropy + $rand = $this->generateRandomKey(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. + $salt = sprintf("$2y$%02d$", $cost); + // Append the random salt data in the required base64 format. + $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); + + return $salt; + } +} \ No newline at end of file diff --git a/tests/unit/framework/base/SecurityTest.php b/tests/unit/framework/base/SecurityTest.php new file mode 100644 index 0000000..401bb7e --- /dev/null +++ b/tests/unit/framework/base/SecurityTest.php @@ -0,0 +1,57 @@ +security = new Security(); + } + + public function testPasswordHash() + { + $password = 'secret'; + $hash = $this->security->generatePasswordHash($password); + $this->assertTrue($this->security->validatePassword($password, $hash)); + $this->assertFalse($this->security->validatePassword('test', $hash)); + } + + public function testHashData() + { + $data = 'known data'; + $key = 'secret'; + $hashedData = $this->security->hashData($data, $key); + $this->assertFalse($data === $hashedData); + $this->assertEquals($data, $this->security->validateData($hashedData, $key)); + $hashedData[strlen($hashedData) - 1] = 'A'; + $this->assertFalse($this->security->validateData($hashedData, $key)); + } + + public function testEncrypt() + { + $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); + } +} From 54ac875e212cedf2383c03dcf78d45fef3d7fade Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Fri, 27 Jun 2014 14:26:51 +0300 Subject: [PATCH 02/19] Component 'security' added tp the base application --- framework/base/Application.php | 11 +++++++++++ framework/classes.php | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index f94e36f..b599e6b 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -30,6 +30,7 @@ use Yii; * read-only. * @property string $runtimePath The directory that stores runtime files. Defaults to the "runtime" * subdirectory under [[basePath]]. + * @property \yii\base\Security $security The security application component. * @property string $timeZone The time zone used by this application. * @property string $uniqueId The unique ID of the module. This property is read-only. * @property \yii\web\UrlManager $urlManager The URL manager for this application. This property is read-only. @@ -592,6 +593,15 @@ abstract class Application extends Module } /** + * Returns the security component. + * @return \yii\base\Security security component + */ + public function getSecurity() + { + return $this->get('security'); + } + + /** * Returns the core application components. * @see set */ @@ -605,6 +615,7 @@ abstract class Application extends Module 'mailer' => ['class' => 'yii\swiftmailer\Mailer'], 'urlManager' => ['class' => 'yii\web\UrlManager'], 'assetManager' => ['class' => 'yii\web\AssetManager'], + 'security' => ['class' => 'yii\base\Security'], ]; } diff --git a/framework/classes.php b/framework/classes.php index 6fc5240..88ebbce 100644 --- a/framework/classes.php +++ b/framework/classes.php @@ -41,6 +41,7 @@ return [ 'yii\base\Object' => YII_PATH . '/base/Object.php', 'yii\base\Request' => YII_PATH . '/base/Request.php', 'yii\base\Response' => YII_PATH . '/base/Response.php', + 'yii\base\Security' => YII_PATH . '/base/Security.php', 'yii\base\Theme' => YII_PATH . '/base/Theme.php', 'yii\base\UnknownClassException' => YII_PATH . '/base/UnknownClassException.php', 'yii\base\UnknownMethodException' => YII_PATH . '/base/UnknownMethodException.php', @@ -167,7 +168,6 @@ return [ 'yii\helpers\Inflector' => YII_PATH . '/helpers/Inflector.php', 'yii\helpers\Json' => YII_PATH . '/helpers/Json.php', 'yii\helpers\Markdown' => YII_PATH . '/helpers/Markdown.php', - 'yii\helpers\Security' => YII_PATH . '/helpers/Security.php', 'yii\helpers\StringHelper' => YII_PATH . '/helpers/StringHelper.php', 'yii\helpers\Url' => YII_PATH . '/helpers/Url.php', 'yii\helpers\VarDumper' => YII_PATH . '/helpers/VarDumper.php', From 2bab6259d1f121de68121c497b322385484fa85d Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Fri, 27 Jun 2014 14:31:08 +0300 Subject: [PATCH 03/19] 'Security' helper usage switched to 'security' application component. --- apps/advanced/common/models/User.php | 10 +++++----- apps/advanced/common/tests/templates/fixtures/user.php | 8 +++----- framework/web/Request.php | 7 +++---- framework/web/Response.php | 3 +-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/apps/advanced/common/models/User.php b/apps/advanced/common/models/User.php index b2145cc..b4ca611 100644 --- a/apps/advanced/common/models/User.php +++ b/apps/advanced/common/models/User.php @@ -3,7 +3,7 @@ namespace common\models; use yii\base\NotSupportedException; use yii\db\ActiveRecord; -use yii\helpers\Security; +use Yii; use yii\web\IdentityInterface; /** @@ -147,7 +147,7 @@ class User extends ActiveRecord implements IdentityInterface */ public function validatePassword($password) { - return Security::validatePassword($password, $this->password_hash); + return Yii::$app->getSecurity()->validatePassword($password, $this->password_hash); } /** @@ -157,7 +157,7 @@ class User extends ActiveRecord implements IdentityInterface */ public function setPassword($password) { - $this->password_hash = Security::generatePasswordHash($password); + $this->password_hash = Yii::$app->getSecurity()->generatePasswordHash($password); } /** @@ -165,7 +165,7 @@ class User extends ActiveRecord implements IdentityInterface */ public function generateAuthKey() { - $this->auth_key = Security::generateRandomKey(); + $this->auth_key = Yii::$app->getSecurity()->generateRandomKey(); } /** @@ -173,7 +173,7 @@ class User extends ActiveRecord implements IdentityInterface */ public function generatePasswordResetToken() { - $this->password_reset_token = Security::generateRandomKey() . '_' . time(); + $this->password_reset_token = Yii::$app->getSecurity()->generateRandomKey() . '_' . time(); } /** diff --git a/apps/advanced/common/tests/templates/fixtures/user.php b/apps/advanced/common/tests/templates/fixtures/user.php index 29b0558..b91ba40 100644 --- a/apps/advanced/common/tests/templates/fixtures/user.php +++ b/apps/advanced/common/tests/templates/fixtures/user.php @@ -1,21 +1,19 @@ 'userName', 'auth_key' => function ($fixture, $faker, $index) { - $fixture['auth_key'] = Security::generateRandomKey(); + $fixture['auth_key'] = Yii::$app->getSecurity()->generateRandomKey(); return $fixture; }, 'password_hash' => function ($fixture, $faker, $index) { - $fixture['password_hash'] = Security::generatePasswordHash('password_' . $index); + $fixture['password_hash'] = Yii::$app->getSecurity()->generatePasswordHash('password_' . $index); return $fixture; }, 'password_reset_token' => function ($fixture, $faker, $index) { - $fixture['password_reset_token'] = Security::generateRandomKey() . '_' . time(); + $fixture['password_reset_token'] = Yii::$app->getSecurity()->generateRandomKey() . '_' . time(); return $fixture; }, diff --git a/framework/web/Request.php b/framework/web/Request.php index 551827e..da48028 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -9,7 +9,6 @@ namespace yii\web; use Yii; use yii\base\InvalidConfigException; -use yii\helpers\Security; use yii\helpers\StringHelper; /** @@ -1188,7 +1187,7 @@ class Request extends \yii\base\Request if ($this->enableCookieValidation) { $key = $this->getCookieValidationKey(); foreach ($_COOKIE as $name => $value) { - if (is_string($value) && ($value = Security::validateData($value, $key)) !== false) { + if (is_string($value) && ($value = Yii::$app->getSecurity()->validateData($value, $key)) !== false) { $cookies[$name] = new Cookie([ 'name' => $name, 'value' => @unserialize($value), @@ -1218,7 +1217,7 @@ class Request extends \yii\base\Request public function getCookieValidationKey() { if ($this->_cookieValidationKey === null) { - $this->_cookieValidationKey = Security::getSecretKey(__CLASS__ . '/' . Yii::$app->id); + $this->_cookieValidationKey = Yii::$app->getSecurity()->getSecretKey(__CLASS__ . '/' . Yii::$app->id); } return $this->_cookieValidationKey; @@ -1323,7 +1322,7 @@ class Request extends \yii\base\Request { $options = $this->csrfCookie; $options['name'] = $this->csrfParam; - $options['value'] = Security::generateRandomKey(); + $options['value'] = Yii::$app->getSecurity()->generateRandomKey(); return new Cookie($options); } diff --git a/framework/web/Response.php b/framework/web/Response.php index 2a28a86..332175b 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -12,7 +12,6 @@ use yii\base\InvalidConfigException; use yii\base\InvalidParamException; use yii\helpers\Url; use yii\helpers\FileHelper; -use yii\helpers\Security; use yii\helpers\StringHelper; /** @@ -371,7 +370,7 @@ class Response extends \yii\base\Response foreach ($this->getCookies() as $cookie) { $value = $cookie->value; if ($cookie->expire != 1 && isset($validationKey)) { - $value = Security::hashData(serialize($value), $validationKey); + $value = Yii::$app->getSecurity()->hashData(serialize($value), $validationKey); } setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); } From 63c7a4cfca418ba15036f9624946d1e87e059b51 Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Fri, 27 Jun 2014 14:33:20 +0300 Subject: [PATCH 04/19] Docs regarding `Security` component usage updated. --- docs/guide-es/intro-upgrade-from-v1.md | 1 - docs/guide-fr/intro-upgrade-from-v1.md | 1 - docs/guide-pt-BR/intro-upgrade-from-v1.md | 1 - docs/guide-ru/intro-upgrade-from-v1.md | 1 - docs/guide-zh-CN/intro-upgrade-from-v1.md | 1 - docs/guide/intro-upgrade-from-v1.md | 1 - docs/guide/security-authentication.md | 4 ++-- docs/guide/security-passwords.md | 15 +++++++-------- extensions/faker/README.md | 6 ++---- 9 files changed, 11 insertions(+), 20 deletions(-) diff --git a/docs/guide-es/intro-upgrade-from-v1.md b/docs/guide-es/intro-upgrade-from-v1.md index 4082754..1f7a8be 100644 --- a/docs/guide-es/intro-upgrade-from-v1.md +++ b/docs/guide-es/intro-upgrade-from-v1.md @@ -350,7 +350,6 @@ Yii 2.0 introduce muchos helpers estáticos comúnmente utilizados, incluyendo: * [[yii\helpers\StringHelper]] * [[yii\helpers\FileHelper]] * [[yii\helpers\Json]] -* [[yii\helpers\Security]] Por favor, consulta la sección [Información General de Helpers](helper-overview.md) para más detalles. diff --git a/docs/guide-fr/intro-upgrade-from-v1.md b/docs/guide-fr/intro-upgrade-from-v1.md index 989009d..e77e66a 100644 --- a/docs/guide-fr/intro-upgrade-from-v1.md +++ b/docs/guide-fr/intro-upgrade-from-v1.md @@ -348,7 +348,6 @@ Yii 2.0 introduit de nombreuses assistants couramment utilisés, sous la forme d * [[yii\helpers\StringHelper]] * [[yii\helpers\FileHelper]] * [[yii\helpers\Json]] -* [[yii\helpers\Security]] Merci de lire la partie [Assistants](helper-overview.md) pour plus de détails. diff --git a/docs/guide-pt-BR/intro-upgrade-from-v1.md b/docs/guide-pt-BR/intro-upgrade-from-v1.md index ac0a8a8..8b9d84a 100644 --- a/docs/guide-pt-BR/intro-upgrade-from-v1.md +++ b/docs/guide-pt-BR/intro-upgrade-from-v1.md @@ -398,7 +398,6 @@ O Yii 2.0 introduz muitas classes de helper estáticas comumente usadas, incluin * [[yii\helpers\StringHelper]] * [[yii\helpers\FileHelper]] * [[yii\helpers\Json]] -* [[yii\helpers\Security]] Por favor consulte a seção [Visão Geral](helper-overview.md) dos helpers para mais detalhes. diff --git a/docs/guide-ru/intro-upgrade-from-v1.md b/docs/guide-ru/intro-upgrade-from-v1.md index 1072b23..334b8eb 100644 --- a/docs/guide-ru/intro-upgrade-from-v1.md +++ b/docs/guide-ru/intro-upgrade-from-v1.md @@ -344,7 +344,6 @@ public function behaviors() * [[yii\helpers\StringHelper]] * [[yii\helpers\FileHelper]] * [[yii\helpers\Json]] -* [[yii\helpers\Security]] Более детальная информация представлена в разделе [Хелперы](helper-overview.md). diff --git a/docs/guide-zh-CN/intro-upgrade-from-v1.md b/docs/guide-zh-CN/intro-upgrade-from-v1.md index 2c1377a..b26adf8 100644 --- a/docs/guide-zh-CN/intro-upgrade-from-v1.md +++ b/docs/guide-zh-CN/intro-upgrade-from-v1.md @@ -317,7 +317,6 @@ Yii 2.0 很多常用的静态助手类,包括: * [[yii\helpers\StringHelper]] * [[yii\helpers\FileHelper]] * [[yii\helpers\Json]] -* [[yii\helpers\Security]] 请参考 [助手一览](helper-overview.md) 章节来了解更多。 diff --git a/docs/guide/intro-upgrade-from-v1.md b/docs/guide/intro-upgrade-from-v1.md index cec34f4..be13896 100644 --- a/docs/guide/intro-upgrade-from-v1.md +++ b/docs/guide/intro-upgrade-from-v1.md @@ -349,7 +349,6 @@ Yii 2.0 introduces many commonly used static helper classes, including. * [[yii\helpers\StringHelper]] * [[yii\helpers\FileHelper]] * [[yii\helpers\Json]] -* [[yii\helpers\Security]] Please refer to the [Helper Overview](helper-overview.md) section for more details. diff --git a/docs/guide/security-authentication.md b/docs/guide/security-authentication.md index fb9cd0f..2c5d3f8 100644 --- a/docs/guide/security-authentication.md +++ b/docs/guide/security-authentication.md @@ -65,14 +65,14 @@ 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 `Security::generateRandomKey()`. It's a good idea to also save this as part of the user's record: +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: ```php public function beforeSave($insert) { if (parent::beforeSave($insert)) { if ($this->isNewRecord) { - $this->auth_key = Security::generateRandomKey(); + $this->auth_key = Yii::$app->getSecurity()->generateRandomKey(); } return true; } diff --git a/docs/guide/security-passwords.md b/docs/guide/security-passwords.md index 225ade1..309a6c8 100644 --- a/docs/guide/security-passwords.md +++ b/docs/guide/security-passwords.md @@ -17,7 +17,7 @@ When a user provides a password for the first time (e.g., upon registration), th ```php -$hash = \yii\helpers\Security::generatePasswordHash($password); +$hash = \yii\helpers\Yii::$app->getSecurity()->generatePasswordHash($password); ``` The hash can then be associated with the corresponding model attribute, so it can be stored in the database for later use. @@ -26,8 +26,7 @@ When a user attempts to log in, the submitted password must be verified against ```php -use yii\helpers\Security; -if (Security::validatePassword($password, $hash)) { +if (Yii::$app->getSecurity()->validatePassword($password, $hash)) { // all good, logging user in } else { // wrong password @@ -43,7 +42,7 @@ Yii security helper makes generating pseudorandom data simple: ```php -$key = \yii\helpers\Security::generateRandomKey(); +$key = \yii\helpers\Yii::$app->getSecurity()->generateRandomKey(); ``` Note that you need to have the `openssl` extension installed in order to generate cryptographically secure random data. @@ -57,7 +56,7 @@ For example, we need to store some information in our database but we need to ma ```php // $data and $secretKey are obtained from the form -$encryptedData = \yii\helpers\Security::encrypt($data, $secretKey); +$encryptedData = \yii\helpers\Yii::$app->getSecurity()->encrypt($data, $secretKey); // store $encryptedData to database ``` @@ -65,7 +64,7 @@ Subsequently when user wants to read the data: ```php // $secretKey is obtained from user input, $encryptedData is from the database -$data = \yii\helpers\Security::decrypt($encryptedData, $secretKey); +$data = \yii\helpers\Yii::$app->getSecurity()->decrypt($encryptedData, $secretKey); ``` Confirming data integrity @@ -78,14 +77,14 @@ Prefix the data with a hash generated from the secret key and data ```php // $secretKey our application or user secret, $genuineData obtained from a reliable source -$data = \yii\helpers\Security::hashData($genuineData, $secretKey); +$data = \yii\helpers\Yii::$app->getSecurity()->hashData($genuineData, $secretKey); ``` Checks if the data integrity has been compromised ```php // $secretKey our application or user secret, $data obtained from an unreliable source -$data = \yii\helpers\Security::validateData($data, $secretKey); +$data = \yii\helpers\Yii::$app->getSecurity()->validateData($data, $secretKey); ``` diff --git a/extensions/faker/README.md b/extensions/faker/README.md index 59afcfe..6a7f330 100644 --- a/extensions/faker/README.md +++ b/extensions/faker/README.md @@ -69,18 +69,16 @@ After you set all needed fields in callback, you need to return $fixture array b Another example of valid template: ```php -use yii\helpers\Security; - return [ 'name' => 'firstName', 'phone' => 'phoneNumber', 'city' => 'city', 'password' => function ($fixture, $faker, $index) { - $fixture['password'] = Security::generatePasswordHash('password_' . $index); + $fixture['password'] = Yii::$app->getSecurity()->generatePasswordHash('password_' . $index); return $fixture; }, 'auth_key' => function ($fixture, $faker, $index) { - $fixture['auth_key'] = Security::generateRandomKey(); + $fixture['auth_key'] = Yii::$app->getSecurity()->generateRandomKey(); return $fixture; }, ]; From e6f7d9b62571550721568009d1733b5ea9d5715b Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Fri, 27 Jun 2014 14:34:11 +0300 Subject: [PATCH 05/19] Security helper files removed --- framework/helpers/BaseSecurity.php | 368 -------------------------- framework/helpers/Security.php | 29 -- tests/unit/framework/helpers/SecurityTest.php | 46 ---- 3 files changed, 443 deletions(-) delete mode 100644 framework/helpers/BaseSecurity.php delete mode 100644 framework/helpers/Security.php delete mode 100644 tests/unit/framework/helpers/SecurityTest.php diff --git a/framework/helpers/BaseSecurity.php b/framework/helpers/BaseSecurity.php deleted file mode 100644 index b2e9b03..0000000 --- a/framework/helpers/BaseSecurity.php +++ /dev/null @@ -1,368 +0,0 @@ - - * @author Tom Worster - * @since 2.0 - */ -class BaseSecurity -{ - /** - * Uses AES, block size is 128-bit (16 bytes). - */ - const CRYPT_BLOCK_SIZE = 16; - - /** - * Uses AES-192, key size is 192-bit (24 bytes). - */ - const CRYPT_KEY_SIZE = 24; - - /** - * Uses SHA-256. - */ - const DERIVATION_HASH = 'sha256'; - - /** - * Uses 1000 iterations. - */ - const DERIVATION_ITERATIONS = 1000; - - /** - * Encrypts data. - * @param string $data data to be encrypted. - * @param string $password the encryption password - * @return string the encrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see decrypt() - */ - public static function encrypt($data, $password) - { - $module = static::openCryptModule(); - $data = static::addPadding($data); - $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); - $key = static::deriveKey($password, $iv); - mcrypt_generic_init($module, $key, $iv); - $encrypted = $iv . mcrypt_generic($module, $data); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - - return $encrypted; - } - - /** - * 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 - * @see encrypt() - */ - public static function decrypt($data, $password) - { - if ($data === null) { - return null; - } - $module = static::openCryptModule(); - $ivSize = mcrypt_enc_get_iv_size($module); - $iv = StringHelper::byteSubstr($data, 0, $ivSize); - $key = static::deriveKey($password, $iv); - mcrypt_generic_init($module, $key, $iv); - $decrypted = mdecrypt_generic($module, StringHelper::byteSubstr($data, $ivSize, StringHelper::byteLength($data))); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - - return static::stripPadding($decrypted); - } - - /** - * Adds a padding to the given data (PKCS #7). - * @param string $data the data to pad - * @return string the padded data - */ - protected static function addPadding($data) - { - $pad = self::CRYPT_BLOCK_SIZE - (StringHelper::byteLength($data) % self::CRYPT_BLOCK_SIZE); - - return $data . str_repeat(chr($pad), $pad); - } - - /** - * Strips the padding from the given data. - * @param string $data the data to trim - * @return string the trimmed data - */ - protected static function stripPadding($data) - { - $end = StringHelper::byteSubstr($data, -1, null); - $last = ord($end); - $n = StringHelper::byteLength($data) - $last; - if (StringHelper::byteSubstr($data, $n, null) == str_repeat($end, $last)) { - return StringHelper::byteSubstr($data, 0, $n); - } - - return false; - } - - /** - * Derives a key from the given password (PBKDF2). - * @param string $password the source password - * @param string $salt the random salt - * @return string the derived key - */ - protected static function deriveKey($password, $salt) - { - if (function_exists('hash_pbkdf2')) { - return hash_pbkdf2(self::DERIVATION_HASH, $password, $salt, self::DERIVATION_ITERATIONS, self::CRYPT_KEY_SIZE, true); - } - $hmac = hash_hmac(self::DERIVATION_HASH, $salt . pack('N', 1), $password, true); - $xorsum = $hmac; - for ($i = 1; $i < self::DERIVATION_ITERATIONS; $i++) { - $hmac = hash_hmac(self::DERIVATION_HASH, $hmac, $password, true); - $xorsum ^= $hmac; - } - - return substr($xorsum, 0, self::CRYPT_KEY_SIZE); - } - - /** - * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. - * @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. - * @return string the data prefixed with the keyed hash - * @see validateData() - * @see getSecretKey() - */ - public static function hashData($data, $key, $algorithm = 'sha256') - { - return hash_hmac($algorithm, $data, $key) . $data; - } - - /** - * Validates if the given data is tampered. - * @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. - * @return string the real data with the hash stripped off. False if the data is tampered. - * @see hashData() - */ - public static function validateData($data, $key, $algorithm = 'sha256') - { - $hashSize = StringHelper::byteLength(hash_hmac($algorithm, 'test', $key)); - $n = StringHelper::byteLength($data); - if ($n >= $hashSize) { - $hash = StringHelper::byteSubstr($data, 0, $hashSize); - $data2 = StringHelper::byteSubstr($data, $hashSize, $n - $hashSize); - - return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; - } else { - return false; - } - } - - /** - * Returns a secret key associated with the specified name. - * If the secret key does not exist, a random key will be generated - * and saved in the file "keys.json" under the application's runtime directory - * so that the same secret key can be returned in future requests. - * @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 - * @return string the secret key associated with the specified name - */ - public static function getSecretKey($name, $length = 32) - { - static $keys; - $keyFile = Yii::$app->getRuntimePath() . '/keys.json'; - if ($keys === null) { - $keys = []; - if (is_file($keyFile)) { - $keys = json_decode(file_get_contents($keyFile), true); - } - } - if (!isset($keys[$name])) { - $keys[$name] = static::generateRandomKey($length); - file_put_contents($keyFile, json_encode($keys)); - } - - return $keys[$name]; - } - - /** - * Generates a random key. The key may contain uppercase and lowercase latin letters, digits, underscore, dash and dot. - * @param integer $length the length of the key that should be generated - * @return string the generated random key - */ - public static function generateRandomKey($length = 32) - { - if (function_exists('openssl_random_pseudo_bytes')) { - $key = strtr(base64_encode(openssl_random_pseudo_bytes($length, $strong)), '+/=', '_-.'); - if ($strong) { - return substr($key, 0, $length); - } - } - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'; - - return substr(str_shuffle(str_repeat($chars, 5)), 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 static function openCryptModule() - { - if (!extension_loaded('mcrypt')) { - throw new InvalidConfigException('The mcrypt PHP extension is not installed.'); - } - // AES uses a 128-bit block size - $module = @mcrypt_module_open('rijndael-128', '', 'cbc', ''); - if ($module === false) { - throw new Exception('Failed to initialize the mcrypt module.'); - } - - return $module; - } - - /** - * 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). - * Later when a password needs to be validated, the hash can be fetched and passed - * to [[validatePassword()]]. For example, - * - * ~~~ - * // generates the hash (usually done during user registration or when the password is changed) - * $hash = Security::generatePasswordHash($password); - * // ...save $hash in database... - * - * // during login, validate if the password entered is correct using $hash fetched from database - * if (Security::validatePassword($password, $hash) { - * // password is good - * } else { - * // password is bad - * } - * ~~~ - * - * @param string $password The password to be hashed. - * @param integer $cost Cost parameter used by the Blowfish hash algorithm. - * The higher the value of cost, - * 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. - * @throws Exception on bad password parameter or cost parameter - * @return string The password hash string, ASCII and not longer than 64 characters. - * @see validatePassword() - */ - public static function generatePasswordHash($password, $cost = 13) - { - $salt = static::generateSalt($cost); - $hash = crypt($password, $salt); - - if (!is_string($hash) || strlen($hash) < 32) { - throw new Exception('Unknown error occurred while generating hash.'); - } - - return $hash; - } - - /** - * Verifies a password against a hash. - * @param string $password The password to verify. - * @param string $hash The hash to verify the password against. - * @return boolean whether the password is correct. - * @throws InvalidParamException on bad password or hash parameters or if crypt() with Blowfish hash is not available. - * @see generatePasswordHash() - */ - public static function validatePassword($password, $hash) - { - if (!is_string($password) || $password === '') { - throw new InvalidParamException('Password must be a string and cannot be empty.'); - } - - if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) { - throw new InvalidParamException('Hash is invalid.'); - } - - $test = crypt($password, $hash); - $n = strlen($test); - if ($n < 32 || $n !== strlen($hash)) { - return false; - } - - // Use a for-loop to compare two strings to prevent timing attacks. See: - // http://codereview.stackexchange.com/questions/13512 - $check = 0; - for ($i = 0; $i < $n; ++$i) { - $check |= (ord($test[$i]) ^ ord($hash[$i])); - } - - return $check === 0; - } - - /** - * Generates a salt that can be used to generate a password hash. - * - * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function - * requires, for the Blowfish hash algorithm, a salt string in a specific format: - * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters - * from the alphabet "./0-9A-Za-z". - * - * @param integer $cost the cost parameter - * @return string the random salt value. - * @throws InvalidParamException if the cost parameter is not between 4 and 31 - */ - protected static function generateSalt($cost = 13) - { - $cost = (int) $cost; - if ($cost < 4 || $cost > 31) { - throw new InvalidParamException('Cost must be between 4 and 31.'); - } - - // Get 20 * 8bits of random entropy - if (function_exists('openssl_random_pseudo_bytes')) { - // https://github.com/yiisoft/yii2/pull/2422 - $rand = openssl_random_pseudo_bytes(20); - } else { - $rand = ''; - for ($i = 0; $i < 20; ++$i) { - $rand .= chr(mt_rand(0, 255)); - } - } - - // 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. - $salt = sprintf("$2y$%02d$", $cost); - // Append the random salt data in the required base64 format. - $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); - - return $salt; - } -} diff --git a/framework/helpers/Security.php b/framework/helpers/Security.php deleted file mode 100644 index 0e3ee38..0000000 --- a/framework/helpers/Security.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @author Tom Worster - * @since 2.0 - */ -class Security extends BaseSecurity -{ -} diff --git a/tests/unit/framework/helpers/SecurityTest.php b/tests/unit/framework/helpers/SecurityTest.php deleted file mode 100644 index a3fe4e9..0000000 --- a/tests/unit/framework/helpers/SecurityTest.php +++ /dev/null @@ -1,46 +0,0 @@ -assertTrue(Security::validatePassword($password, $hash)); - $this->assertFalse(Security::validatePassword('test', $hash)); - } - - public function testHashData() - { - $data = 'known data'; - $key = 'secret'; - $hashedData = Security::hashData($data, $key); - $this->assertFalse($data === $hashedData); - $this->assertEquals($data, Security::validateData($hashedData, $key)); - $hashedData[strlen($hashedData) - 1] = 'A'; - $this->assertFalse(Security::validateData($hashedData, $key)); - } - - public function testEncrypt() - { - $data = 'known data'; - $key = 'secret'; - $encryptedData = Security::encrypt($data, $key); - $this->assertFalse($data === $encryptedData); - $decryptedData = Security::decrypt($encryptedData, $key); - $this->assertEquals($data, $decryptedData); - } -} From c86db2613629192b1055b8205e9ef401b93bc01f Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Fri, 27 Jun 2014 14:35:01 +0300 Subject: [PATCH 06/19] Notes about `Security` class refactoring added to CHANGELOG.md and UPGRADE.md --- framework/CHANGELOG.md | 1 + framework/UPGRADE.md | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index dd16e15..eec980e 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -65,6 +65,7 @@ Yii Framework 2 Change Log - Bug: URL encoding for the route parameter added to `\yii\web\UrlManager` (klimov-paul) - Bug: Fixed the bug that requesting protected or private action methods would cause 500 error instead of 404 (qiangxue) - Bug: Fixed Object of class Imagick could not be converted to string in CaptchaAction (eXprojects, cebe) +- Enh #87: Helper `yii\helpers\Security` converted into application component, cryptographic strength improved (klimov-paul) - Enh #422: Added Support for BIT(M) data type default values in Schema (cebe) - Enh #1452: Added `Module::getInstance()` to allow accessing the module instance from anywhere within the module (qiangxue) - Enh #2264: `CookieCollection::has()` will return false for expired or removed cookies (qiangxue) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index 83fc0fc..beb1753 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -72,3 +72,21 @@ Upgrade from Yii 2.0 Beta * `mail` component was renamed to `mailer`, `yii\log\EmailTarget::$mail` was renamed to `yii\log\EmailTarget::$mailer`. Please update all references in the code and config files. + +* Static helper `yii\helpers\Security` has been converted into an application component. You should change all usage of + its methods to a new syntax, for example: instead of `yii\helpers\Security::hashData()` use `Yii::$app->getSecurity()->hashData()`. + If you have used `yii\helpers\Security` for encryption or hash generating, you need to explicitly configure 'security' + component for the legacy code support in following way: + ``` + return [ + 'components' => [ + 'security' => [ + 'cryptBlockSize' => 16, + 'cryptKeySize' => 24, + 'derivationIterations' => 1000, + ], + // ... + ], + // ... + ]; + ``` \ No newline at end of file From 47f8eafb6dd4d0f5e7716b3b2d639147f73b8537 Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Fri, 27 Jun 2014 14:37:59 +0300 Subject: [PATCH 07/19] Doc comments at `yii\base\Security` fixed --- framework/base/Security.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/framework/base/Security.php b/framework/base/Security.php index bb02474..4523e84 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -1,9 +1,8 @@ - * @link http://www.zfort.com/ - * @copyright Copyright © 2000-2014 Zfort Group - * @license http://www.zfort.com/terms-of-use + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ */ namespace yii\base; From 0daf67d8ae9a4f98a6e8c530cc38f3d708240c17 Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Fri, 27 Jun 2014 15:59:36 +0300 Subject: [PATCH 08/19] Extra namespace at docs removed --- docs/guide/security-passwords.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/guide/security-passwords.md b/docs/guide/security-passwords.md index 309a6c8..b2fbb38 100644 --- a/docs/guide/security-passwords.md +++ b/docs/guide/security-passwords.md @@ -17,7 +17,7 @@ When a user provides a password for the first time (e.g., upon registration), th ```php -$hash = \yii\helpers\Yii::$app->getSecurity()->generatePasswordHash($password); +$hash = Yii::$app->getSecurity()->generatePasswordHash($password); ``` The hash can then be associated with the corresponding model attribute, so it can be stored in the database for later use. @@ -42,7 +42,7 @@ Yii security helper makes generating pseudorandom data simple: ```php -$key = \yii\helpers\Yii::$app->getSecurity()->generateRandomKey(); +$key = Yii::$app->getSecurity()->generateRandomKey(); ``` Note that you need to have the `openssl` extension installed in order to generate cryptographically secure random data. @@ -56,7 +56,7 @@ For example, we need to store some information in our database but we need to ma ```php // $data and $secretKey are obtained from the form -$encryptedData = \yii\helpers\Yii::$app->getSecurity()->encrypt($data, $secretKey); +$encryptedData = Yii::$app->getSecurity()->encrypt($data, $secretKey); // store $encryptedData to database ``` @@ -64,7 +64,7 @@ Subsequently when user wants to read the data: ```php // $secretKey is obtained from user input, $encryptedData is from the database -$data = \yii\helpers\Yii::$app->getSecurity()->decrypt($encryptedData, $secretKey); +$data = Yii::$app->getSecurity()->decrypt($encryptedData, $secretKey); ``` Confirming data integrity @@ -77,14 +77,14 @@ Prefix the data with a hash generated from the secret key and data ```php // $secretKey our application or user secret, $genuineData obtained from a reliable source -$data = \yii\helpers\Yii::$app->getSecurity()->hashData($genuineData, $secretKey); +$data = Yii::$app->getSecurity()->hashData($genuineData, $secretKey); ``` Checks if the data integrity has been compromised ```php // $secretKey our application or user secret, $data obtained from an unreliable source -$data = \yii\helpers\Yii::$app->getSecurity()->validateData($data, $secretKey); +$data = Yii::$app->getSecurity()->validateData($data, $secretKey); ``` From 4a47a59323e37c82480486a4f610aae3da7d4f4b Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Fri, 27 Jun 2014 18:03:55 +0300 Subject: [PATCH 09/19] Upgrade not about `Security` component adjusted --- framework/UPGRADE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index beb1753..75dd2c1 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -75,8 +75,8 @@ Upgrade from Yii 2.0 Beta * Static helper `yii\helpers\Security` has been converted into an application component. You should change all usage of its methods to a new syntax, for example: instead of `yii\helpers\Security::hashData()` use `Yii::$app->getSecurity()->hashData()`. - If you have used `yii\helpers\Security` for encryption or hash generating, you need to explicitly configure 'security' - component for the legacy code support in following way: + Default encryption and hash parameters has been upgraded. If you need to decrypt/validate data that was encrypted/hashed + before, use the following configuration of the 'security' component: ``` return [ 'components' => [ From 4768dcdbc2f7751f3aab3bc6a11f4ea1538d351a Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 27 Jun 2014 21:33:42 +0300 Subject: [PATCH 10/19] Method `Security::compareString()` extracted --- framework/base/Security.php | 35 +++++++++++++++++++----------- tests/unit/framework/base/SecurityTest.php | 3 +++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/framework/base/Security.php b/framework/base/Security.php index 4523e84..73ed472 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -184,12 +184,11 @@ class Security extends Component $calculatedHash = hash_hmac($algorithm, $pureData, $key); - // timing attack resistant approach: - $diff = 0; - for ($i = 0; $i < StringHelper::byteLength($calculatedHash); $i++) { - $diff |= (ord($calculatedHash[$i]) ^ ord($hash[$i])); + if ($this->compareString($hash, $calculatedHash)) { + return $pureData; + } else { + return false; } - return $diff === 0 ? $pureData : false; } else { return false; } @@ -322,14 +321,7 @@ class Security extends Component return false; } - // Use a for-loop to compare two strings to prevent timing attacks. See: - // http://codereview.stackexchange.com/questions/13512 - $check = 0; - for ($i = 0; $i < $n; ++$i) { - $check |= (ord($test[$i]) ^ ord($hash[$i])); - } - - return $check === 0; + return $this->compareString($test, $hash); } /** @@ -365,4 +357,21 @@ class Security extends Component return $salt; } + + /** + * Performs string comparison using timing attack resistant approach. + * @see http://codereview.stackexchange.com/questions/13512 + * @param string $expected string to compare. + * @param string $actual string to compare. + * @return boolean whether strings are equal. + */ + protected function compareString($expected, $actual) + { + // timing attack resistant approach: + $diff = 0; + for ($i = 0; $i < StringHelper::byteLength($actual); $i++) { + $diff |= (ord($actual[$i]) ^ ord($expected[$i])); + } + return $diff === 0; + } } \ No newline at end of file diff --git a/tests/unit/framework/base/SecurityTest.php b/tests/unit/framework/base/SecurityTest.php index 401bb7e..d51db0e 100644 --- a/tests/unit/framework/base/SecurityTest.php +++ b/tests/unit/framework/base/SecurityTest.php @@ -24,8 +24,11 @@ class SecurityTest extends TestCase { parent::setUp(); $this->security = new Security(); + $this->security->derivationIterations = 100; // speed up test running } + // Tests : + public function testPasswordHash() { $password = 'secret'; From 846596294d4f3f747799ea50ae8f8f65ab0cfa5f Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 27 Jun 2014 21:45:52 +0300 Subject: [PATCH 11/19] Fallback for `Security::generateRandomKey()` added --- framework/base/Security.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/framework/base/Security.php b/framework/base/Security.php index 73ed472..bdd882f 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -223,12 +223,17 @@ class Security extends Component /** * Generates a random key. The key may contain uppercase and lowercase latin letters, digits, underscore, dash and dot. + * Note: for delivering high security key, this method requires PHP 'mcrypt' extension. * @param integer $length the length of the key that should be generated * @return string the generated random key */ public function generateRandomKey($length = 32) { - return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + if (function_exists('mcrypt_create_iv')) { + return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + } + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'; + return substr(str_shuffle(str_repeat($chars, 5)), 0, $length); } /** From 4063502439c93107fcb2b38938d236a214b059a7 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 27 Jun 2014 22:10:02 +0300 Subject: [PATCH 12/19] Option `Security::deriveKeyStrategy` added --- framework/base/Security.php | 45 +++++++++++++++++++++++++++++- tests/unit/framework/base/SecurityTest.php | 32 ++++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/framework/base/Security.php b/framework/base/Security.php index bdd882f..5e58cc1 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -6,6 +6,7 @@ */ namespace yii\base; + use yii\helpers\StringHelper; use Yii; @@ -34,22 +35,33 @@ 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 */ public $cryptKeySize = 32; /** * @var string derivation hash algorithm name. + * Recommended value: 'sha256' */ public $derivationHash = 'sha256'; /** * @var integer derivation iterations count. + * Recommended value: 1000000 */ public $derivationIterations = 1000000; + /** + * @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. + */ + public $deriveKeyStrategy = 'hmac'; /** * Encrypts data. @@ -131,20 +143,51 @@ class Security extends Component * Derives a key from the given password (PBKDF2). * @param string $password the source password * @param string $salt the random salt + * @throws InvalidConfigException if unsupported derive key strategy is configured. * @return string the derived key */ protected function deriveKey($password, $salt) { + 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}'"); + } + } + + /** + * Derives a key from the given password using PBKDF2. + * @param string $password the source password + * @param string $salt the random salt + * @throws InvalidConfigException if environment does not allows PBKDF2. + * @return string the derived key + */ + protected function deriveKeyPbkdf2($password, $salt) + { if (function_exists('hash_pbkdf2')) { return hash_pbkdf2($this->derivationHash, $password, $salt, $this->derivationIterations, $this->cryptKeySize, true); + } else { + throw new InvalidConfigException('Derive key strategy "pbkdf2" requires PHP >= 5.5.0, either upgrade your environment or use another strategy.'); } + } + + /** + * 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; } - return substr($xorsum, 0, $this->cryptKeySize); } diff --git a/tests/unit/framework/base/SecurityTest.php b/tests/unit/framework/base/SecurityTest.php index d51db0e..93e8e69 100644 --- a/tests/unit/framework/base/SecurityTest.php +++ b/tests/unit/framework/base/SecurityTest.php @@ -48,8 +48,38 @@ class SecurityTest extends TestCase $this->assertFalse($this->security->validateData($hashedData, $key)); } - public function testEncrypt() + /** + * Data provider for [[testEncrypt()]] + * @return array test data + */ + public function dataProviderEncrypt() + { + return [ + [ + 'hmac', + false + ], + [ + 'pbkdf2', + !function_exists('hash_pbkdf2') + ], + ]; + } + + /** + * @dataProvider dataProviderEncrypt + * + * @param string $deriveKeyStrategy + * @param boolean $isSkipped + */ + public function testEncrypt($deriveKeyStrategy, $isSkipped) { + if ($isSkipped) { + $this->markTestSkipped("Unable to test '{$deriveKeyStrategy}' derive key strategy"); + return; + } + $this->security->deriveKeyStrategy = $deriveKeyStrategy; + $data = 'known data'; $key = 'secret'; $encryptedData = $this->security->encrypt($data, $key); From 4ce4707a3a5a9ab8444982d26b9f318b60770cfc Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 27 Jun 2014 23:05:23 +0300 Subject: [PATCH 13/19] Option `Security::passwordHashStrategy` added --- framework/base/Security.php | 57 ++++++++++++++++++++++-------- tests/unit/framework/base/SecurityTest.php | 42 +++++++++++++++++----- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/framework/base/Security.php b/framework/base/Security.php index 5e58cc1..08b80fc 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -58,10 +58,18 @@ class Security extends Component /** * @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 + * - 'pbkdf2' - PBKDF2 key derivation. This option is recommended, but it requires PHP version >= 5.5.0 * - 'hmac' - HMAC hash key derivation. */ public $deriveKeyStrategy = 'hmac'; + /** + * @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 $passwordHashStrategy = 'crypt'; /** * Encrypts data. @@ -154,7 +162,7 @@ class Security extends Component case 'hmac': return $this->deriveKeyHmac($password, $salt); default: - throw new InvalidConfigException("Unknown Derive key strategy '{$this->deriveKeyStrategy}'"); + throw new InvalidConfigException("Unknown derive key strategy '{$this->deriveKeyStrategy}'"); } } @@ -335,14 +343,23 @@ class Security extends Component */ public function generatePasswordHash($password, $cost = 13) { - $salt = $this->generateSalt($cost); - $hash = crypt($password, $salt); - - if (!is_string($hash) || strlen($hash) < 32) { - throw new Exception('Unknown error occurred while generating hash.'); + switch ($this->passwordHashStrategy) { + case 'password_hash': + 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.'); + } + 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) { + throw new Exception('Unknown error occurred while generating hash.'); + } + return $hash; + default: + throw new InvalidConfigException("Unknown password hash strategy '{$this->passwordHashStrategy}'"); } - - return $hash; } /** @@ -351,6 +368,7 @@ class Security extends Component * @param string $hash The hash to verify the password against. * @return boolean whether the password is correct. * @throws InvalidParamException on bad password or hash parameters or if crypt() with Blowfish hash is not available. + * @throws InvalidConfigException on unsupported password hash strategy is configured. * @see generatePasswordHash() */ public function validatePassword($password, $hash) @@ -363,13 +381,22 @@ class Security extends Component throw new InvalidParamException('Hash is invalid.'); } - $test = crypt($password, $hash); - $n = strlen($test); - if ($n < 32 || $n !== strlen($hash)) { - return false; + switch ($this->passwordHashStrategy) { + case 'password_hash': + if (!function_exists('password_verify')) { + throw new InvalidConfigException('Password hash key strategy "password_hash" requires PHP >= 5.5.0, either upgrade your environment or use another strategy.'); + } + return password_verify($password, $hash); + case 'crypt': + $test = crypt($password, $hash); + $n = strlen($test); + if ($n < 32 || $n !== strlen($hash)) { + return false; + } + return $this->compareString($test, $hash); + default: + throw new InvalidConfigException("Unknown password hash strategy '{$this->passwordHashStrategy}'"); } - - return $this->compareString($test, $hash); } /** diff --git a/tests/unit/framework/base/SecurityTest.php b/tests/unit/framework/base/SecurityTest.php index 93e8e69..a1ad25c 100644 --- a/tests/unit/framework/base/SecurityTest.php +++ b/tests/unit/framework/base/SecurityTest.php @@ -29,14 +29,6 @@ class SecurityTest extends TestCase // Tests : - public function testPasswordHash() - { - $password = 'secret'; - $hash = $this->security->generatePasswordHash($password); - $this->assertTrue($this->security->validatePassword($password, $hash)); - $this->assertFalse($this->security->validatePassword('test', $hash)); - } - public function testHashData() { $data = 'known data'; @@ -48,6 +40,40 @@ class SecurityTest extends TestCase $this->assertFalse($this->security->validateData($hashedData, $key)); } + public function dataProviderPasswordHash() + { + return [ + [ + 'crypt', + false + ], + [ + 'password_hash', + !function_exists('password_hash') + ], + ]; + } + + /** + * @dataProvider dataProviderPasswordHash + * + * @param string $passwordHashStrategy + * @param boolean $isSkipped + */ + public function testPasswordHash($passwordHashStrategy, $isSkipped) + { + if ($isSkipped) { + $this->markTestSkipped("Unable to test '{$passwordHashStrategy}' password hash strategy"); + return; + } + $this->security->passwordHashStrategy = $passwordHashStrategy; + + $password = 'secret'; + $hash = $this->security->generatePasswordHash($password); + $this->assertTrue($this->security->validatePassword($password, $hash)); + $this->assertFalse($this->security->validatePassword('test', $hash)); + } + /** * Data provider for [[testEncrypt()]] * @return array test data From 772667fa1ce70e4971b6e377fce497236d8edea3 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 27 Jun 2014 23:13:57 +0300 Subject: [PATCH 14/19] Doc comments at `Security` updated --- framework/base/Security.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/framework/base/Security.php b/framework/base/Security.php index 08b80fc..213b3b4 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -10,7 +10,6 @@ namespace yii\base; use yii\helpers\StringHelper; use Yii; - /** * Security provides a set of methods to handle common security-related tasks. * @@ -24,6 +23,13 @@ use Yii; * 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. + * * @author Qiang Xue * @author Tom Worster * @author Klimov Paul From 5a429857509000fe766609defa6ad5cd831eda1b Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 27 Jun 2014 23:30:28 +0300 Subject: [PATCH 15/19] Option `Security::useDeriveKeyUniqueSalt` added --- framework/base/Security.php | 30 +++++++++++++++++++++++++----- tests/unit/framework/base/SecurityTest.php | 18 ++++++++++++++++-- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/framework/base/Security.php b/framework/base/Security.php index 213b3b4..344f99c 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -76,6 +76,12 @@ class Security extends Component * - 'crypt' - use PHP `crypt()` function. */ public $passwordHashStrategy = 'crypt'; + /** + * @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 reduce security. + */ + public $useDeriveKeyUniqueSalt = true; /** * Encrypts data. @@ -89,10 +95,18 @@ class Security extends Component { $module = $this->openCryptModule(); $data = $this->addPadding($data); - $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_DEV_URANDOM); - $key = $this->deriveKey($password, $iv); + $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 .= $iv . mcrypt_generic($module, $data); mcrypt_generic_deinit($module); mcrypt_module_close($module); @@ -115,9 +129,15 @@ class Security extends Component $module = $this->openCryptModule(); $ivSize = mcrypt_enc_get_iv_size($module); $iv = StringHelper::byteSubstr($data, 0, $ivSize); - $key = $this->deriveKey($password, $iv); + $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); mcrypt_generic_init($module, $key, $iv); - $decrypted = mdecrypt_generic($module, StringHelper::byteSubstr($data, $ivSize, StringHelper::byteLength($data))); + $decrypted = mdecrypt_generic($module, $encrypted); mcrypt_generic_deinit($module); mcrypt_module_close($module); diff --git a/tests/unit/framework/base/SecurityTest.php b/tests/unit/framework/base/SecurityTest.php index a1ad25c..d33787e 100644 --- a/tests/unit/framework/base/SecurityTest.php +++ b/tests/unit/framework/base/SecurityTest.php @@ -83,10 +83,22 @@ class SecurityTest extends TestCase return [ [ 'hmac', - false + true, + false, + ], + [ + 'hmac', + false, + false, + ], + [ + 'pbkdf2', + true, + !function_exists('hash_pbkdf2') ], [ 'pbkdf2', + false, !function_exists('hash_pbkdf2') ], ]; @@ -96,15 +108,17 @@ class SecurityTest extends TestCase * @dataProvider dataProviderEncrypt * * @param string $deriveKeyStrategy + * @param boolean $useDeriveKeyUniqueSalt * @param boolean $isSkipped */ - public function testEncrypt($deriveKeyStrategy, $isSkipped) + public function testEncrypt($deriveKeyStrategy, $useDeriveKeyUniqueSalt, $isSkipped) { 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'; From 25a36377090b98ebe64c01ff3352ea3740578f4b Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 27 Jun 2014 23:33:16 +0300 Subject: [PATCH 16/19] Upgrade note about `Security` updated --- framework/UPGRADE.md | 3 +++ framework/base/Security.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index 75dd2c1..0b3bd7b 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -84,6 +84,9 @@ Upgrade from Yii 2.0 Beta 'cryptBlockSize' => 16, 'cryptKeySize' => 24, 'derivationIterations' => 1000, + 'deriveKeyStrategy' => 'hmac', // for PHP version < 5.5.0 + //'deriveKeyStrategy' => 'pbkdf2', // for PHP version >= 5.5.0 + 'useDeriveKeyUniqueSalt' => false, ], // ... ], diff --git a/framework/base/Security.php b/framework/base/Security.php index 344f99c..11c77f4 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -79,7 +79,7 @@ class Security extends Component /** * @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 reduce security. + * If disabled this option reduces encrypted text length, but also reduces security. */ public $useDeriveKeyUniqueSalt = true; From 052ae83340338e8579df46151f7d2105c7d6393b Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Sat, 28 Jun 2014 00:11:48 +0300 Subject: [PATCH 17/19] Option `Security::autoGenerateSecretKey` added --- framework/UPGRADE.md | 1 + framework/base/Security.php | 42 +++++++++++++++++++++--------- tests/unit/framework/base/SecurityTest.php | 25 ++++++++++++++++++ 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index 0b3bd7b..9f569e9 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -87,6 +87,7 @@ Upgrade from Yii 2.0 Beta 'deriveKeyStrategy' => 'hmac', // for PHP version < 5.5.0 //'deriveKeyStrategy' => 'pbkdf2', // for PHP version >= 5.5.0 'useDeriveKeyUniqueSalt' => false, + 'autoGenerateSecretKey' => true, ], // ... ], diff --git a/framework/base/Security.php b/framework/base/Security.php index 11c77f4..b9bd6b6 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -82,6 +82,20 @@ class Security extends Component * If disabled this option reduces encrypted text length, but also reduces security. */ public $useDeriveKeyUniqueSalt = true; + /** + * @var array list of predefined secret keys in format: keyVerboseName => keyValue + * While retrieving secret keys [[getSecretKey()]] method usage is recommended. + */ + public $secretKeys = []; + /** + * @var boolean whether to automatically generate secret key, if it is missing at [[secretKeys]] list + * while being requested via [[getSecretKey()]]. + * Usage of this feature is not recommended - it is better to explicitly define list of secret keys. + * However, you may enable this option while project is in development stage to simplify generating of the keys + * list for the future explicit configuration. + * Generated keys can be found under 'runtime' application directory in 'keys.json' file. + */ + public $autoGenerateSecretKey = false; /** * Encrypts data. @@ -273,29 +287,31 @@ class Security extends Component /** * Returns a secret key associated with the specified name. - * If the secret key does not exist, a random key will be generated - * and saved in the file "keys.json" under the application's runtime directory - * so that the same secret key can be returned in future requests. + * If the secret key does not exist and [[autoGenerateSecretKey]] enabled, + * a random key will be generated and saved in the file "keys.json" under the application's runtime + * directory so that the same secret key can be returned in future requests. * @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 + * @throws InvalidParamException if secret key not exist and its generation is not allowed * @return string the secret key associated with the specified name */ public function getSecretKey($name, $length = 32) { - static $keys; - $keyFile = Yii::$app->getRuntimePath() . '/keys.json'; - if ($keys === null) { - $keys = []; + if (!array_key_exists($name, $this->secretKeys)) { + if (!$this->autoGenerateSecretKey) { + throw new InvalidParamException("Unknown secret key '{$name}'"); + } + $keyFile = Yii::$app->getRuntimePath() . '/keys.json'; if (is_file($keyFile)) { $keys = json_decode(file_get_contents($keyFile), true); + $this->secretKeys = array_merge($keys, $this->secretKeys); + } + if (!isset($this->secretKeys[$name])) { + $this->secretKeys[$name] = $this->generateRandomKey($length); + file_put_contents($keyFile, json_encode($this->secretKeys)); } } - if (!isset($keys[$name])) { - $keys[$name] = $this->generateRandomKey($length); - file_put_contents($keyFile, json_encode($keys)); - } - - return $keys[$name]; + return $this->secretKeys[$name]; } /** diff --git a/tests/unit/framework/base/SecurityTest.php b/tests/unit/framework/base/SecurityTest.php index d33787e..661a529 100644 --- a/tests/unit/framework/base/SecurityTest.php +++ b/tests/unit/framework/base/SecurityTest.php @@ -40,6 +40,10 @@ class SecurityTest extends TestCase $this->assertFalse($this->security->validateData($hashedData, $key)); } + /** + * Data provider for [[testPasswordHash()]] + * @return array test data + */ public function dataProviderPasswordHash() { return [ @@ -127,4 +131,25 @@ class SecurityTest extends TestCase $decryptedData = $this->security->decrypt($encryptedData, $key); $this->assertEquals($data, $decryptedData); } + + public function testGetSecretKey() + { + $this->security->autoGenerateSecretKey = false; + $keyName = 'testGet'; + $keyValue = 'testGetValue'; + $this->security->secretKeys = [ + $keyName => $keyValue + ]; + $this->assertEquals($keyValue, $this->security->getSecretKey($keyName)); + + $this->setExpectedException('yii\base\InvalidParamException'); + $this->security->getSecretKey('notExistingKey'); + } + + /*public function testGenerateSecretKey() + { + $this->security->autoGenerateSecretKey = true; + $keyValue = $this->security->getSecretKey('test'); + $this->assertNotEmpty($keyValue); + }*/ } From 69abbc7ff30ffea9bb5b1f54df91d6df7a68e94e Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Sat, 28 Jun 2014 19:02:53 +0300 Subject: [PATCH 18/19] Fallback at `Security::generateRandomKey()` removed --- framework/base/Security.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/framework/base/Security.php b/framework/base/Security.php index b9bd6b6..0354f81 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -180,7 +180,7 @@ class Security extends Component $end = StringHelper::byteSubstr($data, -1, null); $last = ord($end); $n = StringHelper::byteLength($data) - $last; - if (StringHelper::byteSubstr($data, $n, null) == str_repeat($end, $last)) { + if (StringHelper::byteSubstr($data, $n, null) === str_repeat($end, $last)) { return StringHelper::byteSubstr($data, 0, $n); } @@ -322,11 +322,7 @@ class Security extends Component */ public function generateRandomKey($length = 32) { - if (function_exists('mcrypt_create_iv')) { - return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); - } - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'; - return substr(str_shuffle(str_repeat($chars, 5)), 0, $length); + return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); } /** From 84cbf19bfef27669b01451d3a228b6bd0e243afe Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Sat, 28 Jun 2014 19:05:16 +0300 Subject: [PATCH 19/19] Doc comments at `Security::generateRandomKey()` adjusted --- framework/base/Security.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/base/Security.php b/framework/base/Security.php index 0354f81..0c4f8d6 100644 --- a/framework/base/Security.php +++ b/framework/base/Security.php @@ -30,6 +30,8 @@ use Yii; * 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 available, for the highest security level PHP version >= 5.5.0 required. + * * @author Qiang Xue * @author Tom Worster * @author Klimov Paul