setKey($key); } } /** * Returns the singleton self object. * For function wrapper use * * @param string|null $key encryption key * @return SymmetricEncryption object */ public static function getInstance(?string $key = null): self { // new if no instsance or key is different if ( empty(self::$instance) || self::$instance->key != $key ) { self::$instance = new self($key); } return self::$instance; } /** * clean up * * @return void */ public function __deconstruct() { if (empty($this->key)) { return; } try { // would set it to null, but we we do not want to make key null sodium_memzero($this->key); return; } catch (SodiumException) { // empty catch } if (is_null($this->key)) { return; } $zero = str_repeat("\0", mb_strlen($this->key, '8bit')); $this->key = $this->key ^ ( $zero ^ $this->key ); unset($zero); unset($this->key); /** @phan-suppress-current-line PhanTypeObjectUnsetDeclaredProperty */ } /* ************************************************************************ * 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 * @throws \UnexpectedValueException empty key * @throws \UnexpectedValueException invalid hex key * @throws \RangeException invalid length */ private function createKey( #[\SensitiveParameter] ?string $key ): string { if (empty($key)) { throw new \UnexpectedValueException('Key cannot be empty'); } try { $key = CreateKey::hex2bin($key); } catch (SodiumException $e) { 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).' ); } return $key; } /** * Decryption call * * @param string $encrypted Text to decrypt * @param ?string $key Mandatory encryption key, will throw exception if empty * @return string Plain text * @throws \UnexpectedValueException key cannot be empty * @throws \UnexpectedValueException decipher message failed * @throws \UnexpectedValueException invalid key */ private function decryptData( #[\SensitiveParameter] string $encrypted, #[\SensitiveParameter] ?string $key ): string { if (empty($encrypted)) { throw new \UnexpectedValueException('Encrypted string cannot be empty'); } $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'); $plaintext = false; try { $plaintext = sodium_crypto_secretbox_open( $ciphertext, $nonce, $key ); } catch (SodiumException $e) { sodium_memzero($ciphertext); sodium_memzero($key); throw new \UnexpectedValueException('Decipher message failed: ' . $e->getMessage()); } sodium_memzero($ciphertext); sodium_memzero($key); if (!is_string($plaintext)) { throw new \UnexpectedValueException('Invalid Key'); } return $plaintext; } /** * Encrypt a message * * @param string $message Message to encrypt * @param ?string $key Mandatory encryption key, will throw exception if empty * @return string Ciphered text * @throws \UnexpectedValueException create message failed */ private function encryptData( #[\SensitiveParameter] string $message, #[\SensitiveParameter] ?string $key ): string { $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) { sodium_memzero($message); sodium_memzero($key); 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 SymmetricEncryption * @throws \UnexpectedValueException key cannot be empty */ public function setKey( #[\SensitiveParameter] string $key ): SymmetricEncryption { if (empty($key)) { throw new \UnexpectedValueException('Key cannot be empty'); } // check that this is a valid key $this->createKey($key); // set key $this->key = $key; sodium_memzero($key); return $this; } /** * Checks if set key is equal to parameter key * * @param string $key * @return bool */ public function compareKey( #[\SensitiveParameter] string $key ): bool { return $key === $this->key; } /** * returns the current set key, null if not set * * @return ?string */ public function getKey(): ?string { return $this->key; } /** * Decrypt a message * static version * * @param string $encrypted Message encrypted with safeEncrypt() * @param string $key Encryption key (as hex string) * @return string */ public static function decryptKey( #[\SensitiveParameter] string $encrypted, #[\SensitiveParameter] string $key ): string { return self::getInstance()->decryptData($encrypted, $key); } /** * Decrypt a message * * @param string $encrypted Message encrypted with safeEncrypt() * @return string */ public function decrypt( #[\SensitiveParameter] 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 */ public static function encryptKey( #[\SensitiveParameter] string $message, #[\SensitiveParameter] string $key ): string { return self::getInstance()->encryptData($message, $key); } /** * Encrypt a message * * @param string $message Message to encrypt * @return string */ public function encrypt( #[\SensitiveParameter] string $message ): string { return $this->encryptData($message, $this->key); } } // __END__