diff --git a/src/Security/SymmetricEncryption.php b/src/Security/SymmetricEncryption.php index 8062fb5..c12f4b3 100644 --- a/src/Security/SymmetricEncryption.php +++ b/src/Security/SymmetricEncryption.php @@ -21,66 +21,82 @@ use SodiumException; class SymmetricEncryption { + /** @var SymmetricEncryption self instance */ + private static SymmetricEncryption $instance; + + /** @var string bin hex key */ + private string $key = ''; + + /** + * init class + * if key not passed, key must be set with createKey + * + * @param string|null|null $key + */ + public function __construct( + string|null $key = null + ) { + if ($key != null) { + $this->setKey($key); + } + } + + /** + * Returns the singleton self object. + * For function wrapper use + * + * @return SymmetricEncryption object + */ + public static function getInstance(string|null $key = null): self + { + if (empty(self::$instance)) { + self::$instance = new self($key); + } + return self::$instance; + } + + /* ************************************************************************ + * MARK: PRIVATE + * *************************************************************************/ + /** * create key and check validity * * @param string $key The key from which the binary key will be created * @return string Binary key string */ - public static function createKey(string $key): string + private function createKey(string $key): string { try { $key = CreateKey::hex2bin($key); } catch (SodiumException $e) { - throw new \UnexpectedValueException('Invalid hex key'); + throw new \UnexpectedValueException('Invalid hex key: ' . $e->getMessage()); } if (mb_strlen($key, '8bit') !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { throw new \RangeException( 'Key is not the correct size (must be ' - . 'SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes long).' + . SODIUM_CRYPTO_SECRETBOX_KEYBYTES . ' bytes long).' ); } return $key; } /** - * Encrypt a message + * Decryption call * - * @param string $message Message to encrypt - * @param string $key Encryption key (as hex string) - * @return string - * @throws \Exception + * @param string $encrypted Text to decrypt + * @param ?string $key Mandatory encryption key, will throw exception if empty + * @return string Plain text * @throws \RangeException + * @throws \UnexpectedValueException + * @throws \UnexpectedValueException */ - public static function encrypt(string $message, string $key): string + private function decryptData(string $encrypted, ?string $key): string { - $key = self::createKey($key); - $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); - - $cipher = base64_encode( - $nonce - . sodium_crypto_secretbox( - $message, - $nonce, - $key - ) - ); - sodium_memzero($message); - sodium_memzero($key); - return $cipher; - } - - /** - * Decrypt a message - * - * @param string $encrypted Message encrypted with safeEncrypt() - * @param string $key Encryption key (as hex string) - * @return string - * @throws \Exception - */ - public static function decrypt(string $encrypted, string $key): string - { - $key = self::createKey($key); + if (empty($key)) { + throw new \UnexpectedValueException('Key not set'); + } + $key = $this->createKey($key); $decoded = base64_decode($encrypted); $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); @@ -93,7 +109,7 @@ class SymmetricEncryption $key ); } catch (SodiumException $e) { - throw new \UnexpectedValueException('Invalid ciphertext (too short)'); + throw new \UnexpectedValueException('Decipher message failed: ' . $e->getMessage()); } if (!is_string($plain)) { throw new \UnexpectedValueException('Invalid Key'); @@ -102,6 +118,117 @@ class SymmetricEncryption sodium_memzero($key); return $plain; } + + /** + * Encrypt a message + * + * @param string $message Message to encrypt + * @param ?string $key Mandatory encryption key, will throw exception if empty + * @return string + * @throws \Exception + * @throws \RangeException + */ + private function encryptData(string $message, ?string $key): string + { + if (empty($this->key) || $key === null) { + throw new \UnexpectedValueException('Key not set'); + } + $key = $this->createKey($key); + $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + try { + $cipher = base64_encode( + $nonce + . sodium_crypto_secretbox( + $message, + $nonce, + $key, + ) + ); + } catch (SodiumException $e) { + throw new \UnexpectedValueException("Create encrypted message failed: " . $e->getMessage()); + } + sodium_memzero($message); + sodium_memzero($key); + return $cipher; + } + + /* ************************************************************************ + * MARK: PUBLIC + * *************************************************************************/ + + + /** + * set a new key for encryption + * + * @param string $key + * @return void + */ + public function setKey(string $key) + { + if (empty($key)) { + throw new \UnexpectedValueException('Key cannot be empty'); + } + $this->key = $key; + } + + /** + * Decrypt a message + * static version + * + * @param string $encrypted Message encrypted with safeEncrypt() + * @param string $key Encryption key (as hex string) + * @return string + * @throws \Exception + * @throws \RangeException + * @throws \UnexpectedValueException + * @throws \UnexpectedValueException + */ + public static function decryptKey(string $encrypted, string $key): string + { + return self::getInstance()->decryptData($encrypted, $key); + } + + /** + * Decrypt a message + * + * @param string $encrypted Message encrypted with safeEncrypt() + * @return string + * @throws \RangeException + * @throws \UnexpectedValueException + * @throws \UnexpectedValueException + */ + public function decrypt(string $encrypted): string + { + return $this->decryptData($encrypted, $this->key); + } + + /** + * Encrypt a message + * static version + * + * @param string $message Message to encrypt + * @param string $key Encryption key (as hex string) + * @return string + * @throws \Exception + * @throws \RangeException + */ + public static function encryptKey(string $message, string $key): string + { + return self::getInstance()->encryptData($message, $key); + } + + /** + * Encrypt a message + * + * @param string $message Message to encrypt + * @return string + * @throws \Exception + * @throws \RangeException + */ + public function encrypt(string $message): string + { + return $this->encryptData($message, $this->key); + } } // __END__ diff --git a/test/phpunit/Security/CoreLibsSecuritySymmetricEncryptionTest.php b/test/phpunit/Security/CoreLibsSecuritySymmetricEncryptionTest.php index 33a2f45..d3f502a 100644 --- a/test/phpunit/Security/CoreLibsSecuritySymmetricEncryptionTest.php +++ b/test/phpunit/Security/CoreLibsSecuritySymmetricEncryptionTest.php @@ -46,12 +46,34 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase public function testEncryptDecryptSuccess(string $input, string $expected): void { $key = CreateKey::generateRandomKey(); - $encrypted = SymmetricEncryption::encrypt($input, $key); - $decrypted = SymmetricEncryption::decrypt($encrypted, $key); + + // test class + $crypt = new SymmetricEncryption($key); + $encrypted = $crypt->encrypt($input); + $decrypted = $crypt->decrypt($encrypted); + $this->assertEquals( + $expected, + $decrypted, + 'Class call', + ); + + // test indirect + $encrypted = SymmetricEncryption::getInstance($key)->encrypt($input); + $decrypted = SymmetricEncryption::getInstance($key)->decrypt($encrypted); + $this->assertEquals( + $expected, + $decrypted, + 'Class Instance call', + ); + + // test static + $encrypted = SymmetricEncryption::encryptKey($input, $key); + $decrypted = SymmetricEncryption::decryptKey($encrypted, $key); $this->assertEquals( $expected, - $decrypted + $decrypted, + 'Static call', ); } @@ -86,10 +108,24 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase public function testEncryptFailed(string $input, string $exception_message): void { $key = CreateKey::generateRandomKey(); - $encrypted = SymmetricEncryption::encrypt($input, $key); $wrong_key = CreateKey::generateRandomKey(); + + // wrong key in class call + $crypt = new SymmetricEncryption($key); + $encrypted = $crypt->encrypt($input); $this->expectExceptionMessage($exception_message); - SymmetricEncryption::decrypt($encrypted, $wrong_key); + $crypt->setKey($key); + $crypt->decrypt($encrypted); + + // class instance + $encrypted = SymmetricEncryption::getInstance($key)->encrypt($input); + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::getInstance($wrong_key)->decrypt($encrypted); + + // class static + $encrypted = SymmetricEncryption::encryptKey($input, $key); + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::decryptKey($encrypted, $wrong_key); } /** @@ -107,7 +143,6 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase 'too short hex key' => [ 'key' => '1cabd5cba9e042f12522f4ff2de5c31d233b', 'excpetion_message' => 'Key is not the correct size (must be ' - . 'SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes long).' ], ]; } @@ -126,13 +161,33 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase */ public function testWrongKey(string $key, string $exception_message): void { - $this->expectExceptionMessage($exception_message); - SymmetricEncryption::encrypt('test', $key); - // we must encrypt valid thing first so we can fail with the wrong kjey $enc_key = CreateKey::generateRandomKey(); - $encrypted = SymmetricEncryption::encrypt('test', $enc_key); + + // class + $crypt = new SymmetricEncryption($key); $this->expectExceptionMessage($exception_message); - SymmetricEncryption::decrypt($encrypted, $key); + $crypt->encrypt('test'); + $crypt->setKey($enc_key); + $encrypted = $crypt->encrypt('test'); + $this->expectExceptionMessage($exception_message); + $crypt->setKey($key); + $crypt->decrypt($encrypted); + + // class instance + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::getInstance($key)->encrypt('test'); + // we must encrypt valid thing first so we can fail with the wrong key + $encrypted = SymmetricEncryption::getInstance($enc_key)->encrypt('test'); + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::getInstance($key)->decrypt($encrypted); + + // class static + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::encryptKey('test', $key); + // we must encrypt valid thing first so we can fail with the wrong key + $encrypted = SymmetricEncryption::encryptKey('test', $enc_key); + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::decryptKey($encrypted, $key); } /** @@ -145,7 +200,7 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase return [ 'too short ciphertext' => [ 'input' => 'short', - 'exception_message' => 'Invalid ciphertext (too short)' + 'exception_message' => 'Decipher message failed: ' ], ]; } @@ -164,8 +219,18 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase public function testWrongCiphertext(string $input, string $exception_message): void { $key = CreateKey::generateRandomKey(); + // class + $crypt = new SymmetricEncryption($key); $this->expectExceptionMessage($exception_message); - SymmetricEncryption::decrypt($input, $key); + $crypt->decrypt($input); + + // class instance + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::getInstance($key)->decrypt($input); + + // class static + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::decryptKey($input, $key); } }