setPublicKey($public_key); } if ($key_pair !== null) { $this->setKeyPair($key_pair); if (empty($public_key)) { $public_key = CreateKey::getPublicKey($key_pair); $this->setPublicKey($public_key); } } } /** * Returns the singleton self object. * For function wrapper use * * @param string|null $key_pair * @param string|null $public_key * @return AsymmetricAnonymousEncryption object */ public static function getInstance( #[\SensitiveParameter] string|null $key_pair = null, string|null $public_key = null ): self { // new if no instsance or key is different if ( empty(self::$instance) || self::$instance->key_pair != $key_pair || self::$instance->public_key != $public_key ) { self::$instance = new self($key_pair, $public_key); } return self::$instance; } /** * clean up */ public function __destruct() { if (empty($this->key_pair)) { return; } try { // would set it to null, but we we do not want to make key null sodium_memzero($this->key_pair); return; } catch (SodiumException) { // empty catch } if (is_null($this->key_pair)) { return; } $zero = str_repeat("\0", mb_strlen($this->key_pair, '8bit')); $this->key_pair = $this->key_pair ^ ( $zero ^ $this->key_pair ); unset($zero); unset($this->key_pair); /** @phan-suppress-current-line PhanTypeObjectUnsetDeclaredProperty */ } /* ************************************************************************ * MARK: PRIVATE * *************************************************************************/ /** * Create the internal key pair in binary * * @param ?string $key_pair * @return string * @throws \UnexpectedValueException key pair empty * @throws \UnexpectedValueException invalid hex key pair * @throws \RangeException key pair not correct size */ private function createKeyPair( #[\SensitiveParameter] ?string $key_pair ): string { if (empty($key_pair)) { throw new \UnexpectedValueException('Key pair cannot be empty'); } try { $key_pair = CreateKey::hex2bin($key_pair); } catch (SodiumException $e) { sodium_memzero($key_pair); throw new \UnexpectedValueException('Invalid hex key pair: ' . $e->getMessage()); } if (mb_strlen($key_pair, '8bit') !== SODIUM_CRYPTO_BOX_KEYPAIRBYTES) { sodium_memzero($key_pair); throw new \RangeException( 'Key pair is not the correct size (must be ' . SODIUM_CRYPTO_BOX_KEYPAIRBYTES . ' bytes long).' ); } return $key_pair; } /** * create the internal public key in binary * * @param ?string $public_key * @return string * @throws \UnexpectedValueException public key empty * @throws \UnexpectedValueException invalid hex key * @throws \RangeException invalid key length */ private function createPublicKey(?string $public_key): string { if (empty($public_key)) { throw new \UnexpectedValueException('Public key cannot be empty'); } try { $public_key = CreateKey::hex2bin($public_key); } catch (SodiumException $e) { sodium_memzero($public_key); throw new \UnexpectedValueException('Invalid hex public key: ' . $e->getMessage()); } if (mb_strlen($public_key, '8bit') !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) { sodium_memzero($public_key); throw new \RangeException( 'Public key is not the correct size (must be ' . SODIUM_CRYPTO_BOX_PUBLICKEYBYTES . ' bytes long).' ); } return $public_key; } /** * encrypt a message asymmetric with a bpulic key * * @param string $message * @param ?string $public_key * @return string * @throws \UnexpectedValueException create encryption failed * @throws \UnexpectedValueException convert to base64 failed */ private function asymmetricEncryption( #[\SensitiveParameter] string $message, ?string $public_key ): string { $public_key = $this->createPublicKey($public_key); try { $encrypted = sodium_crypto_box_seal($message, $public_key); } catch (SodiumException $e) { sodium_memzero($message); throw new \UnexpectedValueException("Create encrypted message failed: " . $e->getMessage()); } sodium_memzero($message); try { $result = sodium_bin2base64($encrypted, SODIUM_BASE64_VARIANT_ORIGINAL); } catch (SodiumException $e) { sodium_memzero($encrypted); throw new \UnexpectedValueException("bin2base64 failed: " . $e->getMessage()); } sodium_memzero($encrypted); return $result; } /** * decrypt a message that is asymmetric encrypted with a key pair * * @param string $message * @param ?string $key_pair * @return string * @throws \UnexpectedValueException message string empty * @throws \UnexpectedValueException base64 decoding failed * @throws \UnexpectedValueException decryption failed * @throws \UnexpectedValueException could not decrypt message */ private function asymmetricDecryption( #[\SensitiveParameter] string $message, #[\SensitiveParameter] ?string $key_pair ): string { if (empty($message)) { throw new \UnexpectedValueException('Encrypted string cannot be empty'); } $key_pair = $this->createKeyPair($key_pair); try { $result = sodium_base642bin($message, SODIUM_BASE64_VARIANT_ORIGINAL); } catch (SodiumException $e) { sodium_memzero($message); sodium_memzero($key_pair); throw new \UnexpectedValueException("base642bin failed: " . $e->getMessage()); } sodium_memzero($message); $plaintext = false; try { $plaintext = sodium_crypto_box_seal_open($result, $key_pair); } catch (SodiumException $e) { sodium_memzero($message); sodium_memzero($key_pair); sodium_memzero($result); throw new \UnexpectedValueException("Decrypting message failed: " . $e->getMessage()); } sodium_memzero($key_pair); sodium_memzero($result); if (!is_string($plaintext)) { throw new \UnexpectedValueException('Invalid key pair'); } return $plaintext; } /* ************************************************************************ * MARK: PUBLIC * *************************************************************************/ /** * sets the private key for encryption * * @param string $key_pair Key pair in hex * @return AsymmetricAnonymousEncryption * @throws \UnexpectedValueException key pair empty */ public function setKeyPair( #[\SensitiveParameter] string $key_pair ): AsymmetricAnonymousEncryption { if (empty($key_pair)) { throw new \UnexpectedValueException('Key pair cannot be empty'); } // check if valid; $this->createKeyPair($key_pair); // set new key pair $this->key_pair = $key_pair; sodium_memzero($key_pair); // set public key if not set if (empty($this->public_key)) { $this->public_key = CreateKey::getPublicKey($this->key_pair); // check if valid $this->createPublicKey($this->public_key); } return $this; } /** * check if set key pair matches given one * * @param string $key_pair * @return bool */ public function compareKeyPair( #[\SensitiveParameter] string $key_pair ): bool { return $this->key_pair === $key_pair; } /** * get the current set key pair, null if not set * * @return string|null */ public function getKeyPair(): ?string { return $this->key_pair; } /** * sets the public key for decryption * if only key pair exists Security\Create::getPublicKey() can be used to * extract the public key from the key pair * * @param string $public_key Public Key in hex * @return AsymmetricAnonymousEncryption * @throws \UnexpectedValueException public key empty */ public function setPublicKey(string $public_key): AsymmetricAnonymousEncryption { if (empty($public_key)) { throw new \UnexpectedValueException('Public key cannot be empty'); } // check if valid $this->createPublicKey($public_key); $this->public_key = $public_key; sodium_memzero($public_key); return $this; } /** * check if the set public key matches the given one * * @param string $public_key * @return bool */ public function comparePublicKey(string $public_key): bool { return $this->public_key === $public_key; } /** * get the current set public key, null if not set * * @return string|null */ public function getPublicKey(): ?string { return $this->public_key; } /** * Encrypt a message with a public key * static version * * @param string $message Message to encrypt * @param string $public_key Public key in hex to encrypt message with * @return string Encrypted message as hex string */ public static function encryptKey( #[\SensitiveParameter] string $message, string $public_key ): string { return self::getInstance()->asymmetricEncryption($message, $public_key); } /** * Encrypt a message * * @param string $message Message to ecnrypt * @return string Encrypted message as hex string */ public function encrypt( #[\SensitiveParameter] string $message ): string { return $this->asymmetricEncryption($message, $this->public_key); } /** * decrypt a message with a key pair * static version * * @param string $message Message to decrypt in hex * @param string $key_pair Key pair in hex to decrypt the message with * @return string Decrypted message */ public static function decryptKey( #[\SensitiveParameter] string $message, #[\SensitiveParameter] string $key_pair ): string { return self::getInstance()->asymmetricDecryption($message, $key_pair); } /** * decrypt a message * * @param string $message Message to decrypt in hex * @return string Decrypted message */ public function decrypt( #[\SensitiveParameter] string $message ): string { return $this->asymmetricDecryption($message, $this->key_pair); } } // __END__