From 60f8b0007fff45043925de0b2f052837a0d25ffe Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 15 Mar 2022 12:02:24 +0300 Subject: [PATCH] First commit --- .gitignore | 5 ++ README.md | 107 +++++++++++++++++++++++++ composer.json | 24 ++++++ src/BaseHandler.php | 80 ++++++++++++++++++ src/Encrypt.php | 56 +++++++++++++ src/Exceptions/EncryptionException.php | 20 +++++ src/HandlerInterface.php | 27 +++++++ src/OpenSSL.php | 81 +++++++++++++++++++ src/Sodium.php | 92 +++++++++++++++++++++ 9 files changed, 492 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 src/BaseHandler.php create mode 100644 src/Encrypt.php create mode 100644 src/Exceptions/EncryptionException.php create mode 100644 src/HandlerInterface.php create mode 100644 src/OpenSSL.php create mode 100644 src/Sodium.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a879886 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.idea/ +/.vs/ +/.vscode/ +/vendor/ +/composer.lock \ No newline at end of file diff --git a/README.md b/README.md index 478f1f3..3a63b86 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,109 @@ # Encryption PHP OpenSSL/Sodium Encryption and Decryption + +[![Latest Stable Version](http://poser.pugx.org/initphp/encryption/v)](https://packagist.org/packages/initphp/encryption) [![Total Downloads](http://poser.pugx.org/initphp/encryption/downloads)](https://packagist.org/packages/initphp/encryption) [![Latest Unstable Version](http://poser.pugx.org/initphp/encryption/v/unstable)](https://packagist.org/packages/initphp/encryption) [![License](http://poser.pugx.org/initphp/encryption/license)](https://packagist.org/packages/initphp/encryption) [![PHP Version Require](http://poser.pugx.org/initphp/encryption/require/php)](https://packagist.org/packages/initphp/encryption) + +## Requirements + +- PHP 7.4 or higher +- MB_String extension +- Depending on usage: + - OpenSSL extesion + - Sodium extension + + +## Installation + +``` +composer require initphp/encryption +``` + +## Configuration + +```php +$options = [ + 'algo' => 'SHA256', + 'cipher' => 'AES-256-CTR', + 'key' => null, + 'blocksize' => 16, +]; +``` + +- `algo` : Used by OpenSSL handler only. The algorithm to use to sign the data. +- `cipher` : Used by OpenSSL handler only. The encryption algorithm that will be used to encrypt the data. +- `key` : The top secret key string to use for encryption. +- `blocksize` : It is used for sodium handler only. It is used in the `sodium_pad()` and `sodium_unpad()` functions. + + +## Usage + +```php +require_once "vendor/autoload.php"; +use \InitPHP\Encryption\Encrypt; + +// OpenSSL Handler +/** @var $openssl \InitPHP\Encryption\HandlerInterface */ +$openssl = Encrypt::use(\InitPHP\Encryption\OpenSSL::class, [ + 'algo' => 'SHA256', + 'cipher' => 'AES-256-CTR', + 'key' => 'TOP_Secret_Key', +]); + +// Sodium Handler +/** @var $sodium \InitPHP\Encryption\HandlerInterface */ +$sodium = Encrypt::use(\InitPHP\Encryption\Sodium::class, [ + 'key' => 'TOP_Secret_Key', + 'blocksize' => 16, +]); +``` + +### Methods + +#### `encrypt()` + +```php +public function encrypt(mixed $data, array $options = []): string; +``` + +#### `decrypt()` + +```php +public function decrypt(string $data, array $options = []): mixed; +``` + +## Writing Your Own Handler + +```php +namespace App; + +use \InitPHP\Encryption\{HandlerInterface, BaseHandler}; + +class MyHandler extends BaseHandler implements HandlerInterface +{ + public function encrypt($data, array $options = []): string + { + $options = $this->options($options); + // ... process + } + + public function decrypt($data, array $options = []) + { + $options = $this->options($options); + // ... process + } +} +``` + +```php +use \InitPHP\Encryption\Encrypt; + +$myhandler = Encrypt::use(\App\MyHandler::class); +``` + +## Credits + +- [Muhammet ŞAFAK](https://www.muhammetsafak.com.tr) + +## License + +Copyright © 2022 [MIT License](./LICENSE) \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..637fdb3 --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "initphp/encryption", + "description": "PHP OpenSSL/Sodium Encryption and Decryption", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "InitPHP\\Encryption\\": "src/" + } + }, + "authors": [ + { + "name": "Muhammet ŞAFAK", + "email": "info@muhammetsafak.com.tr", + "role": "Developer", + "homepage": "https://www.muhammetsafak.com.tr" + } + ], + "minimum-stability": "stable", + "require": { + "php": ">=7.4", + "ext-mbstring": "*" + } +} diff --git a/src/BaseHandler.php b/src/BaseHandler.php new file mode 100644 index 0000000..cca8324 --- /dev/null +++ b/src/BaseHandler.php @@ -0,0 +1,80 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Encryption; + +use const CASE_LOWER; + +use function array_merge; +use function array_change_key_case; +use function strtolower; +use function mb_substr; + +abstract class BaseHandler implements HandlerInterface +{ + + protected array $options = [ + 'algo' => 'SHA256', + 'cipher' => 'AES-256-CTR', + 'key' => null, + 'blocksize' => 16, + ]; + + abstract public function encrypt($data, array $options = []): string; + + abstract public function decrypt($data, array $options = []); + + public function __construct(array $options = []) + { + $this->setOptions($options); + } + + public function setOptions(array $options = []): self + { + if(empty($options)){ + return $this; + } + $this->options = array_merge($this->options, array_change_key_case($options, CASE_LOWER)); + return $this; + } + + public function setOption(string $name, $value): self + { + $this->options[strtolower($name)] = $value; + return $this; + } + + public function getOption(string $name, $default = null) + { + $name = strtolower($name); + return $this->options[$name] ?? $default; + } + + public function getOptions(): array + { + return $this->options ?? []; + } + + protected function options(array $options = []): array + { + return empty($options) ? $this->options : array_merge($this->options, array_change_key_case($options, CASE_LOWER)); + } + + protected function substr($str, int $offset, ?int $length = null): string + { + return mb_substr($str, $offset, $length, '8bit'); + } + +} diff --git a/src/Encrypt.php b/src/Encrypt.php new file mode 100644 index 0000000..fe6ffef --- /dev/null +++ b/src/Encrypt.php @@ -0,0 +1,56 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Encryption; + +use \InitPHP\Encryption\Exceptions\EncryptionException; + +use function is_string; +use function class_exists; + +class Encrypt +{ + + /** + * @param string|HandlerInterface $handler + * @param array $options + * @return HandlerInterface + * @throws EncryptionException + */ + public static function use($handler, array $options = []): HandlerInterface + { + if(is_string($handler) && class_exists($handler)){ + $handler = new $handler($options); + $options = null; + } + if(!($handler instanceof HandlerInterface)){ + throw new EncryptionException(''); + } + return empty($options) ? $handler : $handler->setOptions($options); + } + + /** + * @param string|HandlerInterface $handler + * @param array $options + * @return HandlerInterface + * @throws EncryptionException + */ + public static function create($handler, array $options = []): HandlerInterface + { + return self::use($handler, $options); + } + + +} diff --git a/src/Exceptions/EncryptionException.php b/src/Exceptions/EncryptionException.php new file mode 100644 index 0000000..ae5f7d8 --- /dev/null +++ b/src/Exceptions/EncryptionException.php @@ -0,0 +1,20 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Encryption\Exceptions; + +class EncryptionException extends \Exception +{ +} diff --git a/src/HandlerInterface.php b/src/HandlerInterface.php new file mode 100644 index 0000000..2d8d6f6 --- /dev/null +++ b/src/HandlerInterface.php @@ -0,0 +1,27 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Encryption; + +interface HandlerInterface +{ + + public function encrypt($data, array $options = []): string; + + public function decrypt($data, array $options = []); + + public function setOptions(array $options = []): HandlerInterface; + +} diff --git a/src/OpenSSL.php b/src/OpenSSL.php new file mode 100644 index 0000000..7adafdf --- /dev/null +++ b/src/OpenSSL.php @@ -0,0 +1,81 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Encryption; + +use const OPENSSL_RAW_DATA; + +use function extension_loaded; +use function serialize; +use function unserialize; +use function bin2hex; +use function hex2bin; +use function openssl_encrypt; +use function openssl_decrypt; +use function openssl_cipher_iv_length; +use function openssl_random_pseudo_bytes; +use function hash_hkdf; +use function hash_hmac; +use function hash_equals; + +class OpenSSL extends BaseHandler implements HandlerInterface +{ + + public function __construct(array $options = []) + { + if(extension_loaded('openssl') === FALSE){ + throw new \InitPHP\Encryption\Exceptions\EncryptionException('The "openssl" extension must be installed.'); + } + parent::__construct($options); + } + + public function encrypt($data, array $options = []): string + { + $options = $this->options($options); + + $secret = hash_hkdf($options['algo'], $options['key']); + $iv = ($IVSize = openssl_cipher_iv_length($options['cipher'])) ? openssl_random_pseudo_bytes($IVSize) : null; + $data = serialize($data); + + if(($data = openssl_encrypt($data, $options['cipher'], $secret, OPENSSL_RAW_DATA, $iv)) === FALSE){ + throw new \InitPHP\Encryption\Exceptions\EncryptionException('Encryption failed.'); + } + $res = $iv . $data; + $hmac = hash_hmac($options['algo'], $res, $secret, true); + return bin2hex($hmac . $res); + } + + public function decrypt($data, array $options = []) + { + $options = $this->options($options); + $data = hex2bin($data); + $secret = hash_hkdf($options['algo'], $options['key']); + + $hmacLength = $this->substr($options['algo'], 3) / 8; + $hmacKey = $this->substr($data, 0, $hmacLength); + $data = $this->substr($data, $hmacLength); + $hmacCalc = hash_hmac($options['algo'], $data, $secret, true); + if(hash_equals($hmacKey, $hmacCalc) === FALSE){ + throw new \InitPHP\Encryption\Exceptions\EncryptionException('Decryption verification failed.'); + } + $iv = ($ivSize = openssl_cipher_iv_length($options['cipher'])) ? $this->substr($data, 0, $ivSize) : null; + if($iv !== null){ + $data = $this->substr($data, $ivSize); + } + $data = openssl_decrypt($data, $options['cipher'], $secret, OPENSSL_RAW_DATA, $iv); + return unserialize($data); + } + +} diff --git a/src/Sodium.php b/src/Sodium.php new file mode 100644 index 0000000..d575a07 --- /dev/null +++ b/src/Sodium.php @@ -0,0 +1,92 @@ + + * @copyright Copyright © 2022 InitPHP + * @license http://initphp.github.io/license.txt MIT + * @version 1.0 + * @link https://www.muhammetsafak.com.tr + */ + +declare(strict_types=1); + +namespace InitPHP\Encryption; + +use \InitPHP\Encryption\Exceptions\EncryptionException; + +use const SODIUM_CRYPTO_SECRETBOX_NONCEBYTES; +use const SODIUM_CRYPTO_SECRETBOX_MACBYTES; + +use function extension_loaded; +use function bin2hex; +use function hex2bin; +use function serialize; +use function unserialize; +use function random_bytes; +use function mb_strlen; +use function sodium_pad; +use function sodium_crypto_secretbox; +use function sodium_memzero; +use function sodium_crypto_secretbox_open; +use function sodium_unpad; + +class Sodium extends BaseHandler implements HandlerInterface +{ + + public function __construct(array $options = []) + { + if(extension_loaded('sodium') === FALSE){ + throw new \InitPHP\Encryption\Exceptions\EncryptionException('The "sodium" extension must be installed.'); + } + parent::__construct($options); + } + + /** + * @param $data + * @param array $options + * @return string + * @throws \SodiumException + * @throws \Exception + */ + public function encrypt($data, array $options = []): string + { + $options = $this->options($options); + $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $data = serialize($data); + $data = sodium_pad($data, (int)$options['blocksize']); + $ciphertext = $nonce . sodium_crypto_secretbox($data, $nonce, $options['key']); + sodium_memzero($data); + sodium_memzero($options['key']); + return bin2hex($ciphertext); + } + + /** + * @param $data + * @param array $options + * @return mixed + * @throws EncryptionException + * @throws \SodiumException + */ + public function decrypt($data, array $options = []) + { + $options = $this->options($options); + $data = hex2bin($data); + if(mb_strlen($data, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)){ + throw new EncryptionException('Decryption failed!'); + } + $nonce = $this->substr($data, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $ciphertext = $this->substr($data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + + if(($data = sodium_crypto_secretbox_open($ciphertext, $nonce, $options['key'])) === FALSE){ + throw new EncryptionException('Decryption failed!'); + } + $data = sodium_unpad($data, $options['blocksize']); + sodium_memzero($ciphertext); + sodium_memzero($options['key']); + return unserialize($data); + } + +}