315 lines
7.2 KiB
PHP
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__
|