Index: composer.json =================================================================== --- composer.json +++ composer.json @@ -1,5 +1,11 @@ { "name": "In-Portal", + "require": { + "php": ">=5.3.3", + "paragonie/random_compat": "^2.0", + "symfony/polyfill-php55": "^1.10", + "symfony/polyfill-php56": "^1.10" + }, "require-dev": { "aik099/phpunit-mink": "~2.0", "qa-tools/qa-tools": "~1.0", @@ -7,5 +13,10 @@ "mockery/mockery": "~0.9", "behat/mink": "~1.6" + }, + "config": { + "platform": { + "php": "5.3.3" + } } } Index: composer.lock =================================================================== --- composer.lock +++ composer.lock @@ -1,11 +1,267 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "hash": "eb74d1a9944355d24b1a70b7895b60c5", - "packages": [], + "content-hash": "c81a156419a19e712f85783a7ad6df76", + "packages": [ + { + "name": "ircmaxell/password-compat", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/password_compat.git", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "autoload": { + "files": [ + "lib/password.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "homepage": "https://github.com/ircmaxell/password_compat", + "keywords": [ + "hashing", + "password" + ], + "time": "2014-11-20T16:49:30+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v2.0.17", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d", + "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "time": "2018-07-04T16:31:37+00:00" + }, + { + "name": "symfony/polyfill-php55", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php55.git", + "reference": "42a4c00a347625ac8853c3358c47eeadc7fd4e96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/42a4c00a347625ac8853c3358c47eeadc7fd4e96", + "reference": "42a4c00a347625ac8853c3358c47eeadc7fd4e96", + "shasum": "" + }, + "require": { + "ircmaxell/password-compat": "~1.0", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php55\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 5.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2018-10-31T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-php56", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php56.git", + "reference": "ff208829fe1aa48ab9af356992bb7199fed551af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/ff208829fe1aa48ab9af356992bb7199fed551af", + "reference": "ff208829fe1aa48ab9af356992bb7199fed551af", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/polyfill-util": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php56\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2018-09-21T06:26:08+00:00" + }, + { + "name": "symfony/polyfill-util", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-util.git", + "reference": "3b58903eae668d348a7126f999b0da0f2f93611c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/3b58903eae668d348a7126f999b0da0f2f93611c", + "reference": "3b58903eae668d348a7126f999b0da0f2f93611c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Util\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony utilities for portability of PHP codes", + "homepage": "https://symfony.com", + "keywords": [ + "compat", + "compatibility", + "polyfill", + "shim" + ], + "time": "2018-09-30T16:36:12+00:00" + } + ], "packages-dev": [ { "name": "aik099/coding-standard", @@ -17,7 +273,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aik099/CodingStandard/zipball/987a6781521c9293b3e36ff3877cdff63010b291", + "url": "https://api.github.com/repos/aik099/CodingStandard/zipball/a47f2b767c99a33f437802b3315179887018e114", "reference": "a47f2b767c99a33f437802b3315179887018e114", "shasum": "" }, @@ -45,7 +301,7 @@ "PHP_CodeSniffer", "codesniffer" ], - "time": "2015-05-06 08:54:38" + "time": "2015-05-06T08:54:38+00:00" }, { "name": "aik099/phpunit-mink", @@ -105,7 +361,7 @@ "selenium", "tests" ], - "time": "2015-05-06 13:33:55" + "time": "2015-05-06T13:33:55+00:00" }, { "name": "behat/mink", @@ -160,7 +416,7 @@ "testing", "web" ], - "time": "2015-02-04 17:02:06" + "time": "2015-02-04T17:02:06+00:00" }, { "name": "behat/mink-selenium2-driver", @@ -218,7 +474,7 @@ "testing", "webdriver" ], - "time": "2014-09-29 13:12:12" + "time": "2014-09-29T13:12:12+00:00" }, { "name": "doctrine/instantiator", @@ -272,7 +528,7 @@ "constructor", "instantiate" ], - "time": "2014-10-13 12:58:55" + "time": "2014-10-13T12:58:55+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -317,7 +573,7 @@ "keywords": [ "test" ], - "time": "2015-05-11 14:41:42" + "time": "2015-05-11T14:41:42+00:00" }, { "name": "instaclick/php-webdriver", @@ -375,7 +631,7 @@ "webdriver", "webtest" ], - "time": "2015-04-05 19:52:55" + "time": "2015-04-05T19:52:55+00:00" }, { "name": "mindplay/annotations", @@ -425,19 +681,19 @@ "annotations", "framework" ], - "time": "2015-04-15 12:54:35" + "time": "2015-04-15T12:54:35+00:00" }, { "name": "mockery/mockery", "version": "0.9.4", "source": { "type": "git", - "url": "https://github.com/padraic/mockery.git", + "url": "https://github.com/mockery/mockery.git", "reference": "70bba85e4aabc9449626651f48b9018ede04f86b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/padraic/mockery/zipball/70bba85e4aabc9449626651f48b9018ede04f86b", + "url": "https://api.github.com/repos/mockery/mockery/zipball/70bba85e4aabc9449626651f48b9018ede04f86b", "reference": "70bba85e4aabc9449626651f48b9018ede04f86b", "shasum": "" }, @@ -490,7 +746,7 @@ "test double", "testing" ], - "time": "2015-04-02 19:54:00" + "time": "2015-04-02T19:54:00+00:00" }, { "name": "phpdocumentor/reflection-docblock", @@ -539,7 +795,7 @@ "email": "mike.vanriel@naenius.com" } ], - "time": "2015-02-03 12:10:50" + "time": "2015-02-03T12:10:50+00:00" }, { "name": "phpspec/prophecy", @@ -599,7 +855,7 @@ "spy", "stub" ], - "time": "2015-04-27 22:15:08" + "time": "2015-04-27T22:15:08+00:00" }, { "name": "phpunit/php-code-coverage", @@ -661,7 +917,7 @@ "testing", "xunit" ], - "time": "2015-04-11 04:35:00" + "time": "2015-04-11T04:35:00+00:00" }, { "name": "phpunit/php-file-iterator", @@ -708,7 +964,7 @@ "filesystem", "iterator" ], - "time": "2015-04-02 05:19:05" + "time": "2015-04-02T05:19:05+00:00" }, { "name": "phpunit/php-text-template", @@ -752,7 +1008,7 @@ "keywords": [ "template" ], - "time": "2014-01-30 17:20:04" + "time": "2014-01-30T17:20:04+00:00" }, { "name": "phpunit/php-timer", @@ -796,7 +1052,7 @@ "keywords": [ "timer" ], - "time": "2013-08-02 07:42:54" + "time": "2013-08-02T07:42:54+00:00" }, { "name": "phpunit/php-token-stream", @@ -845,7 +1101,7 @@ "keywords": [ "tokenizer" ], - "time": "2015-04-08 04:46:07" + "time": "2015-04-08T04:46:07+00:00" }, { "name": "phpunit/phpunit", @@ -917,7 +1173,7 @@ "testing", "xunit" ], - "time": "2015-04-29 15:18:52" + "time": "2015-04-29T15:18:52+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -972,7 +1228,7 @@ "mock", "xunit" ], - "time": "2015-04-02 05:36:41" + "time": "2015-04-02T05:36:41+00:00" }, { "name": "pimple/pimple", @@ -1018,7 +1274,7 @@ "container", "dependency injection" ], - "time": "2014-07-24 09:48:15" + "time": "2014-07-24T09:48:15+00:00" }, { "name": "qa-tools/qa-tools", @@ -1091,7 +1347,7 @@ "phpunit", "tests" ], - "time": "2014-09-26 15:15:51" + "time": "2014-09-26T15:15:51+00:00" }, { "name": "sebastian/comparator", @@ -1155,7 +1411,7 @@ "compare", "equality" ], - "time": "2015-01-29 16:28:08" + "time": "2015-01-29T16:28:08+00:00" }, { "name": "sebastian/diff", @@ -1207,7 +1463,7 @@ "keywords": [ "diff" ], - "time": "2015-02-22 15:13:53" + "time": "2015-02-22T15:13:53+00:00" }, { "name": "sebastian/environment", @@ -1257,7 +1513,7 @@ "environment", "hhvm" ], - "time": "2015-01-01 10:01:08" + "time": "2015-01-01T10:01:08+00:00" }, { "name": "sebastian/exporter", @@ -1323,7 +1579,7 @@ "export", "exporter" ], - "time": "2015-01-27 07:23:06" + "time": "2015-01-27T07:23:06+00:00" }, { "name": "sebastian/global-state", @@ -1374,7 +1630,7 @@ "keywords": [ "global state" ], - "time": "2014-10-06 09:23:50" + "time": "2014-10-06T09:23:50+00:00" }, { "name": "sebastian/recursion-context", @@ -1427,7 +1683,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2015-01-24 09:48:32" + "time": "2015-01-24T09:48:32+00:00" }, { "name": "sebastian/version", @@ -1462,7 +1718,7 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-02-24 06:35:25" + "time": "2015-02-24T06:35:25+00:00" }, { "name": "symfony/css-selector", @@ -1470,12 +1726,12 @@ "target-dir": "Symfony/Component/CssSelector", "source": { "type": "git", - "url": "https://github.com/symfony/CssSelector.git", + "url": "https://github.com/symfony/css-selector.git", "reference": "189cf0f7f56d7c4be3b778df15a7f16a29f3680d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/CssSelector/zipball/189cf0f7f56d7c4be3b778df15a7f16a29f3680d", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/189cf0f7f56d7c4be3b778df15a7f16a29f3680d", "reference": "189cf0f7f56d7c4be3b778df15a7f16a29f3680d", "shasum": "" }, @@ -1516,7 +1772,7 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2015-05-02 15:18:45" + "time": "2015-05-02T15:18:45+00:00" }, { "name": "symfony/event-dispatcher", @@ -1524,12 +1780,12 @@ "target-dir": "Symfony/Component/EventDispatcher", "source": { "type": "git", - "url": "https://github.com/symfony/EventDispatcher.git", + "url": "https://github.com/symfony/event-dispatcher.git", "reference": "672593bc4b0043a0acf91903bb75a1c82d8f2e02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/672593bc4b0043a0acf91903bb75a1c82d8f2e02", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/672593bc4b0043a0acf91903bb75a1c82d8f2e02", "reference": "672593bc4b0043a0acf91903bb75a1c82d8f2e02", "shasum": "" }, @@ -1575,7 +1831,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2015-05-02 15:18:45" + "time": "2015-05-02T15:18:45+00:00" }, { "name": "symfony/yaml", @@ -1625,7 +1881,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-05-02 15:18:45" + "time": "2015-05-02T15:18:45+00:00" } ], "aliases": [], @@ -1635,6 +1891,11 @@ }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": { + "php": ">=5.3.3" + }, + "platform-dev": [], + "platform-overrides": { + "php": "5.3.3" + } } Index: core/install.php =================================================================== --- core/install.php +++ core/install.php @@ -270,7 +270,7 @@ case 'sys_requirements': $required_checks = Array ( 'php_version', 'composer', 'curl', 'simplexml', 'freetype', 'gd_version', - 'jpeg', 'mysql', 'json', 'date.timezone', 'output_buffering', + 'jpeg', 'mysql', 'json', 'openssl', 'date.timezone', 'output_buffering', ); $check_results = $this->toolkit->CallPrerequisitesMethod('core/', 'CheckSystemRequirements'); @@ -836,6 +836,24 @@ } } + if ( !$this->toolkit->systemConfig->get('SecurityHmacKey', 'Misc') + || !$this->toolkit->systemConfig->get('SecurityEncryptionKey', 'Misc') + ) { + $this->toolkit->systemConfig->set( + 'SecurityHmacKey', + 'Misc', + base64_encode(SecurityGenerator::generateString( + SecurityEncrypter::HASHING_KEY_LENGTH, + SecurityGenerator::CHAR_ALNUM | SecurityGenerator::CHAR_SYMBOLS + )) + ); + $this->toolkit->systemConfig->set( + 'SecurityEncryptionKey', + 'Misc', + SecurityGenerator::generateBytes(SecurityEncrypter::ENCRYPTION_KEY_LENGTH) + ); + } + $this->toolkit->systemConfig->save(); break; Index: core/install/prerequisites.php =================================================================== --- core/install/prerequisites.php +++ core/install/prerequisites.php @@ -145,6 +145,9 @@ $ret['mysql'] = function_exists('mysqli_connect'); $ret['json'] = function_exists('json_encode'); + // Unable to properly use "SecurityEncrypter::cipherAvailable" method, because no factory here. + $ret['openssl'] = function_exists('openssl_encrypt') && in_array('aes-128-cbc', openssl_get_cipher_methods(), true); + $output = shell_exec('java -version 2>&1'); $ret['java'] = stripos($output, 'java version') !== false; Index: core/install/step_templates/sys_requirements.tpl =================================================================== --- core/install/step_templates/sys_requirements.tpl +++ core/install/step_templates/sys_requirements.tpl @@ -25,6 +25,7 @@ 'jpeg' => '- JPEG images support*', 'mysql' => '- Database connectivity (via MySQL)*', 'json' => '- JSON processing support*', + 'openssl' => '- OpenSSL support*', 'sep2' => 'PHP settings:', 'memory_limit' => "- Memory requirements changing on the fly", 'display_errors' => "- Prevent script errors in production environment", Index: core/kernel/application.php =================================================================== --- core/kernel/application.php +++ core/kernel/application.php @@ -756,6 +756,11 @@ $this->registerClass('kCache', KERNEL_PATH . '/utility/cache.php', 'kCache', 'Params'); $this->registerClass('kHTTPQuery', KERNEL_PATH . '/utility/http_query.php', 'HTTPQuery'); + // security + $this->registerClass('SecurityGenerator', KERNEL_PATH . '/security/SecurityGenerator.php'); + $this->registerClass('SecurityGeneratorPromise', KERNEL_PATH . '/security/SecurityGeneratorPromise.php'); + $this->registerClass('SecurityEncrypter', KERNEL_PATH . '/security/SecurityEncrypter.php'); + // session $this->registerClass('Session', KERNEL_PATH . '/session/session.php'); $this->registerClass('SessionStorage', KERNEL_PATH . '/session/session_storage.php'); Index: core/kernel/security/SecurityEncrypter.php =================================================================== --- /dev/null +++ core/kernel/security/SecurityEncrypter.php @@ -0,0 +1,228 @@ +getData(); + + // In the config data is encoded stored to avoid corruption. + $this->hmacKey = base64_decode($vars['SecurityHmacKey']); + + // Equivalent of "hex2bin" for PHP < 5.4. + $this->encryptionKey = pack('H*', $vars['SecurityEncryptionKey']); + } + + /** + * Creates signature. + * + * @param string $string String. + * @param boolean $raw_output Return raw signature version. + * @param string|null $key_override Override key. + * + * @return string + */ + public function createSignature($string, $raw_output = false, $key_override = null) + { + return hash_hmac(self::HASHING_ALGORITHM, $string, $this->_getHmacKey($key_override), $raw_output); + } + + /** + * Returns HMAC key. + * + * @param string|null $key_override Key override. + * + * @return string + * @throws InvalidArgumentException When HMAC key is empty. + */ + private function _getHmacKey($key_override = null) + { + $key = $this->doGetHmacKey($key_override); + + if ( empty($key) ) { + throw new InvalidArgumentException('The HMAC key is empty.'); + } + + if ( mb_strlen($key, '8bit') === self::HASHING_KEY_LENGTH ) { + return $key; + } + + return $this->deriveKey($key, self::HASHING_KEY_LENGTH); + } + + /** + * Returns HMAC key. + * + * @param string|null $key_override Key override. + * + * @return string + */ + protected function doGetHmacKey($key_override = null) + { + return isset($key_override) ? $key_override : $this->hmacKey; + } + + /** + * Returns encryption key. + * + * @param string|null $key_override Key override. + * + * @return string + * @throws InvalidArgumentException When encryption key is empty. + */ + private function _getEncryptionKey($key_override = null) + { + $key = $this->doGetEncryptionKey($key_override); + + if ( empty($key) ) { + throw new InvalidArgumentException('The encryption key is empty.'); + } + + if ( mb_strlen($key, '8bit') === self::ENCRYPTION_KEY_LENGTH ) { + return $key; + } + + return $this->deriveKey($key, self::ENCRYPTION_KEY_LENGTH); + } + + /** + * Returns encryption key. + * + * @param string|null $key_override Key override. + * + * @return string + */ + protected function doGetEncryptionKey($key_override = null) + { + return isset($key_override) ? $key_override : $this->encryptionKey; + } + + /** + * Derives given key. + * + * @param string $key Key. + * @param integer $length Length. + * + * @return string + */ + final public function deriveKey($key, $length) + { + $salt = SecurityGenerator::generateBytes(16, true); + + return hash_pbkdf2(self::HASHING_ALGORITHM, $key, $salt, 80000, $length, true); + } + + /** + * Encrypts a plain text. + * + * @param string $plaintext Plain text. + * @param string|null $key_override Key override. + * + * @return string + */ + final public function encrypt($plaintext, $key_override = null) + { + $iv_size = openssl_cipher_iv_length(self::ENCRYPTION_METHOD); + $iv = openssl_random_pseudo_bytes($iv_size); + + $ciphertext = openssl_encrypt( + $plaintext, + self::ENCRYPTION_METHOD, + $this->_getEncryptionKey($key_override), + OPENSSL_RAW_DATA, + $iv + ); + + // Note: We cover the IV in our HMAC. + $hmac = $this->createSignature($iv . $ciphertext, true); + + return base64_encode($hmac . $iv . $ciphertext); + } + + /** + * Decrypts a cipher text. + * + * @param string $ciphertext Cipher text. + * @param string|null $key_override Key override. + * + * @return string|false + * @throws LogicException When signature verification has failed. + */ + final public function decrypt($ciphertext, $key_override = null) + { + $iv_size = openssl_cipher_iv_length(self::ENCRYPTION_METHOD); + + $decoded = base64_decode($ciphertext); + $hmac = mb_substr($decoded, 0, self::HASHING_KEY_LENGTH, '8bit'); + $iv = mb_substr($decoded, self::HASHING_KEY_LENGTH, $iv_size, '8bit'); + $ciphertext = mb_substr($decoded, self::HASHING_KEY_LENGTH + $iv_size, null, '8bit'); + + $calculated = $this->createSignature($iv . $ciphertext, true); + + if ( !hash_equals($hmac, $calculated) ) { + throw new LogicException('Signature verification of ciphertext failed.'); + } + + return openssl_decrypt( + $ciphertext, + self::ENCRYPTION_METHOD, + $this->_getEncryptionKey($key_override), + OPENSSL_RAW_DATA, + $iv + ); + } + + /** + * Determines if used cipher is available. + * + * @return boolean + */ + final public function cipherAvailable() + { + return in_array(self::ENCRYPTION_METHOD, openssl_get_cipher_methods(), true); + } + +} Index: core/kernel/security/SecurityGenerator.php =================================================================== --- /dev/null +++ core/kernel/security/SecurityGenerator.php @@ -0,0 +1,193 @@ + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + self::CHAR_LOWER => 'abcdefghijklmnopqrstuvwxyz', + self::CHAR_DIGITS => '0123456789', + self::CHAR_UPPER_HEX => 'ABCDEF', + self::CHAR_LOWER_HEX => 'abcdef', + self::CHAR_BASE64 => '+/', + self::CHAR_SYMBOLS => '!"#$%&\'()* +,-./:;<=>?@[\]^_`{|}~', + self::CHAR_BRACKETS => '()[]{}<>', + self::CHAR_PUNCT => ',.;:', + ); + + /** + * Generate a random string of specified length using supplied character list. + * + * @param integer $length The length of the generated string. + * @param mixed $characters List of characters to use or character flags. + * + * @return SecurityGeneratorPromise + */ + public static function generateString($length, $characters) + { + // Combine character sets. + if ( is_int($characters) ) { + $characters = self::expandCharacterSets($characters); + } + + return new SecurityGeneratorPromise( + SecurityGeneratorPromise::TYPE_STRING, + array($length, $characters) + ); + } + + /** + * Expand a character set bitwise spec into a string character set. + * This will also replace EASY_TO_READ characters if the flag is set. + * + * @param integer $spec The spec to expand (bitwise combination of flags). + * + * @return string The expanded string + */ + protected static function expandCharacterSets($spec) + { + $combined = ''; + + if ( $spec == self::EASY_TO_READ ) { + $spec |= self::CHAR_ALNUM; + } + + foreach ( self::$charArrays as $flag => $chars ) { + // Handle this later. + if ( $flag == self::EASY_TO_READ ) { + continue; + } + + if ( ($spec & $flag) === $flag ) { + $combined .= $chars; + } + } + + // Remove ambiguous characters. + if ( $spec & self::EASY_TO_READ ) { + $combined = str_replace(str_split(self::AMBIGUOUS_CHARS), '', $combined); + } + + return count_chars($combined, 3); + } + + /** + * Generates a random number. + * + * @param integer $min Smallest value. + * @param integer $max Largest value. + * + * @return SecurityGeneratorPromise + */ + public static function generateNumber($min, $max) + { + return new SecurityGeneratorPromise( + SecurityGeneratorPromise::TYPE_NUMBER, + array($min, $max) + ); + } + + /** + * Generates random bytes. + * + * @param integer $length Raw result length. + * @param boolean $raw_output Return raw result. + * + * @return SecurityGeneratorPromise + */ + public static function generateBytes($length = 16, $raw_output = false) + { + return new SecurityGeneratorPromise( + SecurityGeneratorPromise::TYPE_BYTES, + array($length, $raw_output) + ); + } + +} Index: core/kernel/security/SecurityGeneratorPromise.php =================================================================== --- /dev/null +++ core/kernel/security/SecurityGeneratorPromise.php @@ -0,0 +1,274 @@ + 'generateNumber', + self::TYPE_STRING => 'generateString', + self::TYPE_BYTES => 'generateBytes', + ); + + if ( !isset($mapping[$type]) ) { + throw new InvalidArgumentException('The "' . $type . '" promise type is not supported.'); + } + + $this->method = $mapping[$type]; + $this->arguments = $arguments; + } + + /** + * Makes sure, that resolved value isn't already used in the given database table's column. + * + * @param string $prefix_or_table Unit config prefix or table name. + * @param string $column Column in specified table. + * + * @return mixed + * @throws LogicException When after 10 retries still unable to generate unique value for database. + */ + public function resolveForPersisting($prefix_or_table, $column) + { + $application =& kApplication::Instance(); + + if ( $application->prefixRegistred($prefix_or_table) ) { + $table = $application->getUnitOption($prefix_or_table, 'TableName'); + } + else { + $table = $prefix_or_table; + } + + $retries = 0; + + do { + $resolved_value = $this->resolve(); + + $sql = 'SELECT ' . $column . ' + FROM ' . $table . ' + WHERE ' . $column . ' = ' . $application->Conn->qstr($resolved_value); + $found = $application->Conn->GetOne($sql) !== false; + + if ( $found ) { + $this->resolvedValue = null; + $retries++; + } + } while ( $found && $retries < 10 ); + + if ( $found ) { + throw new LogicException(sprintf( + 'Unable to generate unique value for "%s" column with current generator configuration.', + $table . '.' . $column + )); + } + + return $resolved_value; + } + + /** + * Resolves a promise. + * + * @return mixed + */ + public function resolve() + { + if ( !$this->isResolved() ) { + $this->resolvedValue = call_user_func_array(array($this, $this->method), $this->arguments); + } + + if ( !$this->asSignature ) { + return $this->resolvedValue; + } + + $application =& kApplication::Instance(); + + /** @var SecurityEncrypter $encrypter */ + $encrypter = $application->recallObject('SecurityEncrypter'); + + return $encrypter->createSignature( + $this->resolvedValue, + $this->signatureRawOutput, + $this->signatureKeyOverride + ); + } + + /** + * Configures promise to return signature of a resolved value. + * + * @param boolean $raw_output Return raw signature value during resolving. + * @param string|null $key_override Alternative key used for creating signature of resolved value. + * + * @return self + */ + public function asSignature($raw_output = false, $key_override = null) + { + $this->asSignature = true; + $this->signatureRawOutput = $raw_output; + $this->signatureKeyOverride = $key_override; + + return $this; + } + + /** + * Configures promise to return resolved value as-is. + * + * @return self + */ + public function asValue() + { + $this->asSignature = false; + $this->signatureRawOutput = false; + $this->signatureKeyOverride = null; + + return $this; + } + + /** + * Generate a random string of specified length using supplied character list. + * + * @param integer $length The length of the generated string. + * @param mixed $characters List of characters to use. + * + * @return string + */ + protected function generateString($length, $characters) + { + $ret = ''; + $max_index = mb_strlen($characters) - 1; + + for ( $i = 0; $i < $length; $i++ ) { + $ret .= mb_substr($characters, $this->generateNumber(0, $max_index), 1); + } + + return $ret; + } + + /** + * Generates a random number. + * + * @param integer $min Smallest value. + * @param integer $max Largest value. + * + * @return integer + */ + protected function generateNumber($min, $max) + { + return random_int($min, $max); + } + + /** + * Generates random bytes. + * + * @param integer $length Raw result length. + * @param boolean $raw_output Return raw result. + * + * @return string + */ + protected function generateBytes($length = 16, $raw_output = false) + { + $ret = random_bytes($length); + + return $raw_output ? $ret : bin2hex($ret); + } + + /** + * Determines if promise was already resolved. + * + * @return boolean + */ + protected function isResolved() + { + return $this->resolvedValue !== null; + } + + /** + * Returns promise resolved value casted to string. + * + * @return string + */ + public function __toString() + { + return (string)$this->resolve(); + } + +} Index: core/kernel/utility/system_config.php =================================================================== --- core/kernel/utility/system_config.php +++ core/kernel/utility/system_config.php @@ -84,6 +84,8 @@ 'WebsiteCharset' => 'utf-8', 'WebsitePath' => rtrim(preg_replace('/'.preg_quote(rtrim(defined('REL_PATH') ? REL_PATH : '', '/'), '/').'$/', '', str_replace('\\', '/', dirname($_SERVER['PHP_SELF']))), '/'), 'WriteablePath' => DIRECTORY_SEPARATOR . 'system', + 'SecurityHmacKey' => '', + 'SecurityEncryptionKey' => '', ); return $this->parseSections ? array('Misc' => $ret) : $ret;