diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f54dc58..816f93d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php_version: [8.1, 8.2, 8.3] + php_version: [8.1, 8.2, 8.3, 8.4] composer_flags: ['', '--prefer-lowest'] steps: @@ -20,7 +20,7 @@ jobs: extensions: xdebug - name: Install dependencies - uses: php-actions/composer@v5 + uses: php-actions/composer@v6 with: php_version: ${{ matrix.php_version }} args: ${{ matrix.composer_flags }} @@ -44,4 +44,3 @@ jobs: # run: | # composer global require php-coveralls/php-coveralls # ~/.composer/vendor/bin/php-coveralls -v - diff --git a/.gitignore b/.gitignore index 188e4c4..3882089 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor composer.lock /tests/Unit/logs +.phpunit.result.cache diff --git a/composer.json b/composer.json index 0cf2c1c..6e44d49 100644 --- a/composer.json +++ b/composer.json @@ -4,36 +4,41 @@ "license": "MIT", "require": { "php": ">=8.1", - "php-di/php-di": "^6.4.0", - "rareloop/router": "^6.0.2", - "psr/container": "^1.1.2", - "psr/http-message": "^1.1", + "php-di/php-di": "^7.1.1", + "rareloop/router": "^6.0.3", + "psr/container": "^2.0.2", + "psr/http-message": "^2", "psr/http-server-middleware": "^1.0.2", - "blast/facades": "^1.0", - "timber/timber": "^2.3.0", - "monolog/monolog": "^2.9.1", - "http-interop/response-sender": "^1.0", - "symfony/debug": "^4.4.44", - "illuminate/collections": "^8.53.1||^9.52.16", + "timber/timber": "^2.3.3", + "monolog/monolog": "^3.9", + "illuminate/collections": "^10.49", "statamic/stringy": "^3.1.3", - "laminas/laminas-diactoros": "^2.25.2", - "rareloop/psr7-server-request-extension": "^2.1.0", - "mmeyer2k/dcrypt": "^8.3.1", + "laminas/laminas-diactoros": "^3.6.0", + "rareloop/psr7-server-request-extension": "^2.2.0", "spatie/macroable": "^1.0.1", - "mindplay/middleman": "^3.1.0", - "psr/log": "^1.1.4", - "laminas/laminas-zendframework-bridge": "^1.7", - "symfony/var-dumper": "^5.0||^6.3.6" + "mindplay/middleman": "^4.0.4", + "psr/log": "^2.0.0", + "symfony/var-dumper": "^6.4.26", + "spatie/ignition": "^1.15.1", + "laminas/laminas-httphandlerrunner": "^2.13", + "symfony/error-handler": "^6.4.26", + "illuminate/support": "^10.49", + "illuminate/pipeline": "^10.49", + "spatie/backtrace": "^1.8.1", + "illuminate/conditionable": "^10.49", + "guzzlehttp/guzzle": "^7.10", + "spatie/flare-client-php": "^1.10.1" }, "require-dev": { - "phpunit/phpunit": "^9.6.13", - "php-coveralls/php-coveralls": "^2.6", - "mockery/mockery": "^1.6.6", - "brain/monkey": "^2.6.1", - "squizlabs/php_codesniffer": "^3.7.2", - "php-mock/php-mock": "^2.4.1", - "mikey179/vfsstream": "1.6.11", - "dms/phpunit-arraysubset-asserts": "^0.3.1" + "phpunit/phpunit": "^9.6.29", + "php-coveralls/php-coveralls": "^2.8", + "mockery/mockery": "^1.6.12", + "brain/monkey": "^2.6.2", + "squizlabs/php_codesniffer": "^3.13.4", + "php-mock/php-mock": "^2.6.2", + "mikey179/vfsstream": "^1.6.12", + "dms/phpunit-arraysubset-asserts": "^0.5.0", + "antecedent/patchwork": "^2.2.3" }, "autoload": { "psr-4": { @@ -46,8 +51,9 @@ } }, "config": { + "preferred-install": "dist", "allow-plugins": { "composer/installers": true } } -} \ No newline at end of file +} diff --git a/src/Application.php b/src/Application.php index 22f7443..599f9fd 100644 --- a/src/Application.php +++ b/src/Application.php @@ -3,15 +3,13 @@ namespace Rareloop\Lumberjack; use Closure; -use DI\ContainerBuilder; +use DI\Container; use Illuminate\Support\Collection; -use Interop\Container\ContainerInterface as InteropContainerInterface; +use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; -use Rareloop\Router\Invoker; -use function Http\Response\send; -class Application implements ContainerInterface, InteropContainerInterface +class Application implements ContainerInterface { private $container; private $loadedProviders = []; @@ -24,7 +22,7 @@ class Application implements ContainerInterface, InteropContainerInterface public function __construct($basePath = false) { - $this->container = ContainerBuilder::buildDevContainer(); + $this->container = new Container(); $this->bind(Application::class, $this); @@ -152,7 +150,7 @@ private function isSingletonClassBind($id) * * @return bool */ - public function has($id) + public function has($id): bool { return $this->container->has($id); } @@ -234,7 +232,7 @@ public function bootstrapWith(array $bootstrappers) * * @return boolean */ - public function hasRequestBeenHandled() : bool + public function hasRequestBeenHandled(): bool { return $this->requestHandled; } @@ -265,17 +263,17 @@ public function detectWhenRequestHasNotBeenHandled() if ($this->has('__wp-controller-miss-template') && $this->has('__wp-controller-miss-controller')) { wp_die( 'Loaded template ' . - $this->get('__wp-controller-miss-template') . - ' but couldn\'t find class ' . - $this->get('__wp-controller-miss-controller') . - '' + $this->get('__wp-controller-miss-template') . + ' but couldn\'t find class ' . + $this->get('__wp-controller-miss-controller') . + '' ); } } }); } - public function shutdown(ResponseInterface $response = null) + public function shutdown(?ResponseInterface $response = null) { if ($response) { global $wp; @@ -284,13 +282,13 @@ public function shutdown(ResponseInterface $response = null) // If we're handling a WordPressController response at this point then WordPress will already have // sent headers as it happens earlier in the lifecycle. For this scenario we need to do a bit more // work to make sure that duplicate headers are not sent back. - send($this->removeSentHeadersAndMoveIntoResponse($response)); + (new SapiEmitter())->emit($this->removeSentHeadersAndMoveIntoResponse($response)); } die(); } - protected function removeSentHeadersAndMoveIntoResponse(ResponseInterface $response) : ResponseInterface + protected function removeSentHeadersAndMoveIntoResponse(ResponseInterface $response): ResponseInterface { // 1. Format the previously sent headers into an array of [key, value] // 2. Remove all headers from the output that we find diff --git a/src/Bootstrappers/RegisterExceptionHandler.php b/src/Bootstrappers/RegisterExceptionHandler.php index b4ff838..64e946d 100644 --- a/src/Bootstrappers/RegisterExceptionHandler.php +++ b/src/Bootstrappers/RegisterExceptionHandler.php @@ -2,16 +2,18 @@ namespace Rareloop\Lumberjack\Bootstrappers; -use DI\NotFoundException; use Error; use ErrorException; -use Psr\Http\Message\ResponseInterface; +use DI\NotFoundException; +use function Http\Response\send; +use Rareloop\Router\Responsable; use Rareloop\Lumberjack\Application; +use Psr\Http\Message\ResponseInterface; +use Laminas\Diactoros\ServerRequestFactory; +use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Rareloop\Lumberjack\Exceptions\HandlerInterface; -use Rareloop\Router\Responsable; use Symfony\Component\Debug\Exception\FatalErrorException; -use Zend\Diactoros\ServerRequestFactory; -use function Http\Response\send; +use Symfony\Component\ErrorHandler\Error\FatalError; /** * Determine whether or not we should be in debug mode or not @@ -70,7 +72,7 @@ public function handleException($e) public function send(ResponseInterface $response) { - @send($response); + @(new SapiEmitter())->emit($response); } protected function getExceptionHandler(): HandlerInterface @@ -122,16 +124,14 @@ public function handleShutdown() * * @param array $error * @param int|null $traceOffset - * @return \Symfony\Component\Debug\Exception\FatalErrorException + * @return \Symfony\Component\ErrorHandler\Error\FatalError */ protected function fatalExceptionFromError(array $error, $traceOffset = null) { - return new FatalErrorException( + return new FatalError( $error['message'], - $error['type'], 0, - $error['file'], - $error['line'], + $error, $traceOffset ); } diff --git a/src/Bootstrappers/RegisterFacades.php b/src/Bootstrappers/RegisterFacades.php index d5b34a2..53628a0 100644 --- a/src/Bootstrappers/RegisterFacades.php +++ b/src/Bootstrappers/RegisterFacades.php @@ -2,8 +2,8 @@ namespace Rareloop\Lumberjack\Bootstrappers; -use Blast\Facades\FacadeFactory; use Rareloop\Lumberjack\Application; +use Rareloop\Lumberjack\FacadeFactory; class RegisterFacades { diff --git a/src/Config.php b/src/Config.php index 86b0ea3..9ea2e60 100644 --- a/src/Config.php +++ b/src/Config.php @@ -9,7 +9,7 @@ class Config { private $data = []; - public function __construct(string $path = null) + public function __construct(?string $path = null) { if ($path) { $this->load($path); diff --git a/src/Contracts/QueryBuilder.php b/src/Contracts/QueryBuilder.php index 848720d..8797b6f 100644 --- a/src/Contracts/QueryBuilder.php +++ b/src/Contracts/QueryBuilder.php @@ -16,7 +16,7 @@ public function offset($offset): QueryBuilder; public function orderBy($orderBy, string $order = QueryBuilder::ASC): QueryBuilder; - public function orderByMeta($metaKey, string $order = QueryBuilder::ASC, string $type = null): QueryBuilder; + public function orderByMeta($metaKey, string $order = QueryBuilder::ASC, ?string $type = null): QueryBuilder; public function whereIdIn(array $ids): QueryBuilder; diff --git a/src/Dcrypt/AesCbc.php b/src/Dcrypt/AesCbc.php new file mode 100644 index 0000000..bcf544c --- /dev/null +++ b/src/Dcrypt/AesCbc.php @@ -0,0 +1,42 @@ + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Rareloop\Lumberjack\Dcrypt; + +/** + * Symmetric AES-256-CBC encryption functions powered by OpenSSL. + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + * @link https://apigen.ci/github/mmeyer2k/dcrypt/namespace-Dcrypt.html + */ +class AesCbc extends OpensslBridge +{ + /** + * AES-256 cipher identifier that will be passed to openssl + * + * @var string + */ + const CIPHER = 'aes-256-cbc'; + + /** + * Specify sha256 for message authentication + * + * @var string + */ + const CHKSUM = 'sha256'; +} diff --git a/src/Dcrypt/AesCtr.php b/src/Dcrypt/AesCtr.php new file mode 100644 index 0000000..8ed8814 --- /dev/null +++ b/src/Dcrypt/AesCtr.php @@ -0,0 +1,35 @@ + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Rareloop\Lumberjack\Dcrypt; + +/** + * Symmetric AES-256-CTR encryption functions powered by OpenSSL. + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + * @link https://apigen.ci/github/mmeyer2k/dcrypt/namespace-Dcrypt.html + */ +class AesCtr extends AesCbc +{ + /** + * AES-256 cipher identifier that will be passed to openssl + * + * @var string + */ + const CIPHER = 'aes-256-ctr'; +} diff --git a/src/Dcrypt/Hash.php b/src/Dcrypt/Hash.php new file mode 100644 index 0000000..1177d46 --- /dev/null +++ b/src/Dcrypt/Hash.php @@ -0,0 +1,193 @@ + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Rareloop\Lumberjack\Dcrypt; + +/** + * An opaque 512 bit iterative hash function. + * + * 16 bytes => iv + * 12 bytes => cost checksum + * 4 bytes => cost + * 32 bytes => hmac + * + * ivivivivivivivivsssssssssssscosthmachmachmachmachmachmachmachmac + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + * @link https://apigen.ci/github/mmeyer2k/dcrypt/namespace-Dcrypt.html + */ +class Hash +{ + const ALGO = 'sha256'; + + /** + * Internal function used to build the actual hash. + * + * @param string $input Data to hash + * @param string $password Password to use in HMAC call + * @param int $cost Number of iterations to use + * @param string|null $salt Initialization vector to use in HMAC calls + * @return string + */ + private static function build(string $input, string $password, int $cost, ?string $salt = null): string + { + // Generate salt if needed + $salt = $salt ?? \random_bytes(16); + + // Verify and normalize cost value + $cost = self::cost($cost); + + // Create key to use for hmac operations + $key = self::hmac($salt, $password, self::ALGO); + + // Perform hash iterations. Get a 32 byte output value + $hash = self::ihmac($input, $key, $cost, self::ALGO); + + // Return the salt + cost blob + hmac + return $salt . self::costHash($cost, $salt, $password) . $hash; + } + + /** + * Return a normalized cost value. + * + * @param int $cost Number of iterations to use. + * @return int + */ + private static function cost(int $cost): int + { + return $cost % \pow(2, 32); + } + + /** + * Performs hashing functions + * + * @param int $cost + * @param string $salt + * @param string $password + * @return string + */ + private static function costHash(int $cost, string $salt, string $password): string + { + // Hash and return first 12 bytes + $hash = Str::substr(self::hmac($cost, $salt, self::ALGO), 0, 12); + + // Convert cost to base 256 then encrypt with OTP stream cipher + $cost = Otp::crypt(self::dec2bin($cost), $password); + + return $hash . $cost; + } + + /** + * Perform a raw iterative HMAC operation with a configurable algo. + * + * @param string $data Data to hash. + * @param string $key Key to use to authenticate the hash. + * @param int $iter Number of times to iteratate the hash + * @param string $algo Name of algo (sha256 or sha512 recommended) + * @return string + */ + public static function ihmac(string $data, string $key, int $iter, string $algo = 'sha256'): string + { + // Can't perform negative iterations + $iter = \abs($iter); + + // Perform iterative hmac calls + // Make sure $iter value of 0 is handled + for ($i = 0; $i <= $iter; $i++) { + $data = self::hmac($data . $i . $iter, $key, $algo); + } + + return $data; + } + + /** + * Perform a single hmac iteration. This adds an extra layer of safety because hash_hmac can return false if algo + * is not valid. Return type hint will throw an exception if this happens. + * + * @param string $data Data to hash. + * @param string $key Key to use to authenticate the hash. + * @param string $algo Name of algo + * @return string + */ + public static function hmac(string $data, string $key, string $algo): string + { + return \hash_hmac($algo, $data, $key, true); + } + + /** + * Hash an input string into a salted 512 byte hash. + * + * @param string $input Data to hash. + * @param string $password HMAC validation password. + * @param int $cost Cost value of the hash. + * @return string + */ + public static function make(string $input, string $password, int $cost = 250000): string + { + return self::build($input, $password, $cost, null); + } + + /** + * Check the validity of a hash. + * + * @param string $input Input to test. + * @param string $hash Known hash to validate against. + * @param string $password HMAC password to use during iterative hash. + * @return boolean + */ + public static function verify(string $input, string $hash, string $password): bool + { + // Get the salt value from the decrypted prefix + $salt = Str::substr($hash, 0, 16); + + // Get the encrypted cost bytes + $cost = self::bin2dec(Otp::crypt(Str::substr($hash, 28, 4), $password)); + + // Get the entire cost+hash blob for comparison + $blob = Str::substr($hash, 16, 16); + + if (!Str::equal(self::costHash($cost, $salt, $password), $blob)) { + return false; + } + + // Return the boolean equivalence + return Str::equal($hash, self::build($input, $password, $cost, $salt)); + } + + /** + * Turns an integer into a 4 byte binary representation + * + * @param int $dec Integer to convert to binary + * @return string + */ + private static function dec2bin(int $dec): string + { + return \hex2bin(\str_pad(\dechex($dec), 8, '0', STR_PAD_LEFT)); + } + + /** + * Reverses dec2bin + * + * @param string $bin Binary string to convert to decimal + * @return string + */ + private static function bin2dec(string $bin): string + { + return \hexdec(\bin2hex($bin)); + } +} diff --git a/src/Dcrypt/OpensslBridge.php b/src/Dcrypt/OpensslBridge.php new file mode 100644 index 0000000..ebe329b --- /dev/null +++ b/src/Dcrypt/OpensslBridge.php @@ -0,0 +1,191 @@ + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Rareloop\Lumberjack\Dcrypt; + +/** + * Provides functionality common to the dcrypt AES block ciphers. Extend this class to customize your cipher suite. + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + * @link https://apigen.ci/github/mmeyer2k/dcrypt/namespace-Dcrypt.html + */ +class OpensslBridge +{ + /** + * This string is used when hashing to ensure cross compatibility between + * dcrypt\mcrypt and dcrypt\aes. Since v7, this is only needed for backwards + * compatibility with older versions + */ + const RIJNDA = 'rijndael-128'; + + /** + * Decrypt cyphertext + * + * @param string $data Cyphertext to decrypt + * @param string $pass Password that should be used to decrypt input data + * @param int $cost Number of extra HMAC iterations to perform on key + * @return string + */ + public static function decrypt(string $data, string $pass, int $cost = 0): string + { + // Find the IV at the beginning of the cypher text + $ivr = Str::substr($data, 0, self::ivsize()); + + // Gather the checksum portion of the ciphertext + $sum = Str::substr($data, self::ivsize(), self::cksize()); + + // Gather message portion of ciphertext after iv and checksum + $msg = Str::substr($data, self::ivsize() + self::cksize()); + + // Derive key from password + $key = self::key($pass, $ivr, $cost); + + // Calculate verification checksum + $chk = self::checksum($msg, $ivr, $key); + + // Verify HMAC before decrypting + self::checksumVerify($chk, $sum); + + // Decrypt message and return + return OpensslWrapper::decrypt($msg, static::CIPHER, $key, $ivr); + } + + /** + * Encrypt plaintext + * + * @param string $data Plaintext string to encrypt. + * @param string $pass Password used to encrypt data. + * @param int $cost Number of extra HMAC iterations to perform on key + * @return string + */ + public static function encrypt(string $data, string $pass, int $cost = 0): string + { + // Generate IV of appropriate size. + $ivr = \random_bytes(self::ivsize()); + + // Derive key from password + $key = self::key($pass, $ivr, $cost); + + // Encrypt the plaintext + $msg = OpensslWrapper::encrypt($data, static::CIPHER, $key, $ivr); + + // Create the cypher text prefix (iv + checksum) + $pre = $ivr . self::checksum($msg, $ivr, $key); + + // Return prefix + cyphertext + return $pre . $msg; + } + + /** + * Create a message authentication checksum. + * + * @param string $data Ciphertext that needs a checksum. + * @param string $iv Initialization vector. + * @param string $key HMAC key + * @return string + */ + private static function checksum(string $data, string $iv, string $key): string + { + // Prevent multiple potentially large string concats by hmac-ing the input data + // by itself first... + $sum = Hash::hmac($data, $key, static::CHKSUM); + + // Then add the other input elements together before performing the final hash + $sum = $sum . $iv . self::mode() . self::RIJNDA; + + // ... then hash other elements with previous hmac and return + return Hash::hmac($sum, $key, static::CHKSUM); + } + + /** + * Transform password into key and perform iterative HMAC (if specified) + * + * @param string $pass Encryption key + * @param string $iv Initialization vector + * @param int $cost Number of HMAC iterations to perform on key + * @return string + */ + private static function key(string $pass, string $iv, int $cost): string + { + // Create the authentication string to be hashed + $data = $iv . self::RIJNDA . self::mode(); + + return Hash::ihmac($data, $pass, $cost, static::CHKSUM); + } + + /** + * Verify checksum during decryption step and throw error if mismatching. + * + * @param string $calculated + * @param string $supplied + * @throws \InvalidArgumentException + */ + private static function checksumVerify(string $calculated, string $supplied) + { + if (!Str::equal($calculated, $supplied)) { + $e = 'Decryption can not proceed due to invalid cyphertext checksum.'; + throw new \InvalidArgumentException($e); + } + } + + /** + * Return the encryption mode string. This function is really only needed for backwards + * compatibility. + * + * @return string + */ + private static function mode(): string + { + // To prevent legacy blobs from not decoding, these ciphers (which were implemented before 8.3) have hard coded + // return values. Luckily, this integrates gracefully with overloading. + $legacy = [ + 'bf-cbc' => 'cbc', + 'bf-ofb' => 'ofb', + 'aes-256-cbc' => 'cbc', + 'aes-256-ctr' => 'ctr', + ]; + + $cipher = \strtolower(static::CIPHER); + + if (isset($legacy[$cipher])) { + return $legacy[$cipher]; + } + + return $cipher; + } + + /** + * Calculate checksum size + * + * @return int + */ + private static function cksize(): int + { + return Str::hashSize(static::CHKSUM); + } + + /** + * Get IV size + * + * @return int + */ + private static function ivsize(): int + { + return \openssl_cipher_iv_length(static::CIPHER); + } +} diff --git a/src/Dcrypt/OpensslWrapper.php b/src/Dcrypt/OpensslWrapper.php new file mode 100644 index 0000000..bea6638 --- /dev/null +++ b/src/Dcrypt/OpensslWrapper.php @@ -0,0 +1,67 @@ + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Rareloop\Lumberjack\Dcrypt; + +class OpensslWrapper +{ + + /** + * OpenSSL encrypt wrapper function + * + * @param string $data Data to decrypt + * @param string $method Cipher method to use + * @param string $key Key string + * @param string $iv Initialization vector + * @return string + */ + public static function encrypt(string $data, string $method, string $key, string $iv): string + { + $ret = \openssl_encrypt($data, $method, $key, 1, $iv); + + return self::returnOrException($ret); + } + + /** + * OpenSSL decrypt wrapper function + * + * @param string $data Data to decrypt + * @param string $method Cipher method to use + * @param string $key Key string + * @param string $iv Initialization vector + * @return string + */ + public static function decrypt(string $data, string $method, string $key, string $iv): string + { + $ret = \openssl_decrypt($data, $method, $key, 1, $iv); + + return self::returnOrException($ret); + } + + /** + * Throw an exception if openssl function returns false + * + * @param string|bool $data + * @return string + * @throws \Exception + */ + private static function returnOrException($data): string + { + if ($data === false) { + throw new \Exception('OpenSSL failed to encrypt/decrypt message.'); + } + + return $data; + } +} diff --git a/src/Dcrypt/Otp.php b/src/Dcrypt/Otp.php new file mode 100644 index 0000000..2836e4f --- /dev/null +++ b/src/Dcrypt/Otp.php @@ -0,0 +1,50 @@ + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Rareloop\Lumberjack\Dcrypt; + +/** + * A one time pad stream encryption class. + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + * @link http://en.wikipedia.org/wiki/Stream_cipher + * @link https://apigen.ci/github/mmeyer2k/dcrypt/namespace-Dcrypt.html + */ +class Otp +{ + /** + * Encrypt or decrypt a binary input string. + * + * @param string $input Input data to encrypt + * @param string $password Encryption/decryption key to use on input + * @param string $algo Hashing algo to generate keystream + * @return string + */ + public static function crypt(string $input, string $password, string $algo = 'sha512'): string + { + $chunks = \str_split($input, Str::hashSize($algo)); + + $length = Str::strlen($input); + + foreach ($chunks as $i => &$chunk) { + $chunk = $chunk ^ \hash_hmac($algo, $password . $length, $i, true); + } + + return \implode($chunks); + } +} diff --git a/src/Dcrypt/Pkcs7.php b/src/Dcrypt/Pkcs7.php new file mode 100644 index 0000000..37fc9ca --- /dev/null +++ b/src/Dcrypt/Pkcs7.php @@ -0,0 +1,76 @@ + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Rareloop\Lumberjack\Dcrypt; + +/** + * Provides PKCS #7 padding functionality. + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + * @link https://apigen.ci/github/mmeyer2k/dcrypt/namespace-Dcrypt.html + */ +class Pkcs7 +{ + /** + * PKCS #7 padding function. + * + * @param string $input String to pad + * @param int $blocksize Block size in bytes + * @return string + */ + public static function pad(string $input, int $blocksize): string + { + // Determine the padding string that needs to be appended. + $pad = self::paddingString(Str::strlen($input), $blocksize); + + // Return input + padding + return $input . $pad; + } + + /** + * Create the padding string that will be appended to the input. + * + * @param int $inputsize Size of the input in bytes + * @param int $blocksize Blocksize in bytes + * @return string + */ + private static function paddingString(int $inputsize, int $blocksize): string + { + // Determine the amount of padding to use + $pad = $blocksize - ($inputsize % $blocksize); + + // Create and return the padding string + return \str_repeat(\chr($pad), $pad); + } + + /** + * PKCS #7 unpadding function. + * + * @param string $input Padded string to unpad + * @return string + */ + public static function unpad(string $input): string + { + // Determine the padding size by converting the final byte of the + // input to its decimal value + $padsize = \ord(Str::substr($input, -1)); + + // Return string minus the padding amount + return Str::substr($input, 0, Str::strlen($input) - $padsize); + } +} diff --git a/src/Dcrypt/Rc4.php b/src/Dcrypt/Rc4.php new file mode 100644 index 0000000..8587437 --- /dev/null +++ b/src/Dcrypt/Rc4.php @@ -0,0 +1,76 @@ + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Rareloop\Lumberjack\Dcrypt; + +/** + * An implementation of RC4 symmetric encryption. + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + * @link http://en.wikipedia.org/wiki/Stream_cipher + * @link https://en.wikipedia.org/wiki/RC4 + * @link https://apigen.ci/github/mmeyer2k/dcrypt/namespace-Dcrypt.html + */ +class Rc4 +{ + /** + * Perform (en/de)cryption + * + * @param string $str String to be encrypted + * @param string $key Key to use for encryption + * @return string + */ + public static function crypt(string $str, string $key): string + { + $s = self::initializeState($key); + $i = $j = 0; + $res = ''; + $size = Str::strlen($str); + for ($y = 0; $y < $size; $y++) { + $i = ($i + 1) % 256; + $j = ($j + $s[$i]) % 256; + $x = $s[$i]; + $s[$i] = $s[$j]; + $s[$j] = $x; + $res .= $str[$y] ^ \chr($s[($s[$i] + $s[$j]) % 256]); + } + + return $res; + } + + /** + * Create the initial byte matrix that will be used for swaps. This code + * is identical between RC4 and Spritz. + * + * @param string $key + * @return array + */ + protected static function initializeState(string $key): array + { + $s = \range(0, 255); + $j = 0; + foreach (\range(0, 255) as $i) { + $j = ($j + $s[$i] + \ord($key[$i % Str::strlen($key)])) % 256; + $x = $s[$i]; + $s[$i] = $s[$j]; + $s[$j] = $x; + } + + return $s; + } +} diff --git a/src/Dcrypt/Spritz.php b/src/Dcrypt/Spritz.php new file mode 100644 index 0000000..78d6726 --- /dev/null +++ b/src/Dcrypt/Spritz.php @@ -0,0 +1,59 @@ + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Rareloop\Lumberjack\Dcrypt; + +/** + * An implementation of Spritz symmetric encryption. + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + * @link http://en.wikipedia.org/wiki/Stream_cipher + * @link https://en.wikipedia.org/wiki/RC4 + * @link http://people.csail.mit.edu/rivest/pubs/RS14.pdf + * @link https://apigen.ci/github/mmeyer2k/dcrypt/namespace-Dcrypt.html + */ +class Spritz extends Rc4 +{ + /** + * Perform (en/de)cryption + * + * @param string $str String to be encrypted + * @param string $key Key to use for encryption + * @return string + */ + public static function crypt(string $str, string $key): string + { + $s = self::initializeState($key); + $i = $j = $k = $z = 0; + $w = 1; + $res = ''; + $size = Str::strlen($str); + for ($y = 0; $y < $size; $y++) { + $i = ($i + $w) % 256; + $j = ($k + $s[($j + $s[$i]) % 256]) % 256; + $k = ($i + $k + $s[$j]) % 256; + $x = $s[$i]; + $s[$i] = $s[$j]; + $s[$j] = $x; + $z = $s[($j + $s[($i + $s[($z + $k) % 256]) % 256]) % 256]; + $res .= $str[$y] ^ \chr($z); + } + + return $res; + } +} diff --git a/src/Dcrypt/Str.php b/src/Dcrypt/Str.php new file mode 100644 index 0000000..b9f1b89 --- /dev/null +++ b/src/Dcrypt/Str.php @@ -0,0 +1,92 @@ + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Rareloop\Lumberjack\Dcrypt; + +/** + * Provides time-safe string comparison facilities, and safe string operations + * on systems that have mb_* function overloading enabled. + * + * The functions in this class were inspired by the symfony's StringUtils class. + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + * @link https://github.com/symfony/Security/blob/master/Core/Util/StringUtils.php + * @link https://php.net/manual/en/mbstring.overload.php + * @link https://apigen.ci/github/mmeyer2k/dcrypt/namespace-Dcrypt.html + */ +class Str +{ + /** + * Compares two strings in constant time. Strings are hashed before + * comparison so information is not leaked when strings are not of + * equal length. + * + * @param string $known The string of known length to compare against + * @param string $given The string that the user can control + * @return bool + */ + public static function equal(string $known, string $given): bool + { + // Create some entropy + $nonce = \random_bytes(32); + + // We hash the 2 inputs at this point because hash_equals is still + // vulnerable to timing attacks when the inputs have different sizes. + // Inputs are also cast to string like in symfony stringutils. + $known = Hash::hmac($known, $nonce, 'sha256'); + $given = Hash::hmac($given, $nonce, 'sha256'); + + return \hash_equals($known, $given); + } + + /** + * Determine the length of the output of a given hash algorithm in bytes. + * + * @param string $algo Name of algorithm to look up + * @return int + */ + public static function hashSize(string $algo): int + { + return self::strlen(\hash($algo, 'hash me', true)); + } + + /** + * Returns the number of bytes in a string. + * + * @param string $string The string whose length we wish to obtain + * @return int + */ + public static function strlen(string $string): int + { + return \mb_strlen($string, '8bit'); + } + + /** + * Returns part of a string. + * + * @param string $string The string whose length we wish to obtain + * @param int $start + * @param int $length + * + * @return string the extracted part of string; or FALSE on failure, or an empty string. + */ + public static function substr(string $string, int $start, ?int $length = null): string + { + return \mb_substr($string, $start, $length, '8bit'); + } +} diff --git a/src/Encrypter.php b/src/Encrypter.php index 12e3049..034da2a 100644 --- a/src/Encrypter.php +++ b/src/Encrypter.php @@ -2,7 +2,7 @@ namespace Rareloop\Lumberjack; -use Dcrypt\AesCbc; +use Rareloop\Lumberjack\Dcrypt\AesCbc; use Rareloop\Lumberjack\Contracts\Encrypter as EncrypterContract; class Encrypter implements EncrypterContract diff --git a/src/Exceptions/Handler.php b/src/Exceptions/Handler.php index 045b6f1..cc5409b 100644 --- a/src/Exceptions/Handler.php +++ b/src/Exceptions/Handler.php @@ -3,13 +3,12 @@ namespace Rareloop\Lumberjack\Exceptions; use Exception; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; +use Spatie\Ignition\Ignition; use Rareloop\Lumberjack\Application; +use Psr\Http\Message\ResponseInterface; use Rareloop\Lumberjack\Facades\Config; -use Symfony\Component\Debug\ExceptionHandler as SymfonyExceptionHandler; -use Symfony\Component\Debug\Exception\FlattenException; -use Zend\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\HtmlResponse; +use Psr\Http\Message\ServerRequestInterface; class Handler implements HandlerInterface { @@ -34,13 +33,22 @@ public function report(Exception $e) } } - public function render(ServerRequestInterface $request, Exception $e) : ResponseInterface + public function render(ServerRequestInterface $request, Exception $e): ResponseInterface { - $e = FlattenException::create($e); + $isDebug = Config::get('app.debug', false) === true; + + $ignition = Ignition::make() + ->shouldDisplayException($isDebug) + ->runningInProductionEnvironment(!$isDebug) + ->register(); + + ob_start(); + + $ignition->handleException($e); - $handler = new SymfonyExceptionHandler(Config::get('app.debug', false)); + $html = ob_get_clean(); - return new HtmlResponse($handler->getHtml($e), $e->getStatusCode(), $e->getHeaders()); + return new HtmlResponse($html); } protected function shouldNotReport(Exception $e) diff --git a/src/FacadeFactory.php b/src/FacadeFactory.php new file mode 100644 index 0000000..eb09caf --- /dev/null +++ b/src/FacadeFactory.php @@ -0,0 +1,25 @@ +get($accessor), $name], ...$arguments); + } +} diff --git a/src/Facades/AbstractFacade.php b/src/Facades/AbstractFacade.php new file mode 100644 index 0000000..72b81c4 --- /dev/null +++ b/src/Facades/AbstractFacade.php @@ -0,0 +1,20 @@ +get(static::accessor()); + } + + abstract protected static function accessor(); +} diff --git a/src/Facades/Config.php b/src/Facades/Config.php index 174a692..bfd4a26 100644 --- a/src/Facades/Config.php +++ b/src/Facades/Config.php @@ -2,8 +2,6 @@ namespace Rareloop\Lumberjack\Facades; -use Blast\Facades\AbstractFacade; - class Config extends AbstractFacade { protected static function accessor() diff --git a/src/Facades/Log.php b/src/Facades/Log.php index 324c66b..d7bbf8b 100644 --- a/src/Facades/Log.php +++ b/src/Facades/Log.php @@ -2,8 +2,6 @@ namespace Rareloop\Lumberjack\Facades; -use Blast\Facades\AbstractFacade; - class Log extends AbstractFacade { protected static function accessor() diff --git a/src/Facades/MiddlewareAliases.php b/src/Facades/MiddlewareAliases.php index cf8bf7d..72d1080 100644 --- a/src/Facades/MiddlewareAliases.php +++ b/src/Facades/MiddlewareAliases.php @@ -2,8 +2,6 @@ namespace Rareloop\Lumberjack\Facades; -use Blast\Facades\AbstractFacade; - class MiddlewareAliases extends AbstractFacade { protected static function accessor() diff --git a/src/Facades/Router.php b/src/Facades/Router.php index 6859dec..85fa666 100644 --- a/src/Facades/Router.php +++ b/src/Facades/Router.php @@ -2,8 +2,6 @@ namespace Rareloop\Lumberjack\Facades; -use Blast\Facades\AbstractFacade; - class Router extends AbstractFacade { protected static function accessor() diff --git a/src/Facades/Session.php b/src/Facades/Session.php index cf9b053..2b28d9f 100644 --- a/src/Facades/Session.php +++ b/src/Facades/Session.php @@ -2,8 +2,6 @@ namespace Rareloop\Lumberjack\Facades; -use Blast\Facades\AbstractFacade; - class Session extends AbstractFacade { protected static function accessor() diff --git a/src/Http/MiddlewareAliasStore.php b/src/Http/MiddlewareAliasStore.php index c9c2682..80fbc9d 100644 --- a/src/Http/MiddlewareAliasStore.php +++ b/src/Http/MiddlewareAliasStore.php @@ -32,7 +32,7 @@ public function get(string $name) return $middleware; } - protected function parseName($name) : array + protected function parseName($name): array { list($name, $params) = array_pad(explode(':', $name), 2, ''); @@ -41,7 +41,7 @@ protected function parseName($name) : array return [$name, $params]; } - public function has(string $name) : bool + public function has(string $name): bool { list($name, $params) = $this->parseName($name); diff --git a/src/Http/Responses/RedirectResponse.php b/src/Http/Responses/RedirectResponse.php index d51db77..a6d992d 100644 --- a/src/Http/Responses/RedirectResponse.php +++ b/src/Http/Responses/RedirectResponse.php @@ -3,7 +3,7 @@ namespace Rareloop\Lumberjack\Http\Responses; use Rareloop\Lumberjack\Helpers; -use Zend\Diactoros\Response\RedirectResponse as DiactorosRedirectResponse; +use Laminas\Diactoros\Response\RedirectResponse as DiactorosRedirectResponse; class RedirectResponse extends DiactorosRedirectResponse { diff --git a/src/Http/Responses/TimberResponse.php b/src/Http/Responses/TimberResponse.php index 06a31cb..8ac161e 100644 --- a/src/Http/Responses/TimberResponse.php +++ b/src/Http/Responses/TimberResponse.php @@ -6,7 +6,7 @@ use Rareloop\Lumberjack\Contracts\Arrayable; use Rareloop\Lumberjack\Exceptions\TwigTemplateNotFoundException; use Timber\Timber; -use Zend\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\HtmlResponse; class TimberResponse extends HtmlResponse { @@ -21,7 +21,7 @@ public function __construct($twigTemplate, $context, $status = 200, array $heade parent::__construct($template, $status, $headers); } - private function flattenContextToArrays(array $context) : array + private function flattenContextToArrays(array $context): array { // Recursively walk the array, when we find something that implements the Arrayable interface // flatten it to an array. Because we're passing by reference by updating what the value of diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index 5135e39..3274e66 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -5,7 +5,7 @@ use Psr\Http\Message\ServerRequestInterface; use Rareloop\Psr7ServerRequestExtension\InteractsWithInput; use Rareloop\Psr7ServerRequestExtension\InteractsWithUri; -use Zend\Diactoros\ServerRequest as DiactorosServerRequest; +use Laminas\Diactoros\ServerRequest as DiactorosServerRequest; class ServerRequest extends DiactorosServerRequest { @@ -34,7 +34,7 @@ public static function fromRequest(ServerRequestInterface $request) ); } - public function ajax() : bool + public function ajax(): bool { if (!$this->hasHeader('X-Requested-With')) { return false; @@ -43,7 +43,7 @@ public function ajax() : bool return 'XMLHttpRequest' === $this->getHeader('X-Requested-With')[0]; } - public function getMethod() : string + public function getMethod(): string { return strtoupper(parent::getMethod()); } diff --git a/src/Providers/EncryptionServiceProvider.php b/src/Providers/EncryptionServiceProvider.php index ec762cc..87b7c3f 100644 --- a/src/Providers/EncryptionServiceProvider.php +++ b/src/Providers/EncryptionServiceProvider.php @@ -2,11 +2,8 @@ namespace Rareloop\Lumberjack\Providers; -use Rareloop\Lumberjack\Application; use Rareloop\Lumberjack\Contracts\Encrypter as EncrypterContract; use Rareloop\Lumberjack\Encrypter; -use Rareloop\Lumberjack\Facades\Config; -use Rareloop\Lumberjack\Session\SessionManager; class EncryptionServiceProvider extends ServiceProvider { @@ -15,9 +12,11 @@ class EncryptionServiceProvider extends ServiceProvider public function register() { if ($this->app->has('config')) { - $encryptionKey = $this->app->get('config')->get('app.key'); + $encrypter = function () { + $encryptionKey = $this->app->get('config')->get('app.key'); - $encrypter = new Encrypter($encryptionKey); + return new Encrypter($encryptionKey); + }; $this->app->bind(EncrypterContract::class, $encrypter); $this->app->bind('encrypter', $encrypter); diff --git a/src/Providers/LogServiceProvider.php b/src/Providers/LogServiceProvider.php index b04a0b3..a6cf812 100644 --- a/src/Providers/LogServiceProvider.php +++ b/src/Providers/LogServiceProvider.php @@ -7,6 +7,7 @@ use Monolog\Handler\StreamHandler; use Monolog\Formatter\LineFormatter; use Monolog\Handler\ErrorLogHandler; +use Monolog\Level; class LogServiceProvider extends ServiceProvider { @@ -44,9 +45,9 @@ private function shouldUseErrorLogHandler() return $config && $config->get('app.logs.path') === false && $config->get('app.logs.enabled') === true; } - private function getLogLevel() + private function getLogLevel(): Level { - $logLevel = Logger::DEBUG; + $logLevel = Level::Debug; if ($this->app->has('config')) { $logLevel = $this->app->get('config')->get('app.logs.level', $logLevel); diff --git a/src/Providers/RouterServiceProvider.php b/src/Providers/RouterServiceProvider.php index 5a61ee5..81f82e1 100644 --- a/src/Providers/RouterServiceProvider.php +++ b/src/Providers/RouterServiceProvider.php @@ -9,7 +9,7 @@ use Rareloop\Lumberjack\Http\Router; use Rareloop\Lumberjack\Http\ServerRequest; use Rareloop\Router\MiddlewareResolver as MiddlewareResolverInterface; -use Zend\Diactoros\ServerRequestFactory; +use Laminas\Diactoros\ServerRequestFactory; class RouterServiceProvider extends ServiceProvider { diff --git a/src/Providers/WordPressControllersServiceProvider.php b/src/Providers/WordPressControllersServiceProvider.php index 29170f5..49a206f 100644 --- a/src/Providers/WordPressControllersServiceProvider.php +++ b/src/Providers/WordPressControllersServiceProvider.php @@ -9,7 +9,7 @@ use Rareloop\Router\ResponseFactory; use Psr\Http\Message\RequestInterface; use Rareloop\Lumberjack\Http\Middleware\PasswordProtected; -use Zend\Diactoros\ServerRequestFactory; +use Laminas\Diactoros\ServerRequestFactory; use Rareloop\Router\ProvidesControllerMiddleware; class WordPressControllersServiceProvider extends ServiceProvider @@ -94,7 +94,7 @@ function ($request) use ($controller, $methodName) { ]; $dispatcher = $this->createDispatcher($middlewares); - return $dispatcher->dispatch($request); + return $dispatcher->handle($request); } private function createDispatcher(array $middlewares): Dispatcher diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 384fee6..f51ad99 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -82,7 +82,7 @@ public function orderBy($orderBy, string $order = QueryBuilder::ASC): QueryBuild return $this; } - public function orderByMeta($metaKey, string $order = QueryBuilder::ASC, string $type = null): QueryBuilderContract + public function orderByMeta($metaKey, string $order = QueryBuilder::ASC, ?string $type = null): QueryBuilderContract { $order = strtoupper($order); diff --git a/src/Session/EncryptedStore.php b/src/Session/EncryptedStore.php index 856b7c1..d6388f0 100644 --- a/src/Session/EncryptedStore.php +++ b/src/Session/EncryptedStore.php @@ -17,7 +17,7 @@ public function __construct( SessionHandlerInterface $handler, Encrypter $encrypter, $id = null, - HandlerInterface $exceptionHandler = null + ?HandlerInterface $exceptionHandler = null ) { $this->encrypter = $encrypter; $this->exceptionHandler = $exceptionHandler; @@ -32,10 +32,14 @@ protected function prepareForStorage($data) protected function prepareForUnserialize($data) { + if ($data === '') { + return ''; + } + try { return $this->encrypter->decrypt($data); } catch (Exception $e) { - $this->exceptionHandler->report($e); + $this->exceptionHandler?->report($e); return ''; } } diff --git a/src/Session/FileSessionHandler.php b/src/Session/FileSessionHandler.php index 80d5a48..56f3b5e 100644 --- a/src/Session/FileSessionHandler.php +++ b/src/Session/FileSessionHandler.php @@ -46,7 +46,7 @@ public function write($sessionId, $data) { try { file_put_contents($this->getFilepath($sessionId), $data); - } catch (Exception $e) { + } catch (Exception) { Log::error('Failed to create session on disk'); } diff --git a/src/Session/SessionManager.php b/src/Session/SessionManager.php index 0926a03..6359bae 100644 --- a/src/Session/SessionManager.php +++ b/src/Session/SessionManager.php @@ -5,6 +5,7 @@ use Rareloop\Lumberjack\Application; use Rareloop\Lumberjack\Config; use Rareloop\Lumberjack\Contracts\Encrypter as EncrypterContract; +use Rareloop\Lumberjack\Exceptions\HandlerInterface; use Rareloop\Lumberjack\Manager; class SessionManager extends Manager @@ -54,8 +55,9 @@ protected function buildSession($handler) if ($this->config->get('session.encrypt')) { $encrypter = $this->app->get(EncrypterContract::class); + $exceptionHandler = $this->app->get(HandlerInterface::class); - return new EncryptedStore($this->name, $handler, $encrypter, $sessionId); + return new EncryptedStore($this->name, $handler, $encrypter, $sessionId, $exceptionHandler); } return new Store($this->name, $handler, $sessionId); diff --git a/src/ViewModel.php b/src/ViewModel.php index a7a9a31..9ae3fa2 100644 --- a/src/ViewModel.php +++ b/src/ViewModel.php @@ -9,7 +9,7 @@ abstract class ViewModel implements Arrayable { - public function toArray() : array + public function toArray(): array { $propertyKeyValues = collect($this->validPropertyNames()) ->mapWithKeys(function ($method) { @@ -31,7 +31,7 @@ public function toArray() : array return array_merge($propertyKeyValues, $methodKeyValues); } - protected function validMethodNames() : array + protected function validMethodNames(): array { $class = new ReflectionClass(static::class); return collect($class->getMethods(ReflectionMethod::IS_PUBLIC)) @@ -44,7 +44,7 @@ protected function validMethodNames() : array ->all(); } - protected function validPropertyNames() : array + protected function validPropertyNames(): array { $class = new ReflectionClass(static::class); @@ -58,7 +58,7 @@ protected function validPropertyNames() : array ->all(); } - protected function ignoredMethods() : array + protected function ignoredMethods(): array { return [ 'toArray', diff --git a/tests/Unit/Bootstrappers/RegisterExceptionHandlerTest.php b/tests/Unit/Bootstrappers/RegisterExceptionHandlerTest.php index c19f006..ff38c42 100644 --- a/tests/Unit/Bootstrappers/RegisterExceptionHandlerTest.php +++ b/tests/Unit/Bootstrappers/RegisterExceptionHandlerTest.php @@ -14,10 +14,9 @@ use Rareloop\Lumberjack\Exceptions\HandlerInterface; use Rareloop\Lumberjack\Test\Unit\BrainMonkeyPHPUnitIntegration; use Rareloop\Router\Responsable; -use Symfony\Component\Debug\Exception\FatalErrorException; -use Zend\Diactoros\Response; -use Zend\Diactoros\Response\TextResponse; -use Zend\Diactoros\ServerRequest; +use Laminas\Diactoros\Response; +use Laminas\Diactoros\Response\TextResponse; +use Laminas\Diactoros\ServerRequest; /** * @runTestsInSeparateProcesses diff --git a/tests/Unit/Bootstrappers/RegisterFacadesTest.php b/tests/Unit/Bootstrappers/RegisterFacadesTest.php index 71653c9..5a2f234 100644 --- a/tests/Unit/Bootstrappers/RegisterFacadesTest.php +++ b/tests/Unit/Bootstrappers/RegisterFacadesTest.php @@ -2,11 +2,10 @@ namespace Rareloop\Lumberjack\Test\Bootstrappers; -use Blast\Facades\FacadeFactory; -use Mockery; use PHPUnit\Framework\TestCase; use Rareloop\Lumberjack\Application; use Rareloop\Lumberjack\Bootstrappers\RegisterFacades; +use Rareloop\Lumberjack\FacadeFactory; class RegisterFacadesTest extends TestCase { diff --git a/tests/Unit/Dcrypt/AesCbcTest.php b/tests/Unit/Dcrypt/AesCbcTest.php new file mode 100644 index 0000000..e7a4cb7 --- /dev/null +++ b/tests/Unit/Dcrypt/AesCbcTest.php @@ -0,0 +1,49 @@ +expectException(\InvalidArgumentException::class); + + $input = 'AAAAAAAA'; + $key = 'AAAAAAAA'; + $encrypted = AesCbc::encrypt($input, $key, 10); + $this->assertEquals($input, AesCbc::decrypt($encrypted, $key, 10)); + + $corrupt = self::swaprandbyte($encrypted); + AesCbc::decrypt($corrupt, $key, 10); + } + + public function testEngine() + { + $this->expectException(\InvalidArgumentException::class); + + $input = 'AAAAAAAA'; + $key = 'AAAAAAAA'; + + $encrypted = AesCbc::encrypt($input, $key); + $this->assertEquals($input, AesCbc::decrypt($encrypted, $key)); + + // Perform a validation by replacing a random byte to make sure + // the decryption fails. After enough successful runs, + // all areas of the cypher text will have been tested + // for integrity + $corrupt = self::swaprandbyte($encrypted); + AesCbc::decrypt($corrupt, $key); + } + + public function testVector() + { + $input = 'hello world'; + $pass = 'password'; + $vector = \base64_decode('eZu2DqB2gYhdA2YkjagLNJJVMVo1BbpJ75tW/PO2bGIY98XHD+Gp+YlO5cv/rHzo45LHMCxL2qOircdST1w5hg=='); + + $this->assertEquals($input, AesCbc::decrypt($vector, $pass)); + } +} diff --git a/tests/Unit/Dcrypt/AesCtrTest.php b/tests/Unit/Dcrypt/AesCtrTest.php new file mode 100644 index 0000000..e640a2f --- /dev/null +++ b/tests/Unit/Dcrypt/AesCtrTest.php @@ -0,0 +1,44 @@ +input, $this->key, 10); + $this->assertEquals($this->input, AesCtr::decrypt($encrypted, $this->key, 10)); + } + + public function testEngine() + { + $encrypted = AesCtr::encrypt($this->input, $this->key); + $this->assertEquals($this->input, AesCtr::decrypt($encrypted, $this->key)); + } + + public function testCorrupt() + { + $this->expectException(\InvalidArgumentException::class); + + $encrypted = AesCtr::encrypt($this->input, $this->key); + + // Perform a validation by replacing a random byte to make sure + // the decryption fails. After enough successful runs, + // all areas of the cypher text will have been tested + // for integrity + $corrupt = self::swaprandbyte($encrypted); + AesCtr::decrypt($corrupt, $this->key); + } + + public function testVector() + { + $input = 'hello world'; + $pass = 'password'; + $vector = \base64_decode('Vpbd71CIVcRPALeSg126DhRKYozXlbusn/eSSxrQPtzj/U7hOhlN8D21Y0gmlmUKorpoXuDS6bklvD8='); + $this->assertEquals($input, AesCtr::decrypt($vector, $pass)); + } +} diff --git a/tests/Unit/Dcrypt/HashTest.php b/tests/Unit/Dcrypt/HashTest.php new file mode 100644 index 0000000..3e1ba86 --- /dev/null +++ b/tests/Unit/Dcrypt/HashTest.php @@ -0,0 +1,64 @@ +assertNotEquals('aaaa', Hash::ihmac('aaaa', 'bbbb', 0)); + $this->assertNotEquals('aaaa', Hash::ihmac('aaaa', 'bbbb', -1)); + } + + public function testBadCost() + { + $this->assertEquals(64, strlen(Hash::make('test', '1234', 0))); + } + + public function testLength() + { + $this->assertEquals(64, strlen(Hash::make('test', '1234'))); + } + + public function testCycle() + { + $input = 'input test'; + $key = 'key123'; + $hash = Hash::make($input, $key, 1); + $this->assertTrue(Hash::verify($input, $hash, $key)); + } + + public function testHmacAlgoFailure() + { + $this->expectException(\ValueError::class); + + Hash::hmac('test', '1234', 'an algo that does not exist'); + } + + public function testFail() + { + $input = str_repeat('A', rand(0, 10000)); + $key = str_repeat('A', rand(10, 100)); + $cost = 1; + + $output = Hash::make($input, $key, $cost); + $this->assertTrue(Hash::verify($input, $output, $key)); + + for ($i = 0; $i < 10; $i++) { + $corrupt = self::swaprandbyte($output); + $this->assertFalse(Hash::verify($input, $corrupt, $key)); + } + } + + public function testVector() + { + $input = 'hello world'; + $key = 'password'; + $vector = base64_decode('dvvWMEFPCo9EV+l+htGGcoK5Uj8zrh6bfxCh16NOjJxuugObuidTQ3+R3qiyZLnHl7zRxSmfHRasEJQpTymZDw=='); + $this->assertTrue(Hash::verify($input, $vector, $key)); + } +} diff --git a/tests/Unit/Dcrypt/OtpTest.php b/tests/Unit/Dcrypt/OtpTest.php new file mode 100644 index 0000000..38b910a --- /dev/null +++ b/tests/Unit/Dcrypt/OtpTest.php @@ -0,0 +1,38 @@ +assertEquals(strlen($input), strlen($encrypted)); + $this->assertNotEquals($input, $encrypted); + + /* + * Test decryption + */ + $decrypted = Otp::crypt($encrypted, $key); + $this->assertEquals($input, $decrypted); + } + } + + public function testVector() + { + $input = 'hello world'; + $pass = 'password'; + $vector = base64_decode('Cf6ULwbiZEbJr1w='); + + $this->assertEquals($input, Otp::crypt($vector, $pass)); + } +} diff --git a/tests/Unit/Dcrypt/Pkcs7Test.php b/tests/Unit/Dcrypt/Pkcs7Test.php new file mode 100644 index 0000000..6f8ed2e --- /dev/null +++ b/tests/Unit/Dcrypt/Pkcs7Test.php @@ -0,0 +1,19 @@ +assertEquals(Pkcs7::pad('aaaabbbb', 3), "aaaabbbb\x01"); + + $this->assertEquals(Pkcs7::pad('aaaabbbb', 4), "aaaabbbb\x04\x04\x04\x04"); + + $this->assertEquals(Pkcs7::unpad("aaaabbbb\x01"), "aaaabbbb"); + + $this->assertEquals(Pkcs7::unpad("aaaabbbb\x04\x04\x04\x04"), "aaaabbbb"); + } +} diff --git a/tests/Unit/Dcrypt/Rc4Test.php b/tests/Unit/Dcrypt/Rc4Test.php new file mode 100644 index 0000000..eb1dd49 --- /dev/null +++ b/tests/Unit/Dcrypt/Rc4Test.php @@ -0,0 +1,39 @@ +assertEquals(strlen($input), strlen($encrypted)); + $this->assertNotEquals($input, $encrypted); + + /* + * Test decryption + */ + $decrypted = Rc4::crypt($encrypted, $key); + $this->assertEquals($input, $decrypted); + } + + public function testVector() + { + /* + * Test that known cypher text decrypts properly + */ + $cyphertext = hex2bin('140ad3d278a229ff3c487d'); + $plain = 'Hello World'; + $key = 'asdf'; + + $this->assertEquals($plain, Rc4::crypt($cyphertext, $key)); + } +} diff --git a/tests/Unit/Dcrypt/SpritzTest.php b/tests/Unit/Dcrypt/SpritzTest.php new file mode 100644 index 0000000..d2bc421 --- /dev/null +++ b/tests/Unit/Dcrypt/SpritzTest.php @@ -0,0 +1,27 @@ +assertEquals(strlen($input), strlen($encrypted)); + $this->assertNotEquals($input, $encrypted); + + /* + * Test decryption + */ + $decrypted = Spritz::crypt($encrypted, $key); + $this->assertEquals($input, $decrypted); + } +} diff --git a/tests/Unit/Dcrypt/StrTest.php b/tests/Unit/Dcrypt/StrTest.php new file mode 100644 index 0000000..19022e0 --- /dev/null +++ b/tests/Unit/Dcrypt/StrTest.php @@ -0,0 +1,19 @@ +assertTrue(Str::equal('2222', '2222', true)); + $this->assertFalse(Str::equal('2222', '3333', true)); + + // Test without hash_equals + $this->assertTrue(Str::equal('2222', '2222', false)); + $this->assertFalse(Str::equal('2222', '3333', false)); + } +} diff --git a/tests/Unit/Dcrypt/SwapTest.php b/tests/Unit/Dcrypt/SwapTest.php new file mode 100644 index 0000000..8267ea3 --- /dev/null +++ b/tests/Unit/Dcrypt/SwapTest.php @@ -0,0 +1,15 @@ +assertFalse($orig === self::swaprandbyte($orig)); + $this->assertEquals(levenshtein($orig, self::swaprandbyte($orig)), 1); + } + } +} diff --git a/tests/Unit/Dcrypt/TestSupport.php b/tests/Unit/Dcrypt/TestSupport.php new file mode 100644 index 0000000..dcf1c8f --- /dev/null +++ b/tests/Unit/Dcrypt/TestSupport.php @@ -0,0 +1,30 @@ +container = new Container(); + $this->container->set(FooFacade::accessor(), new Foo()); + } + + /** @test */ + public function can_initiate_facades() + { + FacadeFactory::setContainer($this->container); + $this->assertInstanceOf(ContainerInterface::class, FacadeFactory::getContainer()); + } + + /** @test */ + public function can_get_facade_instance() + { + FacadeFactory::setContainer($this->container); + + $instance = FooFacade::__instance(); + + $this->assertInstanceOf(FooInterface::class, $instance); + $this->assertInstanceOf(Foo::class, $instance); + } + + /** @test */ + public function can_swap_instances() + { + FacadeFactory::setContainer($this->container); + + $instance = FooFacade::__instance(); + + $this->assertInstanceOf(FooInterface::class, $instance); + $this->assertInstanceOf(Foo::class, $instance); + + $this->container->set(FooFacade::accessor(), 'bar'); + + $instance = FooFacade::__instance(); + + $this->assertEquals('bar', $instance); + } + + public function can_call_functions() + { + FacadeFactory::setContainer($this->container); + + $this->assertEquals('bar', forward_static_call([FooFacade::class, 'foo'])); + $this->assertEquals('bar', call_user_func('FooFacade::class::foo')); + $this->assertEquals('bar', FooFacade::foo()); + } +} diff --git a/tests/Unit/Facades/LogTest.php b/tests/Unit/Facades/LogTest.php index 4ae6019..9105726 100644 --- a/tests/Unit/Facades/LogTest.php +++ b/tests/Unit/Facades/LogTest.php @@ -2,10 +2,10 @@ namespace Rareloop\Lumberjack\Test\Facades; -use Blast\Facades\FacadeFactory; use Monolog\Logger; use PHPUnit\Framework\TestCase; use Rareloop\Lumberjack\Application; +use Rareloop\Lumberjack\FacadeFactory; use Rareloop\Lumberjack\Facades\Log as LogFacade; class LogTest extends TestCase diff --git a/tests/Unit/Facades/MiddlewareAliasesTest.php b/tests/Unit/Facades/MiddlewareAliasesTest.php index 5cf4da6..cd847a0 100644 --- a/tests/Unit/Facades/MiddlewareAliasesTest.php +++ b/tests/Unit/Facades/MiddlewareAliasesTest.php @@ -2,9 +2,9 @@ namespace Rareloop\Lumberjack\Test\Facades; -use Blast\Facades\FacadeFactory; use PHPUnit\Framework\TestCase; use Rareloop\Lumberjack\Application; +use Rareloop\Lumberjack\FacadeFactory; use Rareloop\Lumberjack\Facades\MiddlewareAliases; use Rareloop\Lumberjack\Http\MiddlewareAliasStore; diff --git a/tests/Unit/Facades/RouterTest.php b/tests/Unit/Facades/RouterTest.php index 71ee82a..ebc39f6 100644 --- a/tests/Unit/Facades/RouterTest.php +++ b/tests/Unit/Facades/RouterTest.php @@ -2,9 +2,9 @@ namespace Rareloop\Lumberjack\Test\Facades; -use Blast\Facades\FacadeFactory; use PHPUnit\Framework\TestCase; use Rareloop\Lumberjack\Application; +use Rareloop\Lumberjack\FacadeFactory; use Rareloop\Lumberjack\Facades\Router as RouterFacade; use Rareloop\Lumberjack\Http\Router; diff --git a/tests/Unit/Facades/SessionTest.php b/tests/Unit/Facades/SessionTest.php index af7103e..95c7c3f 100644 --- a/tests/Unit/Facades/SessionTest.php +++ b/tests/Unit/Facades/SessionTest.php @@ -2,9 +2,9 @@ namespace Rareloop\Lumberjack\Test\Facades; -use Blast\Facades\FacadeFactory; use PHPUnit\Framework\TestCase; use Rareloop\Lumberjack\Application; +use Rareloop\Lumberjack\FacadeFactory; use Rareloop\Lumberjack\Facades\Session; use Rareloop\Lumberjack\Session\SessionManager; use Rareloop\Lumberjack\Test\Unit\Session\NullSessionHandler; diff --git a/tests/Unit/Facades/Stubs/Foo.php b/tests/Unit/Facades/Stubs/Foo.php new file mode 100644 index 0000000..d5ba726 --- /dev/null +++ b/tests/Unit/Facades/Stubs/Foo.php @@ -0,0 +1,11 @@ +once()->andReturn(false); - $app = new Application(__DIR__.'/../'); + $app = new Application(__DIR__ . '/../'); $lumberjack = new Lumberjack($app); $lumberjack->bootstrap(); @@ -37,7 +38,7 @@ public function log_object_is_always_registered() * @codingStandardsIgnoreLine */ function default_handler_is_in_memory_stream() { - $app = new Application(__DIR__.'/../'); + $app = new Application(__DIR__ . '/../'); $config = new Config; $app->bind('config', $config); @@ -52,7 +53,7 @@ function default_handler_is_in_memory_stream() /** @test */ public function default_log_warning_level_is_debug() { - $app = new Application(__DIR__.'/../'); + $app = new Application(__DIR__ . '/../'); $config = new Config; $app->bind('config', $config); @@ -61,13 +62,13 @@ public function default_log_warning_level_is_debug() RegisterProviders::class, ]); - $this->assertSame(Logger::DEBUG, $app->get('logger')->getHandlers()[0]->getLevel()); + $this->assertSame(Level::Debug, $app->get('logger')->getHandlers()[0]->getLevel()); } /** @test */ public function stream_is_used_when_path_is_set_but_logging_is_disabled() { - $app = new Application(__DIR__.'/../'); + $app = new Application(__DIR__ . '/../'); $config = new Config; $config->set('app.logs.enabled', false); @@ -84,17 +85,17 @@ public function stream_is_used_when_path_is_set_but_logging_is_disabled() /** @test */ public function log_warning_level_can_be_set_in_config() { - $app = new Application(__DIR__.'/../'); + $app = new Application(__DIR__ . '/../'); $config = new Config; - $config->set('app.logs.level', Logger::ERROR); + $config->set('app.logs.level', Level::Error); $app->bind('config', $config); $app->bootstrapWith([ RegisterProviders::class, ]); - $this->assertSame(Logger::ERROR, $app->get('logger')->getHandlers()[0]->getLevel()); + $this->assertSame(Level::Error, $app->get('logger')->getHandlers()[0]->getLevel()); } /** @test */ diff --git a/tests/Unit/Providers/RouterServiceProviderTest.php b/tests/Unit/Providers/RouterServiceProviderTest.php index 780f363..5f7b813 100644 --- a/tests/Unit/Providers/RouterServiceProviderTest.php +++ b/tests/Unit/Providers/RouterServiceProviderTest.php @@ -18,10 +18,10 @@ use Rareloop\Lumberjack\Providers\RouterServiceProvider; use Rareloop\Lumberjack\Test\Unit\BrainMonkeyPHPUnitIntegration; use Rareloop\Router\MiddlewareResolver; -use Zend\Diactoros\Request; -use Zend\Diactoros\Response\HtmlResponse; -use Zend\Diactoros\Response\TextResponse; -use Zend\Diactoros\ServerRequest; +use Laminas\Diactoros\Request; +use Laminas\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\TextResponse; +use Laminas\Diactoros\ServerRequest; class RouterServiceProviderTest extends TestCase { @@ -79,8 +79,7 @@ public function configured_router_can_resolve_middleware_aliases() $store->set('middleware-key', new RSPAddHeaderMiddleware('X-Key', 'abc')); $request = new ServerRequest([], [], '/test/123', 'GET'); - $router->get('/test/123', function () { - })->middleware('middleware-key'); + $router->get('/test/123', function () {})->middleware('middleware-key'); $response = $router->match($request); $this->assertTrue($response->hasHeader('X-Key')); diff --git a/tests/Unit/Providers/WordPressControllersServiceProviderTest.php b/tests/Unit/Providers/WordPressControllersServiceProviderTest.php index f588181..713c84d 100644 --- a/tests/Unit/Providers/WordPressControllersServiceProviderTest.php +++ b/tests/Unit/Providers/WordPressControllersServiceProviderTest.php @@ -22,8 +22,8 @@ use Rareloop\Lumberjack\Providers\WordPressControllersServiceProvider; use Rareloop\Lumberjack\Test\Unit\BrainMonkeyPHPUnitIntegration; use Rareloop\Router\Responsable; -use Zend\Diactoros\Response\TextResponse; -use Zend\Diactoros\ServerRequest; +use Laminas\Diactoros\Response\TextResponse; +use Laminas\Diactoros\ServerRequest; use \Mockery; use Rareloop\Lumberjack\Http\Middleware\PasswordProtected; diff --git a/tests/Unit/Providers/includes/single.php b/tests/Unit/Providers/includes/single.php index e69de29..b3d9bbc 100644 --- a/tests/Unit/Providers/includes/single.php +++ b/tests/Unit/Providers/includes/single.php @@ -0,0 +1 @@ +shouldReceive('read')->andReturn($previousSessionValue); + $handler->shouldReceive('read')->andReturn(@serialize(['foo' => 'bar'])); $errorHandler = Mockery::mock(HandlerInterface::class); $errorHandler->shouldReceive('report')->once(); @@ -73,11 +72,40 @@ public function unexpected_session_data_is_handled_gracefully($previousSessionVa $this->assertSame(null, $store->get('foo')); } - public function unexpectedSessionData() + /** + * @test + */ + public function gracefully_handle_case_with_no_exception_handler() { - return [ - [@serialize(['foo' => 'bar'])], - [''], - ]; + $encryptionKey = 'encryption-key'; + + // Use a mock handler to fake a previously stored state + $handler = Mockery::mock(NullSessionHandler::class . '[read]'); + $handler->shouldReceive('read')->andReturn(@serialize(['foo' => 'bar'])); + + $store = new EncryptedStore('session-name', $handler, new Encrypter($encryptionKey), 'session-id'); + $store->start(); + + $this->assertSame(null, $store->get('foo')); + } + + /** + * @test + */ + public function empty_session_data_is_ignored() + { + $encryptionKey = 'encryption-key'; + + // Use a mock handler to fake a previously stored state + $handler = Mockery::mock(NullSessionHandler::class . '[read]'); + $handler->shouldReceive('read')->andReturn(''); + + $errorHandler = Mockery::mock(HandlerInterface::class); + $errorHandler->shouldNotHaveReceived('report'); + + $store = new EncryptedStore('session-name', $handler, new Encrypter($encryptionKey), 'session-id', $errorHandler); + $store->start(); + + $this->assertSame(null, $store->get('foo')); } } diff --git a/tests/Unit/Session/SessionManagerTest.php b/tests/Unit/Session/SessionManagerTest.php index c46a2c9..f1e34aa 100644 --- a/tests/Unit/Session/SessionManagerTest.php +++ b/tests/Unit/Session/SessionManagerTest.php @@ -3,17 +3,20 @@ namespace Rareloop\Lumberjack\Test; use Mockery; +use ReflectionClass; +use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\TestCase; -use Rareloop\Lumberjack\Application; use Rareloop\Lumberjack\Config; -use Rareloop\Lumberjack\Contracts\Encrypter as EncrypterContract; use Rareloop\Lumberjack\Encrypter; +use Rareloop\Lumberjack\Application; +use Rareloop\Lumberjack\Session\Store; +use Rareloop\Lumberjack\Exceptions\Handler; use Rareloop\Lumberjack\Session\EncryptedStore; -use Rareloop\Lumberjack\Session\FileSessionHandler; use Rareloop\Lumberjack\Session\SessionManager; -use Rareloop\Lumberjack\Session\Store; +use Rareloop\Lumberjack\Session\FileSessionHandler; +use Rareloop\Lumberjack\Exceptions\HandlerInterface; use Rareloop\Lumberjack\Test\Unit\Session\NullSessionHandler; -use org\bovigo\vfs\vfsStream; +use Rareloop\Lumberjack\Contracts\Encrypter as EncrypterContract; class SessionManagerTest extends TestCase { @@ -93,12 +96,22 @@ public function can_create_an_unencrypted_store() /** @test */ public function can_create_an_encrypted_store() { - $app = $app = $this->appWithSessionDriverConfig('file', 'lumberjack', $encrypted = true); + $app = $this->appWithSessionDriverConfig('file', 'lumberjack', $encrypted = true); $app->bind(EncrypterContract::class, new Encrypter('encryption-key')); + $handler = Mockery::mock(Handler::class); + $app->bind(HandlerInterface::class, $handler); + $manager = new SessionManager($app); - $this->assertInstanceOf(EncryptedStore::class, $manager->driver()); + $driver = $manager->driver(); + $this->assertInstanceOf(EncryptedStore::class, $driver); + + $reflection = new ReflectionClass($driver); + $property = $reflection->getProperty('exceptionHandler'); + $property->setAccessible(true); + + $this->assertInstanceOf(HandlerInterface::class, $property->getValue($driver)); } }