|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701 |
- <?php
-
-
- namespace yii\base;
-
- use yii\helpers\StringHelper;
- use Yii;
-
-
- class Security extends Component
- {
-
-
- public $cipher = 'AES-128-CBC';
-
-
- public $allowedCiphers = [
- 'AES-128-CBC' => [16, 16],
- 'AES-192-CBC' => [16, 24],
- 'AES-256-CBC' => [16, 32],
- ];
-
-
- public $kdfHash = 'sha256';
-
-
- public $macHash = 'sha256';
-
-
- public $authKeyInfo = 'AuthorizationKey';
-
-
- public $derivationIterations = 100000;
-
-
- public $passwordHashStrategy;
-
-
- public $passwordHashCost = 13;
-
-
-
-
- public function encryptByPassword($data, $password)
- {
- return $this->encrypt($data, true, $password, null);
- }
-
-
-
- public function encryptByKey($data, $inputKey, $info = null)
- {
- return $this->encrypt($data, false, $inputKey, $info);
- }
-
-
-
- public function decryptByPassword($data, $password)
- {
- return $this->decrypt($data, true, $password, null);
- }
-
-
-
- public function decryptByKey($data, $inputKey, $info = null)
- {
- return $this->decrypt($data, false, $inputKey, $info);
- }
-
-
-
- protected function encrypt($data, $passwordBased, $secret, $info)
- {
- if (!extension_loaded('openssl')) {
- throw new InvalidConfigException('Encryption requires the OpenSSL PHP extension');
- }
- if (!isset($this->allowedCiphers[$this->cipher][0], $this->allowedCiphers[$this->cipher][1])) {
- throw new InvalidConfigException($this->cipher . ' is not an allowed cipher');
- }
-
- list($blockSize, $keySize) = $this->allowedCiphers[$this->cipher];
-
- $keySalt = $this->generateRandomKey($keySize);
- if ($passwordBased) {
- $key = $this->pbkdf2($this->kdfHash, $secret, $keySalt, $this->derivationIterations, $keySize);
- } else {
- $key = $this->hkdf($this->kdfHash, $secret, $keySalt, $info, $keySize);
- }
-
- $iv = $this->generateRandomKey($blockSize);
-
- $encrypted = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
- if ($encrypted === false) {
- throw new \yii\base\Exception('OpenSSL failure on encryption: ' . openssl_error_string());
- }
-
- $authKey = $this->hkdf($this->kdfHash, $key, null, $this->authKeyInfo, $keySize);
- $hashed = $this->hashData($iv . $encrypted, $authKey);
-
-
-
- return $keySalt . $hashed;
- }
-
-
-
- protected function decrypt($data, $passwordBased, $secret, $info)
- {
- if (!extension_loaded('openssl')) {
- throw new InvalidConfigException('Encryption requires the OpenSSL PHP extension');
- }
- if (!isset($this->allowedCiphers[$this->cipher][0], $this->allowedCiphers[$this->cipher][1])) {
- throw new InvalidConfigException($this->cipher . ' is not an allowed cipher');
- }
-
- list($blockSize, $keySize) = $this->allowedCiphers[$this->cipher];
-
- $keySalt = StringHelper::byteSubstr($data, 0, $keySize);
- if ($passwordBased) {
- $key = $this->pbkdf2($this->kdfHash, $secret, $keySalt, $this->derivationIterations, $keySize);
- } else {
- $key = $this->hkdf($this->kdfHash, $secret, $keySalt, $info, $keySize);
- }
-
- $authKey = $this->hkdf($this->kdfHash, $key, null, $this->authKeyInfo, $keySize);
- $data = $this->validateData(StringHelper::byteSubstr($data, $keySize, null), $authKey);
- if ($data === false) {
- return false;
- }
-
- $iv = StringHelper::byteSubstr($data, 0, $blockSize);
- $encrypted = StringHelper::byteSubstr($data, $blockSize, null);
-
- $decrypted = openssl_decrypt($encrypted, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
- if ($decrypted === false) {
- throw new \yii\base\Exception('OpenSSL failure on decryption: ' . openssl_error_string());
- }
-
- return $decrypted;
- }
-
-
-
- public function hkdf($algo, $inputKey, $salt = null, $info = null, $length = 0)
- {
- $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_int($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;
- }
-
-
-
- public function pbkdf2($algo, $password, $salt, $iterations, $length = 0)
- {
- if (function_exists('hash_pbkdf2')) {
- $outputKey = hash_pbkdf2($algo, $password, $salt, $iterations, $length, true);
- if ($outputKey === false) {
- throw new InvalidParamException('Invalid parameters to hash_pbkdf2()');
- }
- return $outputKey;
- }
-
-
- $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_int($iterations) || $iterations < 1) {
- throw new InvalidParamException('Invalid iterations');
- }
- if (is_string($length) && preg_match('{^\d{1,16}$}', $length)) {
- $length = (int) $length;
- }
- if (!is_int($length) || $length < 0) {
- throw new InvalidParamException('Invalid length');
- }
- $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;
- }
-
-
-
- public function hashData($data, $key, $rawHash = false)
- {
- $hash = hash_hmac($this->macHash, $data, $key, $rawHash);
- if (!$hash) {
- throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash);
- }
- return $hash . $data;
- }
-
-
-
- public function validateData($data, $key, $rawHash = false)
- {
- $test = @hash_hmac($this->macHash, '', '', $rawHash);
- if (!$test) {
- throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash);
- }
- $hashLength = StringHelper::byteLength($test);
- if (StringHelper::byteLength($data) >= $hashLength) {
- $hash = StringHelper::byteSubstr($data, 0, $hashLength);
- $pureData = StringHelper::byteSubstr($data, $hashLength, null);
-
- $calculatedHash = hash_hmac($this->macHash, $pureData, $key, $rawHash);
-
- if ($this->compareString($hash, $calculatedHash)) {
- return $pureData;
- }
- }
- return false;
- }
-
- private $_useLibreSSL;
- private $_randomFile;
-
-
-
- public function generateRandomKey($length = 32)
- {
- if (!is_int($length)) {
- throw new InvalidParamException('First parameter ($length) must be an integer');
- }
-
- if ($length < 1) {
- throw new InvalidParamException('First parameter ($length) must be greater than 0');
- }
-
-
- if (function_exists('random_bytes')) {
- return random_bytes($length);
- }
-
-
-
-
- if ($this->_useLibreSSL === null) {
- $this->_useLibreSSL = defined('OPENSSL_VERSION_TEXT')
- && preg_match('{^LibreSSL (\d\d?)\.(\d\d?)\.(\d\d?)$}', OPENSSL_VERSION_TEXT, $matches)
- && (10000 * $matches[1]) + (100 * $matches[2]) + $matches[3] >= 20105;
- }
-
-
-
- if ($this->_useLibreSSL
- || (
- DIRECTORY_SEPARATOR !== '/'
- && substr_compare(PHP_OS, 'win', 0, 3, true) === 0
- && function_exists('openssl_random_pseudo_bytes')
- )
- ) {
- $key = openssl_random_pseudo_bytes($length, $cryptoStrong);
- if ($cryptoStrong === false) {
- throw new Exception(
- 'openssl_random_pseudo_bytes() set $crypto_strong false. Your PHP setup is insecure.'
- );
- }
- if ($key !== false && StringHelper::byteLength($key) === $length) {
- return $key;
- }
- }
-
-
-
- if (function_exists('mcrypt_create_iv')) {
- $key = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
- if (StringHelper::byteLength($key) === $length) {
- return $key;
- }
- }
-
-
- if ($this->_randomFile === null && DIRECTORY_SEPARATOR === '/') {
-
- $device = PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom';
-
-
- $lstat = @lstat($device);
- if ($lstat !== false && ($lstat['mode'] & 0170000) === 020000) {
- $this->_randomFile = fopen($device, 'rb') ?: null;
-
- if (is_resource($this->_randomFile)) {
-
-
-
- $bufferSize = 8;
-
- if (function_exists('stream_set_read_buffer')) {
- stream_set_read_buffer($this->_randomFile, $bufferSize);
- }
-
- if (function_exists('stream_set_chunk_size')) {
- stream_set_chunk_size($this->_randomFile, $bufferSize);
- }
- }
- }
- }
-
- if (is_resource($this->_randomFile)) {
- $buffer = '';
- $stillNeed = $length;
- while ($stillNeed > 0) {
- $someBytes = fread($this->_randomFile, $stillNeed);
- if ($someBytes === false) {
- break;
- }
- $buffer .= $someBytes;
- $stillNeed -= StringHelper::byteLength($someBytes);
- if ($stillNeed === 0) {
-
- return $buffer;
- }
- }
- fclose($this->_randomFile);
- $this->_randomFile = null;
- }
-
- throw new Exception('Unable to generate a random key');
- }
-
-
-
- public function generateRandomString($length = 32)
- {
- if (!is_int($length)) {
- throw new InvalidParamException('First parameter ($length) must be an integer');
- }
-
- if ($length < 1) {
- throw new InvalidParamException('First parameter ($length) must be greater than 0');
- }
-
- $bytes = $this->generateRandomKey($length);
-
-
- return strtr(substr(base64_encode($bytes), 0, $length), '+/', '_-');
- }
-
-
-
- public function generatePasswordHash($password, $cost = null)
- {
- if ($cost === null) {
- $cost = $this->passwordHashCost;
- }
-
- if (function_exists('password_hash')) {
-
- return password_hash($password, PASSWORD_DEFAULT, ['cost' => $cost]);
- }
-
- $salt = $this->generateSalt($cost);
- $hash = crypt($password, $salt);
-
- if (!is_string($hash) || strlen($hash) !== 60) {
- throw new Exception('Unknown error occurred while generating hash.');
- }
-
- return $hash;
- }
-
-
-
- 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.');
- }
-
- if (function_exists('password_verify')) {
- return password_verify($password, $hash);
- }
-
- $test = crypt($password, $hash);
- $n = strlen($test);
- if ($n !== 60) {
- return false;
- }
-
- return $this->compareString($test, $hash);
- }
-
-
-
- protected function generateSalt($cost = 13)
- {
- $cost = (int) $cost;
- if ($cost < 4 || $cost > 31) {
- throw new InvalidParamException('Cost must be between 4 and 31.');
- }
-
-
- $rand = $this->generateRandomKey(20);
-
- $salt = sprintf("$2y$%02d$", $cost);
-
- $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22));
-
- return $salt;
- }
-
-
-
- public function compareString($expected, $actual)
- {
- $expected .= "\0";
- $actual .= "\0";
- $expectedLength = StringHelper::byteLength($expected);
- $actualLength = StringHelper::byteLength($actual);
- $diff = $expectedLength - $actualLength;
- for ($i = 0; $i < $actualLength; $i++) {
- $diff |= (ord($actual[$i]) ^ ord($expected[$i % $expectedLength]));
- }
- return $diff === 0;
- }
- }
|