parent
3f91fc5272
commit
818aebc99c
@ -0,0 +1,16 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types = 1); |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||||
|
* @copyright (c) 2025, Robert Strutts |
||||||
|
* @license MIT |
||||||
|
*/ |
||||||
|
namespace CodeHydrater\enums; |
||||||
|
|
||||||
|
enum compression_method: string { |
||||||
|
case GZIP = 'gzip'; |
||||||
|
case DEFLATE = 'deflate'; |
||||||
|
case ZLIB = 'zlib'; |
||||||
|
} |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types = 1); |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||||
|
* @copyright (c) 2025, Robert Strutts |
||||||
|
* @license MIT |
||||||
|
*/ |
||||||
|
namespace CodeHydrater; |
||||||
|
|
||||||
|
use \CodeHydrater\enums\compression_method as Method; |
||||||
|
|
||||||
|
class gz_compression { |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
public Method $method = Method::DEFLATE, |
||||||
|
public int $compression_level = 4, |
||||||
|
public bool $use_compression = true |
||||||
|
) {} |
||||||
|
|
||||||
|
public function compress(string $data): string|false { |
||||||
|
if ($this->use_compression === false) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
$level = ($this->compression_level < 10 && $this->compression_level > 0) ? $this->compression_level : 4; |
||||||
|
|
||||||
|
$c = match($this->method) { |
||||||
|
Method::DEFLATE => gzdeflate($data, $level), |
||||||
|
Method::GZIP => gzencode($data, $level), |
||||||
|
Method::ZLIB => gzcompress($data, $level), |
||||||
|
}; |
||||||
|
return base64_encode($c); |
||||||
|
} |
||||||
|
|
||||||
|
public function decompress(string $compressed_data): string|false { |
||||||
|
if ($this->use_compression === false) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
$d = base64_decode($compressed_data); |
||||||
|
return match($this->method) { |
||||||
|
Method::DEFLATE => gzinflate($d), |
||||||
|
Method::GZIP => gzdecode($d), |
||||||
|
Method::ZLIB => gzuncompress($d), |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,355 @@ |
|||||||
|
<?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 CodeHydrater\services; |
||||||
|
|
||||||
|
/* |
||||||
|
* 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 key_bytes = 32; // 32 bytes = 256 bits, 64 bytes = 512 bits, 16 bytes = 128 bits encryption key |
||||||
|
|
||||||
|
private $random_engine; |
||||||
|
private $binary = false; |
||||||
|
private $url_encode = false; |
||||||
|
private $method = 'AES-256-CBC'; |
||||||
|
private $default_hash = 'sha256'; // should be sha256 or higher |
||||||
|
private $_iterations = self::iterations, $_length=self::length, $_raw=self::raw, $_key_bytes=self::key_bytes; |
||||||
|
|
||||||
|
public function __construct(private string $key) { |
||||||
|
$this->random_engine = new \CodeHydrater\random_engine(); |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
* 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 weak_quick_hash(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 change_security_level(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->set_loops(98); // Very very fast |
||||||
|
$this->set_length(64); |
||||||
|
$this->set_key_bytes(16); |
||||||
|
$this->method = 'AES-128-CBC'; |
||||||
|
$this->default_hash = 'sha256'; |
||||||
|
return true; |
||||||
|
case 'blaze': // 0.0109 Seconds |
||||||
|
$this->set_loops(1843); |
||||||
|
$this->set_length(64); |
||||||
|
$this->set_key_bytes(32); |
||||||
|
$this->method = 'AES-128-CBC'; |
||||||
|
$this->default_hash = 'sha256'; |
||||||
|
return true; |
||||||
|
case 'quick': // 0.0167 Seconds |
||||||
|
$this->set_loops(2843); |
||||||
|
$this->set_length(64); |
||||||
|
$this->set_key_bytes(32); |
||||||
|
$this->method = 'AES-192-CBC'; |
||||||
|
$this->default_hash = 'sha256'; |
||||||
|
return true; |
||||||
|
case 'good': // 0.0732 Seconds |
||||||
|
$this->set_loops(11952); |
||||||
|
$this->set_length(64); |
||||||
|
$this->set_key_bytes(32); // 32B or 256b, standard key size |
||||||
|
$this->method = 'AES-256-CBC'; |
||||||
|
$this->default_hash = 'sha256'; |
||||||
|
return true; |
||||||
|
case 'normal': // 0.4901 Seconds |
||||||
|
$this->set_loops(81952); // slow |
||||||
|
$this->set_length(64); |
||||||
|
$this->set_key_bytes(32); |
||||||
|
$this->method = 'AES-256-CBC'; |
||||||
|
$this->default_hash = 'sha256'; |
||||||
|
return true; |
||||||
|
case 'paranoid': // 0.6167 Seconds |
||||||
|
case 'max': |
||||||
|
case 'high': |
||||||
|
case 'slow-secure': // Very slow |
||||||
|
$this->set_loops(82952); |
||||||
|
$this->set_length(128); // extended length to make it work |
||||||
|
$this->set_key_bytes(64); // extended key size for 512 hash |
||||||
|
$this->method = 'AES-256-CBC'; |
||||||
|
$this->default_hash = 'sha512'; |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public function set_loops(int $iterations=self::iterations): void { |
||||||
|
$this->_iterations = $iterations; |
||||||
|
} |
||||||
|
|
||||||
|
public function set_length(int $length=self::length): void { |
||||||
|
$this->_length = $length; |
||||||
|
} |
||||||
|
|
||||||
|
public function set_raw_output(bool $output_raw=self::raw): void { |
||||||
|
$this->_raw = $output_raw; |
||||||
|
} |
||||||
|
|
||||||
|
public function set_key_bytes(int $key_bytes=self::key_bytes): void { |
||||||
|
$this->_key_bytes = $key_bytes; |
||||||
|
} |
||||||
|
|
||||||
|
public function list_ssl_methods(): 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 change_openssl(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->list_ssl_methods() )) { |
||||||
|
$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 list_hashes(): 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 change_hash(string $hash): bool { |
||||||
|
if (in_array(strtolower($hash), $this->list_hashes() )) { |
||||||
|
$this->default_hash = strtolower($hash); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public function set_binary_output(bool $bin) { |
||||||
|
$this->binary = $bin; |
||||||
|
} |
||||||
|
|
||||||
|
public function set_url_encode(bool $url_encode) { |
||||||
|
$this->url_encode = $url_encode; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* OpenSSL Encrypt text with key |
||||||
|
* @param string $text data |
||||||
|
* @return string encoded text |
||||||
|
*/ |
||||||
|
public function encrypt(string $text, bool $validate = true): string { |
||||||
|
$key = $this->key; |
||||||
|
$key = ($validate) ? $this->get_valid_key($key) : $key; |
||||||
|
$ivsize = openssl_cipher_iv_length($this->method); |
||||||
|
$iv = $this->random_engine->get_bytes($ivsize); // Requires PHP 7 |
||||||
|
// Encryption key generated by PBKDF2 (since PHP 5.5) |
||||||
|
$keys = hash_pbkdf2($this->default_hash, $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 |
||||||
|
); |
||||||
|
\CodeHydrater\bootstrap\common::wipe($text); |
||||||
|
\CodeHydrater\bootstrap\common::wipe($key); |
||||||
|
\CodeHydrater\bootstrap\common::wipe($encKey); |
||||||
|
$hmac = hash_hmac($this->default_hash, $iv . $ciphertext, $hmacKey); |
||||||
|
\CodeHydrater\bootstrap\common::wipe($hmacKey); |
||||||
|
if (! $this->binary) { |
||||||
|
return (! $this->url_encode) ? base64_encode($hmac . $iv . $ciphertext) : \CodeHydrater\misc::base64url_encode($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->get_valid_key($key) : $key; |
||||||
|
if (! $this->binary) { |
||||||
|
$text = (! $this->url_encode) ? base64_decode($data) : \CodeHydrater\misc::base64url_decode($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->default_hash, $key, $iv, $this->_iterations, $this->_length, $this->_raw); |
||||||
|
$encKey = substr($keys, 0, $this->_key_bytes); // X bit encryption key |
||||||
|
$hmacNew = hash_hmac($this->default_hash, $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 |
||||||
|
); |
||||||
|
\CodeHydrater\bootstrap\common::wipe($ciphertext); |
||||||
|
\CodeHydrater\bootstrap\common::wipe($key); |
||||||
|
\CodeHydrater\bootstrap\common::wipe($encKey); |
||||||
|
\CodeHydrater\bootstrap\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 get_valid_key(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)); |
||||||
|
\CodeHydrater\bootstrap\common::wipe($key); |
||||||
|
return $ret; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Make a new Secure key |
||||||
|
* @return string key |
||||||
|
*/ |
||||||
|
public function generate_valid_key(): string { |
||||||
|
$ivsize = openssl_cipher_iv_length($this->method); |
||||||
|
return bin2hex($this->random_engine->get_bytes($ivsize)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,137 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace CodeHydrater\services\sessions; |
||||||
|
|
||||||
|
use \CodeHydrater\enums\compression_method as Method; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Robert Strutts |
||||||
|
* @copyright Copyright (c) 2022, Robert Strutts. |
||||||
|
* @license MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
class cookie_sessions_handler_exception extends \Exception {} |
||||||
|
|
||||||
|
class cookie_session_handler implements \SessionHandlerInterface { |
||||||
|
public static string $cookie_domain; |
||||||
|
public static string $cookie_name = 'SES'; |
||||||
|
public static string $cookie_path = '/'; |
||||||
|
public static bool $cookie_secure = true; |
||||||
|
public static bool $cookie_HTTP_only = true; |
||||||
|
private $enc; |
||||||
|
|
||||||
|
public function __construct($enc, array $options) { |
||||||
|
if (isset($options['cookie_domain'])) { |
||||||
|
self::$cookie_domain = $options['cookie_domain']; |
||||||
|
} else { |
||||||
|
self::$cookie_domain = $_SERVER['SERVER_NAME'] ?? ''; |
||||||
|
} |
||||||
|
if (isset($options['cookie_name'])) { |
||||||
|
self::$cookie_name = $options['cookie_name']; |
||||||
|
} |
||||||
|
if (isset($options['cookie_path'])) { |
||||||
|
self::$cookie_path = $options['cookie_path']; |
||||||
|
} |
||||||
|
if (isset($options['cookie_secure'])) { |
||||||
|
self::$cookie_secure = $options['cookie_secure']; |
||||||
|
} else { |
||||||
|
$use_secure = \CodeHydrater\bootstrap\site_helper::get_use_secure(); |
||||||
|
if ($use_secure === false) { |
||||||
|
self::$cookie_secure = false; |
||||||
|
} |
||||||
|
} |
||||||
|
if (isset($options['cookie_HTTP_only'])) { |
||||||
|
self::$cookie_HTTP_only = $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 \CodeHydrater\gz_compression($method, $level, $enabled); |
||||||
|
|
||||||
|
$this->enc = $enc; |
||||||
|
} |
||||||
|
|
||||||
|
private function write_helper($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 read_helper($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::$cookie_name]) ? $_COOKIE[self::$cookie_name] : null; |
||||||
|
return $this->read_helper($data) ?: ""; |
||||||
|
} |
||||||
|
|
||||||
|
public function write($id, $data): bool { |
||||||
|
if (headers_sent($file, $line)) { |
||||||
|
throw new \CodeHydrater\services\sessions\cookie_sessions_handler_exception("Error headers were already sent by $file on line # $line "); |
||||||
|
} |
||||||
|
$data = $this->write_helper($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 \tts\services\sessions\cookie_sessions_handler_exception("Session data too big (over 4KB)"); |
||||||
|
} |
||||||
|
$cookie_lifetime = (int) ini_get('session.cookie_lifetime'); |
||||||
|
|
||||||
|
return setcookie(self::$cookie_name, $data, $cookie_lifetime, self::$cookie_path, self::$cookie_domain, self::$cookie_secure, self::$cookie_HTTP_only); |
||||||
|
} |
||||||
|
|
||||||
|
public function destroy($id): bool { |
||||||
|
$expiration_time = time() - 3600; |
||||||
|
return setcookie(self::$cookie_name, "", $expiration_time, self::$cookie_path, self::$cookie_domain, self::$cookie_secure, self::$cookie_HTTP_only); |
||||||
|
} |
||||||
|
|
||||||
|
public function gc($max_lifetime): int|false { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,181 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||||
|
* @copyright (c) 2025, Robert Strutts |
||||||
|
* @license MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace CodeHydrater\services\sessions; |
||||||
|
|
||||||
|
class file_session_handler 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'] ?: \CodeHydrater\CompressionMethod::DEFLATE; |
||||||
|
$level = $config['level'] ?: 4; |
||||||
|
$enabled = $config['enabled'] ?: true; |
||||||
|
|
||||||
|
$this->compression = new \CodeHydrater\gz_compression($method, $level, $enabled); |
||||||
|
|
||||||
|
$this->enc = $enc; |
||||||
|
} |
||||||
|
|
||||||
|
private function write_helper($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 read_helper($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->read_helper($data); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Write session data |
||||||
|
* @param string $sessionId |
||||||
|
* @param string $data |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function write($sessionId, $data): bool |
||||||
|
{ |
||||||
|
$data = $this->write_helper($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,175 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Robert Strutts <Bob_586@Yahoo.com> |
||||||
|
* @copyright (c) 2025, Robert Strutts |
||||||
|
* @license MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace CodeHydrater\services\sessions; |
||||||
|
|
||||||
|
class redis_session_handler 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'] ?: \CodeHydrater\CompressionMethod::DEFLATE; |
||||||
|
$level = $config['level'] ?: 4; |
||||||
|
$enabled = $config['enabled'] ?: true; |
||||||
|
|
||||||
|
$this->compression = new \CodeHydrater\gz_compression($method, $level, $enabled); |
||||||
|
|
||||||
|
$this->enc = $enc; |
||||||
|
} |
||||||
|
|
||||||
|
private function write_helper($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 read_helper($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->read_helper($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,87 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Robert Strutts |
||||||
|
* @copyright Copyright (c) 2022, Robert Strutts. |
||||||
|
* @license MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace CodeHydrater; |
||||||
|
|
||||||
|
final class session_management { |
||||||
|
|
||||||
|
public static function start( |
||||||
|
array $options = [], |
||||||
|
string $type = "", |
||||||
|
$enc = false |
||||||
|
): void { |
||||||
|
if (empty($type)) { |
||||||
|
$type = bootstrap\configure::get('sessions', 'type'); |
||||||
|
} |
||||||
|
if ($enc === false) { |
||||||
|
$exists = bootstrap\registry::get('di')->exists('session_encryption'); |
||||||
|
if ($exists) { |
||||||
|
$enc = bootstrap\registry::get('di')->get_service('session_encryption'); |
||||||
|
} |
||||||
|
} |
||||||
|
if ($type === "none" || $type === "php") { |
||||||
|
self::make_session_started(); |
||||||
|
return; |
||||||
|
} |
||||||
|
$handler = match($type) { |
||||||
|
'redis' => new services\sessions\redis_session_handler($enc, $options), |
||||||
|
'files' => new services\sessions\file_session_handler($enc, $options), |
||||||
|
default => new services\sessions\cookie_session_handler($enc, $options), |
||||||
|
}; |
||||||
|
session_set_save_handler($handler, true); |
||||||
|
self::make_session_started(); |
||||||
|
} |
||||||
|
|
||||||
|
private static function make_session_started(bool $force_secure = false) { |
||||||
|
if ((function_exists('session_status') && session_status() !== PHP_SESSION_ACTIVE) || !session_id()) { |
||||||
|
$name = bootstrap\configure::get('sessions', 'session_name'); |
||||||
|
if ($name !== null) { |
||||||
|
session_name($name); |
||||||
|
} |
||||||
|
|
||||||
|
if (! headers_sent()) { |
||||||
|
$use_secure = (bootstrap\site_helper::get_use_secure()) ? 1 : 0; |
||||||
|
$use_secure = ($force_secure) ? 1 : $use_secure; |
||||||
|
session_start([ |
||||||
|
'cookie_lifetime' => 0, // until browser is closed |
||||||
|
'cookie_secure' => $use_secure, // require secure cookies if HTTPS is used |
||||||
|
'use_only_cookies' => 1, // should be 1 to prevent URL attacks |
||||||
|
'cookie_httponly' => 1, // should be 1 to disable JavaScript access |
||||||
|
'cookie_samesite' => 'Strict', // should be Strict to prevent XSS |
||||||
|
// So you need it when you do not want to allow a user to pre-define the session ID value. You normally want to prevent that to reduce the attack surface. |
||||||
|
'use_strict_mode' => 1, // Note: Enabling session.use_strict_mode is mandatory for general session security. All sites are advised to enable this. |
||||||
|
'use_trans_sid' => 0, // should be kept at the default of 0: URL based session management has additional security risks |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static function has_user_right(string $right): bool { |
||||||
|
$rights = (isset($_SESSION['users_rights'])) ? $_SESSION['users_rights'] : false; |
||||||
|
if ($rights === false) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (! bootstrap\common::is_json($right)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
$assoc = true; // Use Array format |
||||||
|
$a_rights = json_decode($rights, $assoc); |
||||||
|
if (in_array($right, $a_rights)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public static function get_user_id(): int { |
||||||
|
$sid = (isset($_SESSION['user_id'])) ? $_SESSION['user_id'] : 0; |
||||||
|
return intval($sid); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue