Files
CoreLibs-Composer-All/src/Security/SymmetricEncryption.php
Clemens Schwaighofer b7d5a79c3a Allow method chaining in Session and encryption class
For session set/unset/auto write close flag

In the encryption classes for setting keys
2024-12-23 11:36:06 +09:00

315 lines
7.2 KiB
PHP

<?php
/**
* very simple symmetric encryption
* Better use:
* https://paragonie.com/project/halite
* https://github.com/paragonie/halite
*
* current code is just to encrypt and decrypt
*
* must use a valid encryption key created with
* Secruty\CreateKey class
*/
declare(strict_types=1);
namespace CoreLibs\Security;
use CoreLibs\Security\CreateKey;
use SodiumException;
class SymmetricEncryption
{
/** @var SymmetricEncryption self instance */
private static SymmetricEncryption $instance;
/** @var ?string bin hex key */
private ?string $key = null;
/**
* init class
* if key not passed, key must be set with createKey
*
* @param string|null $key encryption key
*/
public function __construct(
?string $key = null
) {
if ($key !== null) {
$this->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__