parent
434883baa8
commit
c3a97ec616
@ -0,0 +1,240 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
/** |
||||
* @license ISC |
||||
* @copyright Copyright (c) 2016-2023, Paragon Initiative Enterprises |
||||
* @link https://paragonie.com/book/pecl-libsodium/read/05-publickey-crypto.md |
||||
*/ |
||||
|
||||
namespace IOcornerstone\Framework\ParagonCrypto; |
||||
|
||||
use IOcornerstone\Framework\RandomEngine; |
||||
|
||||
class Crypto { |
||||
|
||||
const singleKey = true; |
||||
const multipleKeys = false; |
||||
|
||||
private $rnd; |
||||
|
||||
|
||||
public function __construct(#[\SensitiveParameter] private string $key) { |
||||
$this->rnd = new RandomEngine(); |
||||
} |
||||
/* |
||||
* Secret key encryption (or symmetric encryption as it’s also |
||||
* known) uses a single key to both encrypt and decrypt data. |
||||
* In the past PHP relied on mcrypt and openssl for secret key |
||||
* encryption. PHP 7.2 introduced Sodium, which is more modern |
||||
* and widely considered more secure. |
||||
* |
||||
* In order to encrypt a value, first you’ll need an encryption |
||||
* key, which can be generated using the |
||||
* sodium_crypto_secretbox_keygen() function. |
||||
* |
||||
* $key = sodium_crypto_secretbox_keygen(); |
||||
* You can also use the random_bytes() function with the |
||||
* SODIUM_CRYPTO_SECRETBOX_KEYBYTES integer constant for the |
||||
* key length, but using sodium_crypto_secretbox_keygen() |
||||
* ensures that the key length is always correct (i.e. not too |
||||
* short), and it’s easier. |
||||
* |
||||
* $key = random_bytes( SODIUM_CRYPTO_SECRETBOX_KEYBYTES ); |
||||
* Either way, you’ll usually only do this once and store the |
||||
* result as an environment variable. Remember that this key |
||||
* must be kept secret at all costs. If the key is ever |
||||
* compromised, so is any data encrypted by using it. |
||||
*/ |
||||
|
||||
public function encrypt( |
||||
#[\SensitiveParameter] string $message, |
||||
bool $key_usage = self::singleKey |
||||
): string { |
||||
$key = $this->key; |
||||
|
||||
$nonce = $this->rnd->get_bytes( |
||||
SODIUM_CRYPTO_SECRETBOX_NONCEBYTES |
||||
); |
||||
$fn = ($key_usage == self::singleKey) ? "sodium_crypto_secretbox" : "sodium_crypto_box"; |
||||
$cipher = base64_encode( |
||||
$nonce . |
||||
$fn( |
||||
$message, |
||||
$nonce, |
||||
base64_decode($key) |
||||
) |
||||
); |
||||
sodium_memzero($message); |
||||
sodium_memzero($key); |
||||
return $cipher; |
||||
} |
||||
|
||||
public function decrypt( |
||||
string $encrypted, |
||||
bool $key_usage = self::singleKey |
||||
): string | false { |
||||
$key = $this->key; |
||||
|
||||
$decoded = base64_decode($encrypted); |
||||
if ($decoded === false) { |
||||
throw new Exception('Unable to decode'); |
||||
} |
||||
if (mb_strlen($decoded, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)) { |
||||
throw new Exception('Message was truncated'); |
||||
} |
||||
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); |
||||
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); |
||||
|
||||
$fn = ($key_usage == self::singleKey) ? "sodium_crypto_secretbox_open" : "sodium_crypto_box_open"; |
||||
$plain = $fn( |
||||
$ciphertext, |
||||
$nonce, |
||||
base64_decode($key) |
||||
); |
||||
sodium_memzero($ciphertext); |
||||
sodium_memzero($key); |
||||
return $plain; |
||||
} |
||||
|
||||
public static function makeUserBoxKp() { |
||||
return sodium_crypto_box_keypair(); |
||||
} |
||||
|
||||
public static function makeUserSignKp() { |
||||
return sodium_crypto_sign_keypair(); |
||||
} |
||||
|
||||
public static function getUserBoxSecretKey(string $from_make_user_box_kp): string { |
||||
return base64_encode(sodium_crypto_box_secretkey($from_make_user_box_kp)); |
||||
} |
||||
|
||||
public static function getUserBoxPublicKey(string $from_make_user_box_kp): string { |
||||
return base64_encode(sodium_crypto_box_publickey($from_make_user_box_kp)); |
||||
} |
||||
|
||||
public static function getUserSignSecretKey(string $from_make_user_sign_secret_kp): string { |
||||
return sodium_crypto_sign_secretkey($from_make_user_sign_secret_kp); |
||||
} |
||||
|
||||
public static function getUserSignPublicKey(string $from_make_user_sign_public_kp): string { |
||||
return sodium_crypto_sign_secretkey($from_make_user_sign_public_kp); |
||||
} |
||||
|
||||
public static function boxReassembleKeypair( |
||||
string $from_get_userA_box_secret_key, |
||||
string $from_get_userB_box_public_key |
||||
): string { |
||||
return base64_encode(sodium_crypto_box_keypair_from_secretkey_and_publickey( |
||||
base64_decode($from_get_userA_box_secret_key), |
||||
base64_decode($from_get_userB_box_public_key) |
||||
)); |
||||
} |
||||
|
||||
public static function signMessage(#[\SensitiveParameter] string $message, #[\SensitiveParameter] string $user_sign_secretkey): string { |
||||
return sodium_crypto_sign( |
||||
$message, |
||||
$user_sign_secretkey |
||||
); |
||||
} |
||||
|
||||
public static function getSignedMessage(#[\SensitiveParameter] string $signed_message, #[\SensitiveParameter] string $user_sign_publickey): string | false { |
||||
return sodium_crypto_sign_open( |
||||
$signed_message, |
||||
$user_sign_publickey |
||||
); |
||||
} |
||||
|
||||
public static function makeJustSignature(#[\SensitiveParameter] string $message, #[\SensitiveParameter] string $user_sign_secretkey): string { |
||||
return sodium_crypto_sign_detached( |
||||
$message, |
||||
$user_sign_secretkey |
||||
); |
||||
} |
||||
|
||||
public static function isValidSignature( |
||||
#[\SensitiveParameter] string $signature, |
||||
#[\SensitiveParameter] string $message, |
||||
#[\SensitiveParameter] string $user_sign_public_key |
||||
): bool { |
||||
if ( sodium_crypto_sign_verify_detached( |
||||
$signature, |
||||
$message, |
||||
$user_sign_publickey |
||||
) ) { |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
public static function aSingleKeyMaker(): string { |
||||
$rnd = new RandomEngine(); |
||||
return base64_encode($rnd->get_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES)); |
||||
} |
||||
/* |
||||
* Extract the public key from the secret key |
||||
*/ |
||||
public static function signPublickeyFromSecretkey(#[\SensitiveParameter] string $key) { |
||||
return sodium_crypto_sign_publickey_from_secretkey($key); |
||||
} |
||||
|
||||
public static function incrementSequentialNonce($x) { |
||||
return sodium_increment($x); // The obtain the next nonce |
||||
} |
||||
|
||||
// Should you Proceed with crypto_box decryption, if true go ahead do it. |
||||
public static function isSafeToDecode($message_nonce, $expected_nonce): bool { |
||||
return (sodium_compare($message_nonce, $expected_nonce) === 0); |
||||
} |
||||
|
||||
/* |
||||
* Sometimes you don't need to hide the contents of a message with encryption, |
||||
* but you still want to ensure that nobody on the network can tamper with |
||||
* it. For example, if you want to eschew server-side session storage and |
||||
* instead use HTTP cookies as your storage mechanism. |
||||
*/ |
||||
public static function signOutboundMessageOnly(#[\SensitiveParameter] string $message, #[\SensitiveParameter] string $key): string { |
||||
$mac = sodium_crypto_auth($message, $key); |
||||
return $mac . "@@" . $message; |
||||
} |
||||
|
||||
public static function isValidInboundMessage(#[\SensitiveParameter] string $message, #[\SensitiveParameter] string $key): bool { |
||||
$a = explode("@@", $message, 2); |
||||
$mac = $a[0]; |
||||
$old_message = $a[1]; |
||||
$ret_bool = (sodium_crypto_auth_verify($mac, $old_message, $key)); |
||||
if ($ret_bool === false) { |
||||
sodium_memzero($key); |
||||
throw new \Exception("Malformed message or invalid MAC"); |
||||
} |
||||
return $ret_bool; |
||||
} |
||||
|
||||
} |
||||
/* |
||||
* Usage: |
||||
* |
||||
$key = ParagonCrypto\Crypto::SingleKeyMaker(); |
||||
$enc = ParagonCrypto\Crypto::safeEncrypt('Encrypt This String...', $key, ParagonCrypto\Crypto::singleKey); |
||||
echo $enc . "<br/>"; |
||||
$dec = ParagonCrypto\Crypto::safeDecrypt($enc, $key, ParagonCrypto\Crypto::singleKey); |
||||
echo $dec . "<br/>";; |
||||
// -------------------- |
||||
|
||||
$key_pair_Alice = ParagonCrypto\Crypto::makeUserBoxKp(); |
||||
$secret_Alice = ParagonCrypto\Crypto::getUserBoxSecretKey($key_pair_Alice); |
||||
$public_Alice = ParagonCrypto\Crypto::getUserBoxPublicKey($key_pair_Alice); |
||||
|
||||
$key_pair_Bob = ParagonCrypto\Crypto::makeUserBoxKp(); |
||||
$secret_Bob = ParagonCrypto\Crypto::getUserBoxSecretKey($key_pair_Bob); |
||||
$public_Bob = ParagonCrypto\Crypto::getUserBoxPublicKey($key_pair_Bob); |
||||
|
||||
$kA = ParagonCrypto\Crypto::boxReassembleKeypair($secret_Alice, $public_Bob); |
||||
$enc = ParagonCrypto\Crypto::safeEncrypt('Encrypt This String2...', $kA, ParagonCrypto\Crypto::multipleKeys); |
||||
echo $enc . "<br/>"; |
||||
|
||||
$kB = ParagonCrypto\Crypto::boxReassembleKeypair($secret_Bob, $public_Alice); |
||||
$dec = ParagonCrypto\Cyypto::safeDecrypt($enc, $kB, ParagonCrypto\Crypto::multipleKeys); |
||||
echo $dec; |
||||
*/ |
||||
@ -0,0 +1,128 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace IOcornerstone\Framework\ParagonCrypto; |
||||
/** |
||||
* @copyright Paragon Initiative Enterprises |
||||
* @license Creative Commons Attribution 4.0 International / ISC |
||||
* @link https://github.com/paragonie/pecl-libsodium-doc/blob/master/chapters/09-recipes.md |
||||
*/ |
||||
|
||||
use IOcornerstone\Framework\RandomEngine; |
||||
|
||||
class PasswordStorage { |
||||
|
||||
const SALT_SIZE_IN_BYTES = 16; |
||||
private $random_engine; |
||||
|
||||
public function __construct() { |
||||
$this->random_engine = new RandomEngine(); |
||||
} |
||||
|
||||
public function generateKey(): string { |
||||
return sodium_bin2hex($this->random_engine->get_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES)); |
||||
} |
||||
|
||||
/** |
||||
* Hash then encrypt a password |
||||
* |
||||
* @param string $password - The user's password |
||||
* @param string $secret_key - The main_magic key for all passwords |
||||
* @return string |
||||
*/ |
||||
public function hash(#[\SensitiveParameter] string $password, #[\SensitiveParameter] string $secret_key): string { |
||||
// First, let's calculate the hash |
||||
$hashed = sodium_crypto_pwhash_scryptsalsa208sha256_str( |
||||
$password, |
||||
SODIUM_CRYPTO_PWHASH_SCRYPTSALSA208SHA256_OPSLIMIT_INTERACTIVE, |
||||
SODIUM_CRYPTO_PWHASH_SCRYPTSALSA208SHA256_MEMLIMIT_INTERACTIVE |
||||
); |
||||
$salt = $this->random_engine->get_bytes(self::SALT_SIZE_IN_BYTES); |
||||
list ($enc_key, $auth_key) = $this->splitKeys($secret_key, sodium_bin2hex($salt)); |
||||
sodium_memzero($secret_key); |
||||
sodium_memzero($password); |
||||
$nonce = $this->random_engine->get_bytes( |
||||
SODIUM_CRYPTO_STREAM_NONCEBYTES |
||||
); |
||||
$cipher_text = sodium_crypto_stream_xor( |
||||
$hashed, |
||||
$nonce, |
||||
$enc_key |
||||
); |
||||
|
||||
$mac = sodium_crypto_auth($nonce . $cipher_text, $auth_key); |
||||
sodium_memzero($enc_key); |
||||
sodium_memzero($auth_key); |
||||
|
||||
return sodium_bin2hex($mac . $nonce . $salt . $cipher_text); |
||||
} |
||||
|
||||
/** |
||||
* Decrypt then verify a password |
||||
* |
||||
* @param string $password - The user-provided password |
||||
* @param string $stored - The encrypted password hash |
||||
* @param string $secret_key - The main_magic key for all passwords |
||||
*/ |
||||
public function verify(#[\SensitiveParameter] string $password, string $stored, #[\SensitiveParameter] string $secret_key): bool { |
||||
$data = sodium_hex2bin($stored); |
||||
$mac = mb_substr( |
||||
$data, |
||||
0, |
||||
SODIUM_CRYPTO_AUTH_BYTES, |
||||
'8bit' |
||||
); |
||||
$nonce = mb_substr( |
||||
$data, |
||||
SODIUM_CRYPTO_AUTH_BYTES, |
||||
SODIUM_CRYPTO_STREAM_NONCEBYTES, |
||||
'8bit' |
||||
); |
||||
$salt = mb_substr( |
||||
$data, |
||||
SODIUM_CRYPTO_AUTH_BYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES, |
||||
self::SALT_SIZE_IN_BYTES, |
||||
'8bit' |
||||
); |
||||
$cipher_text = mb_substr( |
||||
$data, |
||||
SODIUM_CRYPTO_AUTH_BYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES + self::SALT_SIZE_IN_BYTES, |
||||
null, |
||||
'8bit' |
||||
); |
||||
list ($enc_key, $auth_key) = $this->splitKeys($secret_key, sodium_bin2hex($salt)); |
||||
|
||||
if (sodium_crypto_auth_verify($mac, $nonce . $cipher_text, $auth_key)) { |
||||
sodium_memzero($auth_key); |
||||
$hash_str = sodium_crypto_stream_xor($cipher_text, $nonce, $enc_key); |
||||
sodium_memzero($enc_key); |
||||
if ($hash_str !== false) { |
||||
$ret = sodium_crypto_pwhash_scryptsalsa208sha256_str_verify($hash_str, $password); |
||||
sodium_memzero($password); |
||||
return $ret; |
||||
} |
||||
} else { |
||||
sodium_memzero($auth_key); |
||||
sodium_memzero($enc_key); |
||||
} |
||||
throw new \Exception('Decryption failed.'); |
||||
} |
||||
|
||||
/** |
||||
* @return array(2) [encryption key, authentication key] |
||||
*/ |
||||
private function splitKeys(#[\SensitiveParameter] string $secret_key, #[\SensitiveParameter] string $salt): array { |
||||
$enc_key = hash_hkdf('sha256', $secret_key, SODIUM_CRYPTO_STREAM_KEYBYTES, 'encryption', $salt); |
||||
$auth_key = hash_hkdf('sha256', $secret_key, SODIUM_CRYPTO_AUTH_KEYBYTES, 'authentication', $salt); |
||||
return [$enc_key, $auth_key]; |
||||
} |
||||
|
||||
} |
||||
|
||||
/* |
||||
$c = new ParagonCrypto\passwordStorage(); |
||||
$k = $c->generateKey(); |
||||
$h = $c->hash("HelpMe", $k); |
||||
var_dump( $c->verify("HelpMe", $h, $k) ); |
||||
*/ |
||||
@ -0,0 +1,135 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace IOcornerstone\Framework\ParagonCrypto; |
||||
|
||||
use IOcornerstone\Framework\RandomEngine; |
||||
|
||||
/** |
||||
* @license MIT/ISC |
||||
* @author by Paragon Initiative Enterprises |
||||
* @link https://github.com/paragonie/pecl-libsodium-doc/blob/master/chapters/09-recipes.md |
||||
* |
||||
* I've added HKDF extracts a pseudorandom key (PRK) using an HMAC hash function |
||||
* (e.g. HMAC-SHA256) on salt (acting as a key) to secure the input key |
||||
* |
||||
* |
||||
* Encrypted Storage |
||||
* Problem: We want to store data in a cookie such that user cannot read nor alter its contents. |
||||
* Desired Solution: Authenticated secret-key encryption, wherein the nonce is stored with the ciphertext. |
||||
* Each encryption and authentication key should be attached to the cookie name. |
||||
* This strategy combines both sodium_crypto_stream_xor() with sodium_crypto_auth(). |
||||
*/ |
||||
|
||||
class SodiumStorage { |
||||
private $randomEngine; |
||||
|
||||
const SALT_SIZE_IN_BYTES = 16; |
||||
|
||||
/** |
||||
* Sets the encryption key |
||||
*/ |
||||
public function __construct(#[\SensitiveParameter] private string $key) { |
||||
$this->randomEngine = new RandomEngine(); |
||||
} |
||||
|
||||
public function encrypt(#[\SensitiveParameter] string $plain_text, string $item_name = ""): string { |
||||
$nonce = $this->randomEngine->get_bytes( |
||||
SODIUM_CRYPTO_STREAM_NONCEBYTES |
||||
); |
||||
$salt = $this->randomEngine->get_bytes(self::SALT_SIZE_IN_BYTES); |
||||
list ($enc_key, $auth_key) = $this->splitKeys($item_name, sodium_bin2hex($salt)); |
||||
$cipher_text = sodium_crypto_stream_xor( |
||||
$plain_text, |
||||
$nonce, |
||||
$enc_key |
||||
); |
||||
sodium_memzero($plain_text); |
||||
|
||||
$mac = sodium_crypto_auth($nonce . $cipher_text, $auth_key); |
||||
|
||||
sodium_memzero($enc_key); |
||||
sodium_memzero($auth_key); |
||||
|
||||
return sodium_bin2hex($mac . $nonce . $salt . $cipher_text); |
||||
} |
||||
|
||||
public function decrypt(string $cypher_data, string $item_name = ""): string { |
||||
$bin_data = sodium_hex2bin($cypher_data); |
||||
|
||||
$mac = mb_substr( |
||||
$bin_data, |
||||
0, |
||||
SODIUM_CRYPTO_AUTH_BYTES, |
||||
'8bit' |
||||
); |
||||
$nonce = mb_substr( |
||||
$bin_data, |
||||
SODIUM_CRYPTO_AUTH_BYTES, |
||||
SODIUM_CRYPTO_STREAM_NONCEBYTES, |
||||
'8bit' |
||||
); |
||||
$salt = mb_substr( |
||||
$bin_data, |
||||
SODIUM_CRYPTO_AUTH_BYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES, |
||||
self::SALT_SIZE_IN_BYTES, |
||||
'8bit' |
||||
); |
||||
$cipher_text = mb_substr( |
||||
$bin_data, |
||||
SODIUM_CRYPTO_AUTH_BYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES + self::SALT_SIZE_IN_BYTES, |
||||
null, |
||||
'8bit' |
||||
); |
||||
|
||||
list ($enc_key, $auth_key) = $this->splitKeys($item_name, sodium_bin2hex($salt)); |
||||
|
||||
if (sodium_crypto_auth_verify($mac, $nonce . $cipher_text, $auth_key)) { |
||||
sodium_memzero($auth_key); |
||||
$plaintext = sodium_crypto_stream_xor($cipher_text, $nonce, $enc_key); |
||||
sodium_memzero($enc_key); |
||||
if ($plaintext !== false) { |
||||
return $plaintext; |
||||
} |
||||
} else { |
||||
sodium_memzero($auth_key); |
||||
sodium_memzero($enc_key); |
||||
} |
||||
throw new \Exception('Decryption failed.'); |
||||
} |
||||
|
||||
/** |
||||
* @return array(2) [encryption key, authentication key] |
||||
*/ |
||||
private function splitKeys(string $item_name, #[\SensitiveParameter] string $salt): array { |
||||
$enc_key = hash_hkdf('sha256', $this->key, SODIUM_CRYPTO_STREAM_KEYBYTES, md5('encryption' . $item_name), $salt); |
||||
$auth_key = hash_hkdf('sha256', $this->key, SODIUM_CRYPTO_AUTH_KEYBYTES, md5('authentication' . $item_name), $salt); |
||||
return [$enc_key, $auth_key]; |
||||
} |
||||
|
||||
} |
||||
|
||||
/* |
||||
* Example: |
||||
$secretkey = "78a5011b9997cd03a28a3412c66565b7c32715b35e055d7abfc228236308d3b2"; |
||||
$plain_text = "Hello World!"; |
||||
|
||||
$sc = new ParagonCrypto\SodiumStorage($secretkey); |
||||
$index = "sensitive"; |
||||
$encoded = $sc->encode($index, $plain_text); |
||||
setcookie($index, $encoded); |
||||
|
||||
// On the next page load: |
||||
try { |
||||
if (!array_key_exists($index, $_COOKIE)) { |
||||
throw new \Exception('No Cookie!'); |
||||
} |
||||
$data = $_COOKIE[$index]; |
||||
$plain_text = $sc->decode($index, $data); |
||||
echo $plain_text; |
||||
} catch (Exception $ex) { |
||||
// Handle the exception here |
||||
echo $ex->getMessage(); |
||||
} |
||||
*/ |
||||
@ -0,0 +1,239 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @author Robert Strutts |
||||
* @copyright Copyright (c) 2022, Robert Strutts. |
||||
* @license MIT |
||||
*/ |
||||
|
||||
namespace IOcornerstone\Framework\Services; |
||||
use IOcornerstone\Framework\Registry as Reg; |
||||
|
||||
class Emailer { |
||||
|
||||
/** |
||||
* Send an Email. |
||||
* to=>array(addresses, address, name) |
||||
* @param array $options (to, cc, bcc, from, subject, email, type['html', or 'text']) |
||||
* @return bool |
||||
*/ |
||||
function send_email(array $options): bool { |
||||
|
||||
if (!is_array($options)) { |
||||
return false; |
||||
} |
||||
|
||||
if (! Reg::get('loader')->is_loaded('PHPMailer\PHPMailer')) { |
||||
Reg::get('loader')->add_namespace('PHPMailer\PHPMailer', IO_CORNERSTONE_PROJECT . 'vendor/phpmailer/phpmailer/src'); |
||||
} |
||||
|
||||
$mail = new \PHPMailer\PHPMailer\PHPMailer(); |
||||
|
||||
$message = ''; |
||||
$subject = ''; |
||||
$to = ''; |
||||
$from = ''; |
||||
$from_name = ''; |
||||
$reply = ''; |
||||
$reply_name = ''; |
||||
$content_type = ''; |
||||
$cc = ''; |
||||
$cc_name = ''; |
||||
$bcc = ''; |
||||
$bcc_name = ''; |
||||
|
||||
foreach ($options as $key => $value) { |
||||
|
||||
$default = (isset($value[0])) ? $value[0] : null; |
||||
|
||||
switch (strtolower($key)) { |
||||
case 'to': |
||||
if (is_array($value)) { |
||||
$mto = (isset($value['addresses']) && is_array($value['addresses'])) ? $value['addresses'] : false; |
||||
$to = (isset($value['address'])) ? $value['address'] : $default; |
||||
$to_name = (isset($value['name'])) ? $value['name'] : false; |
||||
if ($mto !== false) { |
||||
foreach ($mto as $name => $address) { |
||||
$mail->addAddress($address, $name); |
||||
$to .= $address . "({$name});"; |
||||
} |
||||
} elseif ($to_name === false) { |
||||
$mail->addAddress($to); |
||||
} else { |
||||
$mail->addAddress($to, $to_name); |
||||
} |
||||
} else { |
||||
$to = $value; |
||||
$mail->addAddress($to); // Add a recipient |
||||
} |
||||
break; |
||||
case 'cc': |
||||
if (is_array($value)) { |
||||
$mcc = (isset($value['addresses']) && is_array($value['addresses'])) ? $value['addresses'] : false; |
||||
$cc = (isset($value['address'])) ? $value['address'] : $default; |
||||
$cc_name = (isset($value['name'])) ? $value['name'] : false; |
||||
if ($mcc !== false) { |
||||
foreach ($mcc as $name => $address) { |
||||
$mail->addCC($address, $name); |
||||
} |
||||
} elseif ($cc_name === false) { |
||||
$mail->addCC($cc); |
||||
} else { |
||||
$mail->addCC($cc, $cc_name); |
||||
} |
||||
} else { |
||||
$mail->addCC($value); |
||||
} |
||||
break; |
||||
case 'bcc': |
||||
if (is_array($value)) { |
||||
$mbcc = (isset($value['addresses']) && is_array($value['addresses'])) ? $value['addresses'] : false; |
||||
$bcc = (isset($value['address'])) ? $value['address'] : $default; |
||||
$bcc_name = (isset($value['name'])) ? $value['name'] : false; |
||||
if ($mbcc !== false) { |
||||
foreach ($mbcc as $name => $address) { |
||||
$mail->addBBC($address, $name); |
||||
} |
||||
} elseif ($bbc_name === false) { |
||||
$mail->addBCC($bcc); |
||||
} else { |
||||
$mail->addBCC($bcc, $bcc_name); |
||||
} |
||||
} else { |
||||
$mail->addBCC($value); |
||||
} |
||||
break; |
||||
case 'from': |
||||
if (is_array($value)) { |
||||
$from = (isset($value['address'])) ? $value['address'] : $default; |
||||
$from_name = (isset($value['name'])) ? $value['name'] : false; |
||||
if ($from_name === false) { |
||||
$mail->setFrom($from); |
||||
} else { |
||||
$mail->setFrom($from, $from_name); |
||||
} |
||||
} else { |
||||
$from = $value; |
||||
$mail->setFrom($from); |
||||
} |
||||
break; |
||||
case 'subject': |
||||
$subject = $value; |
||||
$mail->Subject = $subject; |
||||
break; |
||||
case 'email': |
||||
case 'message': |
||||
$message = $value; |
||||
$mail->Body = $message; |
||||
$mail->AltBody = strip_tags($message); |
||||
break; |
||||
case 'reply to': |
||||
case 'reply': |
||||
if (is_array($value)) { |
||||
$mreply = (isset($value['addresses']) && is_array($value['addresses'])) ? $value['addresses'] : false; |
||||
$reply = (isset($value['address'])) ? $value['address'] : $default; |
||||
$reply_name = (isset($value['name'])) ? $value['name'] : false; |
||||
if ($mreply !== false) { |
||||
foreach ($mreply as $name => $address) { |
||||
$mail->addReplyTo($address, $name); |
||||
} |
||||
} elseif ($reply_name === false) { |
||||
$mail->addReplyTo($reply); |
||||
} else { |
||||
$mail->addReplyTo($reply, $reply_name); |
||||
} |
||||
} else { |
||||
$mail->addReplyTo($value); |
||||
} |
||||
break; |
||||
case 'bounce': |
||||
case 'bounce address': |
||||
case 'bounce backs': |
||||
$bounce = $value; |
||||
break; |
||||
case 'attachment': |
||||
case 'attach': |
||||
if (is_array($value)) { |
||||
$attachment = (isset($value['file'])) ? $value['file'] : $default; |
||||
$file_name = (isset($value['name'])) ? $value['name'] : false; |
||||
if ($file_name === false) { |
||||
$mail->addAttachment($attachment); |
||||
} else { |
||||
$mail->addAttachment($attachment, $file_name); |
||||
} |
||||
} else { |
||||
$mail->addAttachment($value); |
||||
} |
||||
break; |
||||
case 'type': |
||||
case 'content type': |
||||
$content_type = $value; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (empty($to) || empty($from) || empty($subject) || empty($message)) { |
||||
return false; |
||||
} |
||||
|
||||
switch ($content_type) { |
||||
case 'html': |
||||
$mail->isHTML(true); // Set email format to HTML |
||||
break; |
||||
case 'text': |
||||
default: |
||||
$mail->isHTML(false); // Set email format to Plain Text |
||||
break; |
||||
} |
||||
|
||||
$settings = Reg::get('email'); |
||||
|
||||
if (isset($settings['send_emails']) && $settings['send_emails'] === false) { |
||||
try { |
||||
$log = Regy::get('di')->get_service('log', ['emails']); |
||||
$log->write("To: {$to} Subject: {$subject} Message: {$message}"); |
||||
} catch (Exception $e) { |
||||
|
||||
} |
||||
return false; |
||||
} |
||||
|
||||
if (isset($settings['host']) && !empty($settings['host'])) { |
||||
if (isset($settings['smtp_debug'])) { |
||||
$mail->SMTPDebug = $settings['smtp_debug']; // Enable verbose debug output |
||||
} |
||||
|
||||
$mail->isSMTP(); // Set mailer to use SMTP |
||||
$mail->Host = $settings['host']; // Specify main and backup SMTP servers |
||||
$auth = ( isset($settings['username']) && !empty($settings['username']) && |
||||
isset($settings['password']) && !empty($settings['password']) && |
||||
(!isset($settings['auth']) || $settings['auth'] === true) |
||||
) ? true : false; // Enable SMTP authentication |
||||
if ($auth === true) { |
||||
$mail->SMTPAuth = true; |
||||
$mail->Username = $settings['username']; // SMTP username |
||||
$mail->Password = $settings['password']; // SMTP password |
||||
$mail->SMTPSecure = (isset($settings['secure'])) ? $settings['secure'] : 'tls'; // Enable TLS encryption, `ssl` also accepted |
||||
} |
||||
$mail->Port = (isset($settings['port'])) ? $settings['port'] : 587; // TCP port to connect to |
||||
} else { |
||||
$mail->isMail(); // Use SendMail |
||||
} |
||||
|
||||
if ($mail->send()) { |
||||
return true; // Yes, it worked! |
||||
} else { |
||||
try { |
||||
$log = Reg::get('di')->get_service('log', ['failed_emails']); |
||||
$log->write('Mailer Error: ' . $mail->ErrorInfo); |
||||
} catch (Exception $e) { |
||||
|
||||
} |
||||
error_log('Mailer Error: ' . $mail->ErrorInfo); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,358 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Robert@TryingToScale.com> |
||||
* @copyright Copyright (c) 2022, Robert Strutts. |
||||
* @license MIT |
||||
* |
||||
* This file is for non-sensitive data like session data |
||||
*/ |
||||
|
||||
namespace IOcornetstone\Framework\Services; |
||||
|
||||
use IOcornerstone\Framework\Common; |
||||
use IOcornerstone\Framework\RandomEngine; |
||||
|
||||
/* |
||||
* var_dump($enc->list_ssl_methods()); |
||||
* var_dump($enc->list_hashes()); |
||||
*/ |
||||
|
||||
final class Encryption { |
||||
const iterations = 81952; // should be over 80K. The number of internal iterations to perform for the derivation. |
||||
const length = 64; // The length of the output string. If raw_output is TRUE this corresponds to the byte-length of the derived key, if raw_output is FALSE this corresponds to twice the byte-length of the derived key (as every byte of the key is returned as two hexits). |
||||
const raw = true; // When set to TRUE, outputs raw binary data. FALSE outputs lowercase hexits. |
||||
const keyBytes = 32; // 32 bytes = 256 bits, 64 bytes = 512 bits, 16 bytes = 128 bits encryption key |
||||
|
||||
private $randomEngine; |
||||
private $binary = false; |
||||
private $urlEncode = false; |
||||
private $method = 'AES-256-CBC'; |
||||
private $defaultHash = 'sha256'; // should be sha256 or higher |
||||
private $_iterations = self::iterations, $_length=self::length, $_raw=self::raw, $_key_bytes=self::keyBytes; |
||||
|
||||
public function __construct(#[\SensitiveParameter] private string $key) { |
||||
$this->randomEngine = new RandomEngine(); |
||||
} |
||||
|
||||
/* |
||||
* xxHash is an extremely fast hashing algorithm |
||||
* that is not designed for cryptographic purposes, but |
||||
* provides excellent randomness and dispersion of output, |
||||
* and uniqueness of to minimize collisions. |
||||
* |
||||
Algorithm PHP implementation speed (GB/s) |
||||
xxh3 15.19 |
||||
xxh128 14.78 |
||||
crc32c 14.12 |
||||
xxh64 13.32 |
||||
murmur3f 8.87 |
||||
xxh32 7.47 |
||||
sha2-256 0.25 |
||||
sha1-160 0.70 |
||||
md5-128 0.77 |
||||
*/ |
||||
public function weakQuickHash(string $data, int $speed): string { |
||||
$version = (float) phpversion(); |
||||
if ($version >= 8.1) { |
||||
return match($speed) { |
||||
1 => hash('xxh3', $data), // Fastest |
||||
2 => hash('xxh128', $data), |
||||
3 => hash('crc32c', $data), |
||||
4 => hash('xxh64', $data), |
||||
5 => hash('murmur3f', $data), |
||||
6 => hash('xxh32', $data), |
||||
7 => hash('sha256', $data), // Slowest |
||||
8 => hash('sha1', $data), |
||||
9 => hash('md5', $data), |
||||
default => crc32($data) |
||||
}; |
||||
} else { |
||||
return crc32($data); |
||||
} |
||||
} |
||||
|
||||
|
||||
public function changeSecurityLevel(string $level): bool { |
||||
switch (strtolower($level)) { |
||||
/** |
||||
* About blowfish -> |
||||
* Designers: Bruce Schneier |
||||
* First published: 1993 |
||||
* Successors: Twofish |
||||
* Key sizes: 32–448 bits |
||||
* Block sizes: 64 bits |
||||
* Structure: Feistel network |
||||
* Rounds: 16 |
||||
*/ |
||||
|
||||
/* |
||||
* In cryptography, Twofish is a symmetric key block cipher |
||||
* with a block size of 128 bits and key sizes up to 256 bits. |
||||
* Twofish was slightly slower than Rijndael for 128-bit keys, |
||||
* but somewhat faster for 256-bit keys. |
||||
*/ |
||||
|
||||
/* |
||||
* AES is the main block cipher in use today, standardized by NIST. Camellia is a Japanese standardized cipher. ChaCha is a fast stream cipher specified by Bernstein and incorporated into TLS with support from Google. |
||||
* Serpent and Twofish were AES last round candidates that didn't make it. Serpent is not that fast, and Twofish is relatively fast but not compared to AES when hardware acceleration is used. Both are block ciphers that are really not needed as long as we deem AES to be secure. |
||||
* Threefish was mainly designed for the Skein hash function. This tweakable block cipher is not used much. It Skein had been chosen as SHA-3 then it would have stood a better chance. For now an authenticated form of Keccak would make more sense. |
||||
* So yeah, two standardized ciphers and a fast stream ciphers are supported, none of which are broken. There is no reason to include the also-ran's and a cipher made specifically for a hash function which was also not standardized. |
||||
*/ |
||||
|
||||
/** |
||||
* lighting, blaze, and quick are not for sensitive data, |
||||
* it is good for making sure data was not tampered with |
||||
* like encrypted sessions. Where as, good, normal, |
||||
* and paranoid are for cookies, DB storage, etc... |
||||
*/ |
||||
case 'lighting': // 0.0006 Seconds |
||||
$this->setLoops(98); // Very very fast |
||||
$this->setLength(64); |
||||
$this->setKeyBytes(16); |
||||
$this->method = 'AES-128-CBC'; |
||||
$this->defaultHash = 'sha256'; |
||||
return true; |
||||
case 'blaze': // 0.0109 Seconds |
||||
$this->setLoops(1843); |
||||
$this->setLength(64); |
||||
$this->setKeyBytes(32); |
||||
$this->method = 'AES-128-CBC'; |
||||
$this->defaultHash = 'sha256'; |
||||
return true; |
||||
case 'quick': // 0.0167 Seconds |
||||
$this->setLoops(2843); |
||||
$this->setLength(64); |
||||
$this->setKeyBytes(32); |
||||
$this->method = 'AES-192-CBC'; |
||||
$this->defaultHash = 'sha256'; |
||||
return true; |
||||
case 'good': // 0.0732 Seconds |
||||
$this->setLoops(11952); |
||||
$this->setLength(64); |
||||
$this->setKeyBytes(32); // 32B or 256b, standard key size |
||||
$this->method = 'AES-256-CBC'; |
||||
$this->defaultHash = 'sha256'; |
||||
return true; |
||||
case 'normal': // 0.4901 Seconds |
||||
$this->setLoops(81952); // slow |
||||
$this->setLength(64); |
||||
$this->setKeyBytes(32); |
||||
$this->method = 'AES-256-CBC'; |
||||
$this->defaultHash = 'sha256'; |
||||
return true; |
||||
case 'paranoid': // 0.6167 Seconds |
||||
case 'max': |
||||
case 'high': |
||||
case 'slow-secure': // Very slow |
||||
$this->setLoops(82952); |
||||
$this->setLength(128); // extended length to make it work |
||||
$this->setKeyBytes(64); // extended key size for 512 hash |
||||
$this->method = 'AES-256-CBC'; |
||||
$this->defaultHash = 'sha512'; |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
public function setLoops(int $iterations=self::iterations): void { |
||||
$this->_iterations = $iterations; |
||||
} |
||||
|
||||
public function setLength(int $length=self::length): void { |
||||
$this->_length = $length; |
||||
} |
||||
|
||||
public function setRawOutput(bool $output_raw=self::raw): void { |
||||
$this->_raw = $output_raw; |
||||
} |
||||
|
||||
public function setKeyBytes(int $key_bytes=self::keyBytes): void { |
||||
$this->_key_bytes = $key_bytes; |
||||
} |
||||
|
||||
public function listSslMethods(): array { |
||||
$ciphers = openssl_get_cipher_methods(); |
||||
|
||||
//ECB mode should be avoided |
||||
$ciphers = array_filter( $ciphers, function($n) { return stripos($n,"ecb")===FALSE; } ); |
||||
|
||||
//At least as early as Aug 2016, Openssl declared the following weak: RC2, RC4, DES, 3DES, MD5 based |
||||
$ciphers = array_filter( $ciphers, function($c) { return stripos($c,"des")===FALSE; } ); |
||||
$ciphers = array_filter( $ciphers, function($c) { return stripos($c,"rc2")===FALSE; } ); |
||||
$ciphers = array_filter( $ciphers, function($c) { return stripos($c,"rc4")===FALSE; } ); |
||||
$ciphers = array_filter( $ciphers, function($c) { return stripos($c,"md5")===FALSE; } ); |
||||
|
||||
$ciphers = array_map('strtoupper', $ciphers); |
||||
return array_unique($ciphers); |
||||
} |
||||
|
||||
/** |
||||
* Change OpenSSl Security Level |
||||
* @param string $level (low, medium, medium-high, high) or algorithm Method name |
||||
*/ |
||||
public function changeOpenSsl(string $level): bool { |
||||
switch (strtolower($level)) { |
||||
case 'low': |
||||
$this->method = 'AES-128-CBC'; |
||||
return true; |
||||
case 'medium': |
||||
$this->method = 'AES-192-CBC'; |
||||
return true; |
||||
case 'high': |
||||
$this->method = 'AES-256-CBC'; |
||||
return true; |
||||
} // end of switch |
||||
|
||||
if (in_array(strtoupper($level), $this->listSslMethods() )) { |
||||
$this->method = strtoupper($level); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* SHA-224, with 224 bit hash values |
||||
* SHA-256, with 256 bit hash values |
||||
* SHA-384, with 384 bit hash values |
||||
* SHA-512, with 512 bit hash values |
||||
* SHA-512/224, with 512 bit hash values |
||||
* SHA-512/256, with 512 bit hash values |
||||
* Among these, SHA-256 and SHA-512 are the most commonly |
||||
* accepted and used hash functions computed with 32-bit |
||||
* and 64-bit words, respectively. SHA-224 and SHA-384 are |
||||
* truncated versions of SHA-256 and SHA-512 respectively, |
||||
* computed with different initial values. |
||||
*/ |
||||
public function listHashes(): array { |
||||
$hash = hash_algos(); |
||||
// Filter out weak Hash FNs |
||||
$hash = array_filter( $hash, function($n) { return stripos($n,"crc")===FALSE; } ); |
||||
$hash = array_filter( $hash, function($n) { return stripos($n,"md")===FALSE; } ); |
||||
$hash = array_filter( $hash, function($n) { return stripos($n,"sha1")===FALSE; } ); |
||||
return $hash; |
||||
} |
||||
|
||||
public function changeHash(string $hash): bool { |
||||
if (in_array(strtolower($hash), $this->listHashes() )) { |
||||
$this->defaultHash = strtolower($hash); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
public function setBinaryOutput(bool $bin) { |
||||
$this->binary = $bin; |
||||
} |
||||
|
||||
public function setUrlEncode(bool $url_encode) { |
||||
$this->urlEncode = $url_encode; |
||||
} |
||||
|
||||
/** |
||||
* OpenSSL Encrypt text with key |
||||
* @param string $text data |
||||
* @return string encoded text |
||||
*/ |
||||
public function encrypt(#[\SensitiveParameter] string $text, bool $validate = true): string { |
||||
$key = $this->key; |
||||
$key = ($validate) ? $this->getValidKey($key) : $key; |
||||
$ivsize = openssl_cipher_iv_length($this->method); |
||||
$iv = $this->randomEngine->get_bytes($ivsize); // Requires PHP 7 |
||||
// Encryption key generated by PBKDF2 (since PHP 5.5) |
||||
$keys = hash_pbkdf2($this->defaultHash, $key, $iv, $this->_iterations, $this->_length, $this->_raw); |
||||
$encKey = substr($keys, 0, $this->_key_bytes); // X bit encryption key |
||||
$hmacKey = substr($keys, $this->_key_bytes); // X bit hmac key |
||||
$ciphertext = openssl_encrypt( |
||||
$text, |
||||
$this->method, |
||||
$encKey, |
||||
OPENSSL_RAW_DATA, |
||||
$iv |
||||
); |
||||
Common::wipe($text); |
||||
Common::wipe($key); |
||||
Common::wipe($encKey); |
||||
$hmac = hash_hmac($this->defaultHash, $iv . $ciphertext, $hmacKey); |
||||
Common::wipe($hmacKey); |
||||
if (! $this->binary) { |
||||
return (! $this->urlEncode) ? base64_encode($hmac . $iv . $ciphertext) : Common::base64urlEncode($hmac . $iv . $ciphertext); |
||||
} else { |
||||
return $hmac . $iv . $ciphertext; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* OpenSSL Decrypt data with key |
||||
* @param string $data encoded text |
||||
* @return string plain text |
||||
*/ |
||||
public function decrypt(string $data, bool $validate = true): false|string { |
||||
$key = $this->key; |
||||
$key = ($validate) ? $this->getValidKey($key) : $key; |
||||
if (! $this->binary) { |
||||
$text = (! $this->urlEncode) ? base64_decode($data) : Common::base64urlDecode($data); |
||||
} else { |
||||
$text = $data; |
||||
} |
||||
$hmac = substr($text, 0, $this->_length); |
||||
$ivsize = openssl_cipher_iv_length($this->method); |
||||
$iv = (! $this->binary) ? substr($text, $this->_length, $ivsize) : $ivsize; |
||||
$ciphertext = substr($text, $ivsize + $this->_length); |
||||
// Generate the encryption and hmac keys |
||||
$keys = hash_pbkdf2($this->defaultHash, $key, $iv, $this->_iterations, $this->_length, $this->_raw); |
||||
$encKey = substr($keys, 0, $this->_key_bytes); // X bit encryption key |
||||
$hmacNew = hash_hmac($this->defaultHash, $iv . $ciphertext, substr($keys, $this->_key_bytes)); |
||||
if (! hash_equals($hmac, $hmacNew)) { // to prevent timing attacks/Verify MSG Auth |
||||
return false; // Note: hash_equals() requires PHP5.6+ |
||||
} |
||||
$ret = openssl_decrypt( |
||||
$ciphertext, |
||||
$this->method, |
||||
$encKey, |
||||
OPENSSL_RAW_DATA, |
||||
$iv |
||||
); |
||||
Common::wipe($ciphertext); |
||||
Common::wipe($key); |
||||
Common::wipe($encKey); |
||||
Common::wipe($keys); |
||||
return $ret; |
||||
} |
||||
|
||||
/** |
||||
* Try to make sure you have a valid key |
||||
* @param string $key as input |
||||
* @return string new key |
||||
* @throws Exception |
||||
*/ |
||||
public function getValidKey(#[\SensitiveParameter] string $key): string { |
||||
$ivsize = openssl_cipher_iv_length($this->method); |
||||
$key = substr($key, 0, $ivsize * 2); |
||||
$keysize = strlen($key); |
||||
|
||||
if ($keysize != $ivsize * 2) { |
||||
throw new \Exception('Unable to use ENC key: Bad key size!'); |
||||
} |
||||
|
||||
if (! ctype_xdigit($key)) { |
||||
throw new \Exception('Unable to use ENC key: None HEX Digits!'); |
||||
} |
||||
|
||||
$ret = bin2hex(pack('H*', $key)); |
||||
Common::wipe($key); |
||||
return $ret; |
||||
} |
||||
|
||||
/** |
||||
* Make a new Secure key |
||||
* @return string key |
||||
*/ |
||||
public function generateValidKey(): string { |
||||
$ivsize = openssl_cipher_iv_length($this->method); |
||||
return bin2hex($this->randomEngine->get_bytes($ivsize)); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,73 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace IOcornerstone\Framework\Services; |
||||
|
||||
use IOcornerstone\Framework\Registry as Reg; |
||||
|
||||
if (!defined('HTMLPURIFIER_PREFIX')) { |
||||
define('HTMLPURIFIER_PREFIX', IO_CORNERSTONE_PROJECT . 'vendor/ezyang/htmlpurifier/library'); |
||||
} |
||||
|
||||
final class HtmlFilter { |
||||
private $engine = false; |
||||
private $config = null; |
||||
|
||||
public function __construct(private $enable_file_caching = false) { |
||||
if (Reg::get('html_filter') === null) { |
||||
spl_autoload_register(function ($class) { |
||||
// HTMLPurifier uses underscores to indicate subfolders |
||||
$prefix = 'HTMLPurifier'; |
||||
|
||||
if (strpos($class, $prefix) === 0) { |
||||
$path = HTMLPURIFIER_PREFIX . "/" . str_replace('_', '/', $class) . '.php'; |
||||
if (file_exists($path)) { |
||||
require_once $path; |
||||
} |
||||
} |
||||
}); |
||||
Reg::set('html_filter', true); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Keep this as its used by safer_io.php in the p() method |
||||
* @return bool |
||||
*/ |
||||
public function hasLoaded(): bool { |
||||
return ($this->engine === false) ? false : true; |
||||
} |
||||
|
||||
public function setDefaults(): void { |
||||
$cache_dir = BaseDir . "/protected/runtime/html_purifier_filter_cache"; |
||||
|
||||
$this->config = \HTMLPurifier_Config::createDefault(); |
||||
$this->config->set('Core.Encoding', 'UTF-8'); |
||||
|
||||
if ($this->enable_file_caching) { |
||||
// Set the path for the serialized cache files |
||||
$this->config->set('Cache.SerializerPath', $cache_dir); |
||||
|
||||
// Optionally create the directory if it doesn't exist |
||||
if (!file_exists($cache_dir)) { |
||||
mkdir($cache_dir, 0775, true); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public function setConfig(string $key, $value, $a = null): void { |
||||
$this->config->set($key, $value, $a); |
||||
} |
||||
|
||||
public function init(): void { |
||||
$this->engine = new \HTMLPurifier($this->config); |
||||
} |
||||
|
||||
public function purify(string $html): string { |
||||
if ($this->engine === false) { |
||||
$this->init(); |
||||
} |
||||
return $this->engine->purify($html); |
||||
} |
||||
} |
||||
@ -0,0 +1,81 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
|
||||
namespace IOcornerstone\Framework\Services; |
||||
|
||||
use IOcornerstone\Framework\{ |
||||
Registry as Reg, |
||||
Security, |
||||
PhpFileCache as FileCache |
||||
}; |
||||
|
||||
use Liquid\{Liquid, Template, Context}; |
||||
use Liquid\Cache\Local; |
||||
|
||||
final class LiquidTemplates { |
||||
private $liquid; |
||||
private $dir; |
||||
private $extension = 'tpl'; |
||||
|
||||
public function __construct(private $use_local_cache = false, string $template_extension = 'tpl') { |
||||
$this->extension = $template_extension; |
||||
if (! Reg::get('loader')->is_loaded('Liquid')) { |
||||
Reg::get('loader')->add_namespace('Liquid', IO_CORNERSTONE_PROJECT . 'vendor/liquid/liquid/src/Liquid'); |
||||
} |
||||
|
||||
Liquid::set('INCLUDE_SUFFIX', $template_extension); |
||||
Liquid::set('INCLUDE_PREFIX', ''); |
||||
|
||||
$this->dir = IO_CORNERSTONE_PROJECT . 'views/liquid'; |
||||
$this->liquid = new Template($this->dir); |
||||
} |
||||
|
||||
public function whitespaceControl() { |
||||
/* Force whitespace control to be used by switching tags from {%- to {% |
||||
* Also, will error out if you try {%- |
||||
* Need to figure out how to turn back on whitespaces with {%@ errors! not important... |
||||
*/ |
||||
Liquid::set('TAG_START', '(?:{%@)|\s*{%'); |
||||
Liquid::set('TAG_END', '(?:@%}|%}\s*)'); |
||||
} |
||||
|
||||
public function parse(string $source) { |
||||
return $this->liquid->parse($source); |
||||
} |
||||
|
||||
public function parseFile(string $file) { |
||||
$safe_file = Security::filterUri($file); |
||||
if ($this->use_local_cache) { |
||||
$cache = new FileCache(BaseDir . "/protected/runtime/liquid_cache"); |
||||
|
||||
$templateName = $safe_file . "." . $this->extension; |
||||
$templatePath = $this->dir . "/". $safe_file . "." . $this->extension; |
||||
|
||||
$templateSource = file_get_contents($templatePath); |
||||
$cached = $cache->get($templateName); |
||||
if (!$cached) { |
||||
$this->liquid->parseFile($safe_file); |
||||
$cache->set($templateName, $this->liquid); |
||||
} else { |
||||
$this->liquid = $cached; |
||||
} |
||||
} else { |
||||
$this->liquid->parseFile($safe_file); |
||||
} |
||||
} |
||||
|
||||
public function render(array $assigns = array(), $filters = null, array $registers = array()): string { |
||||
return $this->liquid->render($assigns, $filters, $registers); |
||||
} |
||||
|
||||
public function get_engine() { |
||||
return $this->liquid; |
||||
} |
||||
} |
||||
@ -0,0 +1,141 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace IOcornerstone\Framework\Services; |
||||
|
||||
use IOcornerstone\Framework\GzCompression; |
||||
use IOcornerstone\Framework\Enum\CompressionMethod as Method; |
||||
|
||||
/** |
||||
* @author Robert Strutts |
||||
* @copyright Copyright (c) 2022, Robert Strutts. |
||||
* @license MIT |
||||
*/ |
||||
|
||||
class CookieSessionsHandlerException extends \Exception {} |
||||
|
||||
class CookieSessionHandler implements \SessionHandlerInterface { |
||||
public static string $cookieDomain; |
||||
public static string $cookieName = 'SES'; |
||||
public static string $cookiePath = '/'; |
||||
public static bool $cookieSecure = true; |
||||
public static bool $cookieHTTPOnly = true; |
||||
private $enc; |
||||
|
||||
public function __construct($enc, array $options) { |
||||
if (isset($options['cookie_domain'])) { |
||||
self::$cookieDomain = $options['cookie_domain']; |
||||
} else { |
||||
self::$cookieDomain = $_SERVER['SERVER_NAME'] ?? ''; |
||||
} |
||||
if (isset($options['cookie_name'])) { |
||||
self::$cookieName = $options['cookie_name']; |
||||
} |
||||
if (isset($options['cookie_path'])) { |
||||
self::$cookiePath = $options['cookie_path']; |
||||
} |
||||
if (isset($options['cookie_secure'])) { |
||||
self::$cookieSecure = $options['cookie_secure']; |
||||
} else { |
||||
/** |
||||
* @todo Fix this...use_secure |
||||
*/ |
||||
$use_secure = true; |
||||
if ($use_secure === false) { |
||||
self::$cookieSecure = false; |
||||
} |
||||
} |
||||
if (isset($options['cookie_HTTP_only'])) { |
||||
self::$cookieHTTPOnly = $options['cookie_HTTP_only']; |
||||
} |
||||
if (isset($options['use_compression'])) { |
||||
self::$use_compression = $options['use_compression']; |
||||
} |
||||
if (isset($options['method'])) { |
||||
$method = $options['method']; |
||||
} else { |
||||
$method = Method::DEFLATE; |
||||
} |
||||
if (isset($options['level'])) { |
||||
$level = $options['level']; |
||||
} else { |
||||
$level = 4; |
||||
} |
||||
if (isset($options['enabled'])) { |
||||
$enabled = $options['enabled']; |
||||
} else { |
||||
$enabled = true; |
||||
} |
||||
|
||||
$this->compression = new GzCompression($method, $level, $enabled); |
||||
|
||||
$this->enc = $enc; |
||||
} |
||||
|
||||
private function writeHelper($data): string { |
||||
$gc = $this->compression->compress($data); |
||||
if ($gc !== false) { |
||||
$data = $gc; // data is now compressed |
||||
} |
||||
if ($this->enc === false) { |
||||
return $data; // encryption is off |
||||
} |
||||
$e = $this->enc->encrypt($data); |
||||
return ($e !== false) ? $e : $data; // and encrypted |
||||
} |
||||
|
||||
private function readHelper($data): string { |
||||
if ($data === null || $data === false) { |
||||
return ""; |
||||
} |
||||
if ($this->enc !== false) { |
||||
$de = $this->enc->decrypt($data); |
||||
if ($de!== false) { |
||||
$data = $de; // data is now decrypted |
||||
} |
||||
} |
||||
$gd = $this->compression->decompress($data); |
||||
return ($gd !== false) ? $gd : $data; |
||||
} |
||||
|
||||
public function open($save_path, $session_name):bool { |
||||
return true; |
||||
} |
||||
|
||||
public function close(): bool { |
||||
return true; |
||||
} |
||||
|
||||
public function read($id): false|string { |
||||
$data = isset($_COOKIE[self::$cookieName]) ? $_COOKIE[self::$cookieName] : null; |
||||
return $this->readHelper($data) ?: ""; |
||||
} |
||||
|
||||
public function write($id, $data): bool { |
||||
if (headers_sent($file, $line)) { |
||||
throw new CookieSessionsHandlerException("Error headers were already sent by $file on line # $line "); |
||||
} |
||||
$data = $this->writeHelper($data); |
||||
/* |
||||
* Google Chrome - 4096 bytes confirming to RFC. |
||||
* If the data is over 4000 bytes, throw an exception |
||||
* as web browsers only support up to 4K of Cookie Data! |
||||
*/ |
||||
if (strlen($data) > 4000) { |
||||
throw new cookie_sessions_handler_exception("Session data too big (over 4KB)"); |
||||
} |
||||
$cookie_lifetime = (int) ini_get('session.cookie_lifetime'); |
||||
|
||||
return setcookie(self::$cookieName, $data, $cookie_lifetime, self::$cookiePath, self::$cookieDomain, self::$cookieSecure, self::$cookieHTTPOnly); |
||||
} |
||||
|
||||
public function destroy($id): bool { |
||||
$expiration_time = time() - 3600; |
||||
return setcookie(self::$cookieName, "", $expiration_time, self::$cookiePath, self::$cookieDomain, self::$cookieSecure, self::$cookieHTTPOnly); |
||||
} |
||||
|
||||
public function gc($max_lifetime): int|false { |
||||
return 0; |
||||
} |
||||
} |
||||
@ -0,0 +1,184 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
|
||||
namespace IOcornerstone\Framework\Services; |
||||
|
||||
use IOcornerstone\Framework\GzCompression; |
||||
use IOcornerstone\Framework\Enum\CompressionMethod as Method; |
||||
|
||||
class FileSessionHandler implements SessionHandlerInterface |
||||
{ |
||||
private $savePath; |
||||
private $filePrefix; |
||||
private $compression; |
||||
private $enc; |
||||
|
||||
public function __construct(false|callable $enc, array $config = []) |
||||
{ |
||||
|
||||
$savePath = $config['save_path'] ?: null; |
||||
$filePrefix = $config['prefix'] ?: 'sess_'; |
||||
|
||||
$this->filePrefix = $filePrefix; |
||||
|
||||
// Use the system's default save path if none provided |
||||
$this->savePath = $savePath ?: $this->getDefaultSavePath(); |
||||
|
||||
// Create directory if it doesn't exist |
||||
if (!is_dir($this->savePath)) { |
||||
mkdir($this->savePath, 0700, true); |
||||
} |
||||
|
||||
$method = $config['method'] ?: Method::DEFLATE; |
||||
$level = $config['level'] ?: 4; |
||||
$enabled = $config['enabled'] ?: true; |
||||
|
||||
$this->compression = new GzCompression($method, $level, $enabled); |
||||
|
||||
$this->enc = $enc; |
||||
} |
||||
|
||||
private function writeHelper($data): string { |
||||
$gc = $this->compression->compress($data); |
||||
if ($gc !== false) { |
||||
$data = $gc; // data is now compressed |
||||
} |
||||
if ($this->enc === false) { |
||||
return $data; // encryption is off |
||||
} |
||||
$e = $this->enc->encrypt($data); |
||||
return ($e !== false) ? $e : $data; // and encrypted |
||||
} |
||||
|
||||
private function readHelper($data): string { |
||||
if ($data === null || $data === false) { |
||||
return ""; |
||||
} |
||||
if ($this->enc !== false) { |
||||
$de = $this->enc->decrypt($data); |
||||
if ($de!== false) { |
||||
$data = $de; // data is now decrypted |
||||
} |
||||
} |
||||
$gd = $this->compression->decompress($data); |
||||
return ($gd !== false) ? $gd : $data; |
||||
} |
||||
|
||||
/** |
||||
* Get default session save path from php.ini |
||||
* @return string |
||||
*/ |
||||
private function getDefaultSavePath() |
||||
{ |
||||
return rtrim(ini_get('session.save_path'), '/\\'); |
||||
} |
||||
|
||||
/** |
||||
* Open session |
||||
* @param string $savePath |
||||
* @param string $sessionName |
||||
* @return bool |
||||
*/ |
||||
public function open($savePath, $sessionName): bool |
||||
{ |
||||
// We already set savePath in constructor |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Close session |
||||
* @return bool |
||||
*/ |
||||
public function close(): bool |
||||
{ |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Read session data |
||||
* @param string $sessionId |
||||
* @return string |
||||
*/ |
||||
public function read($sessionId): string |
||||
{ |
||||
$file = $this->savePath . '/' . $this->filePrefix . $sessionId; |
||||
|
||||
// Lock the file while reading to prevent corruption |
||||
if (file_exists($file) && is_readable($file)) { |
||||
if ($handle = fopen($file, 'rb')) { |
||||
flock($handle, LOCK_SH); |
||||
$data = fread($handle, filesize($file)); |
||||
flock($handle, LOCK_UN); |
||||
fclose($handle); |
||||
return $this->readHelper($data); |
||||
} |
||||
} |
||||
|
||||
return ''; |
||||
} |
||||
|
||||
/** |
||||
* Write session data |
||||
* @param string $sessionId |
||||
* @param string $data |
||||
* @return bool |
||||
*/ |
||||
public function write($sessionId, $data): bool |
||||
{ |
||||
$data = $this->writeHelper($data); |
||||
$file = $this->savePath . '/' . $this->filePrefix . $sessionId; |
||||
|
||||
// Use exclusive lock while writing |
||||
if ($handle = fopen($file, 'cb')) { |
||||
flock($handle, LOCK_EX); |
||||
ftruncate($handle, 0); |
||||
fwrite($handle, $data); |
||||
fflush($handle); |
||||
flock($handle, LOCK_UN); |
||||
fclose($handle); |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Destroy session |
||||
* @param string $sessionId |
||||
* @return bool |
||||
*/ |
||||
public function destroy($sessionId): bool |
||||
{ |
||||
$file = $this->savePath . '/' . $this->filePrefix . $sessionId; |
||||
|
||||
if (file_exists($file)) { |
||||
unlink($file); |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Garbage collection |
||||
* @param int $maxLifetime |
||||
* @return bool |
||||
*/ |
||||
public function gc($maxLifetime): bool |
||||
{ |
||||
foreach (glob($this->savePath . '/' . $this->filePrefix . '*') as $file) { |
||||
if (file_exists($file) && filemtime($file) < time() - $maxLifetime) { |
||||
unlink($file); |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,178 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
|
||||
namespace IOcornerstone\Framework\Services; |
||||
|
||||
use IOcornerstone\Framework\GzCompression; |
||||
use IOcornerstone\Framework\Enum\CompressionMethod as Method; |
||||
|
||||
class RedisSessionHandler implements SessionHandlerInterface |
||||
{ |
||||
private $redis; |
||||
private $prefix; |
||||
private $ttl; |
||||
private $lockTimeout = 10; |
||||
private $lockRetries = 5; |
||||
private $lockWait = 100000; // microseconds |
||||
private $enc; |
||||
|
||||
public function __construct(callable $enc, array $config = []) |
||||
{ |
||||
$defaults = [ |
||||
'host' => '127.0.0.1', |
||||
'port' => 6379, |
||||
'prefix' => 'PHPREDIS_SESSION:', |
||||
'ttl' => null, // null means use session.gc_maxlifetime |
||||
'auth' => null, |
||||
'persistent' => true, |
||||
'database' => 1, |
||||
'timeout' => 0, |
||||
'read_timeout' => 2, |
||||
'retry_interval' => 100 |
||||
]; |
||||
|
||||
$config = array_merge($defaults, $config); |
||||
|
||||
$this->prefix = $config['prefix']; |
||||
|
||||
// Use configured TTL if provided, otherwise use session.gc_maxlifetime |
||||
$this->ttl = $config['ttl'] ?? (int)ini_get('session.gc_maxlifetime'); |
||||
|
||||
// Fallback to 1440 seconds if neither is set |
||||
if ($this->ttl <= 0) { |
||||
$this->ttl = 1440; |
||||
} |
||||
|
||||
$this->redis = new Redis(); |
||||
|
||||
if ($config['persistent']) { |
||||
$this->redis->pconnect($config['host'], $config['port'], $config['timeout'], $config['persistent']); |
||||
} else { |
||||
$this->redis->connect($config['host'], $config['port'], $config['timeout']); |
||||
} |
||||
|
||||
if ($config['auth']) { |
||||
$this->redis->auth($config['auth']); |
||||
} |
||||
|
||||
if ($config['database']) { |
||||
$this->redis->select($config['database']); |
||||
} |
||||
|
||||
if ($config['read_timeout']) { |
||||
$this->redis->setOption(Redis::OPT_READ_TIMEOUT, $config['read_timeout']); |
||||
} |
||||
|
||||
if ($config['retry_interval']) { |
||||
$this->redis->setOption(Redis::OPT_RETRY_INTERVAL, $config['retry_interval']); |
||||
} |
||||
|
||||
$method = $config['method'] ?: Method::DEFLATE; |
||||
$level = $config['level'] ?: 4; |
||||
$enabled = $config['enabled'] ?: true; |
||||
|
||||
$this->compression = new GzCompression($method, $level, $enabled); |
||||
|
||||
$this->enc = $enc; |
||||
} |
||||
|
||||
private function writeHelper($data): string { |
||||
$gc = $this->compression->compress($data); |
||||
if ($gc !== false) { |
||||
$data = $gc; // data is now compressed |
||||
} |
||||
if ($this->enc === false) { |
||||
return $data; // encryption is off |
||||
} |
||||
$e = $this->enc->encrypt($data); |
||||
return ($e !== false) ? $e : $data; // and encrypted |
||||
} |
||||
|
||||
private function readHelper($data): string { |
||||
if ($data === null || $data === false) { |
||||
return ""; |
||||
} |
||||
if ($this->enc !== false) { |
||||
$de = $this->enc->decrypt($data); |
||||
if ($de!== false) { |
||||
$data = $de; // data is now decrypted |
||||
} |
||||
} |
||||
$gd = $this->compression->decompress($data); |
||||
return ($gd !== false) ? $gd : $data; |
||||
} |
||||
|
||||
public function open($savePath, $sessionName): bool |
||||
{ |
||||
return true; |
||||
} |
||||
|
||||
public function close(): bool |
||||
{ |
||||
return true; |
||||
} |
||||
|
||||
public function read($sessionId): string |
||||
{ |
||||
$key = $this->prefix . $sessionId; |
||||
$attempts = 0; |
||||
|
||||
// Simple locking mechanism to prevent race conditions |
||||
while ($attempts < $this->lockRetries) { |
||||
$lock = $this->redis->setnx($key . '.lock', 1); |
||||
|
||||
if ($lock || $attempts === $this->lockRetries - 1) { |
||||
if ($lock) { |
||||
$this->redis->expire($key . '.lock', $this->lockTimeout); |
||||
} |
||||
|
||||
$data = $this->redis->get($key); |
||||
$this->redis->del($key . '.lock'); |
||||
return $this->readHelper($data) ?: ''; |
||||
} |
||||
|
||||
usleep($this->lockWait); |
||||
$attempts++; |
||||
} |
||||
|
||||
return ''; |
||||
} |
||||
|
||||
public function write($sessionId, $data): bool |
||||
{ |
||||
$data = write_helper($data); |
||||
$key = $this->prefix . $sessionId; |
||||
|
||||
// Only write if the session has changed |
||||
if (isset($_SESSION['__LAST_ACTIVE__'])) { |
||||
$oldData = $this->redis->get($key); |
||||
if ($oldData === $data) { |
||||
// Just update TTL |
||||
return $this->redis->expire($key, $this->ttl); |
||||
} |
||||
} |
||||
|
||||
$_SESSION['__LAST_ACTIVE__'] = time(); |
||||
return $this->redis->setex($key, $this->ttl, $data); |
||||
} |
||||
|
||||
public function destroy($sessionId): bool |
||||
{ |
||||
$key = $this->prefix . $sessionId; |
||||
$this->redis->del($key . '.lock'); |
||||
return (bool)$this->redis->del($key); |
||||
} |
||||
|
||||
public function gc($maxLifetime): bool |
||||
{ |
||||
// Redis handles expiration automatically |
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,26 @@ |
||||
<?php |
||||
|
||||
declare(strict_types = 1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
namespace IOcornerstone\Framework\Services; |
||||
|
||||
use IOcornerstone\Framework\Registry as Reg; |
||||
|
||||
class Twig { |
||||
public static function init(bool $use_file_cache = false) { |
||||
Reg::get('loader')->add_namespace("Twig", IO_CORNERSTONE_PROJECT. "/vendor/twig/twig/src"); |
||||
Reg::get('loader')->add_namespace("Symfony\Polyfill\Mbstring", IO_CORNERSTONE_PROJECT. "/vendor/symfony/polyfill-mbstring"); |
||||
Reg::get('loader')->add_namespace("Symfony\Polyfill\Ctype", IO_CORNERSTONE_PROJECT. "/vendor/symfony/polyfill-ctype"); |
||||
$loader = new \Twig\Loader\FilesystemLoader(IO_CORNERSTONE_PROJECT. "views/twig"); |
||||
|
||||
$a_cache = ($use_file_cache) ? ['cache' => BaseDir . "/protected/runtime/compilation_cache"] : []; |
||||
|
||||
$twig = new \Twig\Environment($loader, $a_cache); |
||||
return $twig; |
||||
} |
||||
} |
||||
@ -0,0 +1,38 @@ |
||||
<?php |
||||
|
||||
declare(strict_types = 1); |
||||
|
||||
/** |
||||
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||
* @copyright (c) 2025, Robert Strutts |
||||
* @license MIT |
||||
*/ |
||||
namespace IOcornerstone\Framework\Services; |
||||
|
||||
use IOcornerstone\Framework\Registry as Reg; |
||||
|
||||
final class TwilioSetup { |
||||
const VENDOR = IO_CORNERSTONE_PROJECT . 'vendor/twilio/sdk/src/Twilio/'; |
||||
|
||||
public static function initSms($sid, $token) { |
||||
if (! Reg::get('loader')->is_loaded('Twilio')) { |
||||
Reg::get('loader')->add_namespace('Twilio', self::VENDOR); |
||||
} |
||||
|
||||
if ($sid === false) { |
||||
throw new \Exception("Twilio SID not defined"); |
||||
} |
||||
if ($token === false) { |
||||
throw new \Exception("Twilio TOKEN not defined"); |
||||
} |
||||
return new \Twilio\Rest\Client($sid, $token); |
||||
} |
||||
|
||||
public static function initVoice() { |
||||
if (! Reg::get('loader')->is_loaded('Twilio')) { |
||||
Reg::get('loader')->add_namespace('Twilio', self::VENDOR); |
||||
return new \Twilio\TwiML\VoiceResponse(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue