Deploy: 2026-05-21 01:26:06

This commit is contained in:
Hamza-Ayed
2026-05-21 01:26:06 +03:00
parent 146ebd7200
commit 16d494b4e1
13 changed files with 816 additions and 32 deletions

View File

@@ -52,6 +52,16 @@ class Request
return $this->bodyParams;
}
public function setBody(array $bodyParams): void
{
$this->bodyParams = $bodyParams;
}
public function setQueryParams(array $queryParams): void
{
$this->queryParams = $queryParams;
}
public function get(string $key, $default = null)
{
return $this->bodyParams[$key] ?? ($this->queryParams[$key] ?? $default);

View File

@@ -0,0 +1,198 @@
<?php
namespace App\Core;
/**
* Advanced OWASP Security Helper
* Handles AES-256-GCM encryption/decryption, HMAC Blind Indexing,
* Bcrypt password hashing, and JWT validation.
*/
class Security
{
/**
* Get the encryption key from environment (must be 32 bytes for AES-256)
*/
private static function getEncryptionKey(): string
{
$key = getenv('ENCRYPTION_KEY') ;
return substr(hash('sha256', $key, true), 0, 32);
}
/**
* Get HMAC Salt for Blind Indexing
*/
private static function getHmacSalt(): string
{
return getenv('HMAC_SALT');
}
/**
* Get JWT Secret
*/
private static function getJwtSecret(): string
{
return getenv('JWT_SECRET');
}
/**
* Encrypt text using AES-256-GCM
*/
public static function encrypt(string $plainText): string
{
if ($plainText === '') {
return '';
}
$key = self::getEncryptionKey();
$iv = openssl_random_pseudo_bytes(12); // GCM standard IV is 12 bytes
$tag = '';
$ciphertext = openssl_encrypt(
$plainText,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($ciphertext === false) {
return '';
}
// Return combined iv + tag + ciphertext base64 encoded
return base64_encode($iv . $tag . $ciphertext);
}
/**
* Decrypt text using AES-256-GCM
*/
public static function decrypt(string $encryptedText): string
{
if ($encryptedText === '') {
return '';
}
$key = self::getEncryptionKey();
$data = base64_decode($encryptedText);
// AES-256-GCM tag is 16 bytes, IV is 12 bytes
if (strlen($data) < 28) {
return '';
}
$iv = substr($data, 0, 12);
$tag = substr($data, 12, 16);
$ciphertext = substr($data, 28);
$decrypted = openssl_decrypt(
$ciphertext,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
return $decrypted !== false ? $decrypted : '';
}
/**
* Generate Blind Index (HMAC-SHA256) for secure database queries
*/
public static function blindIndex(string $value): string
{
if ($value === '') {
return '';
}
$normalized = strtolower(trim($value));
return hash_hmac('sha256', $normalized, self::getHmacSalt());
}
/**
* Hash password using bcrypt
*/
public static function hashPassword(string $password): string
{
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
}
/**
* Verify password
*/
public static function verifyPassword(string $password, string $hash): bool
{
return password_verify($password, $hash);
}
/**
* Generate JWT Token with HMAC-SHA256 signature
* Includes user_id, company_id, role, iss, aud, and jti.
*/
public static function generateJWT(array $payload, int $expirySeconds = 86400): string
{
$header = self::base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
// Standard OWASP Claims
$payload['iat'] = time();
$payload['exp'] = time() + $expirySeconds;
$payload['iss'] = getenv('APP_URL'); // Issuer
$payload['aud'] = 'nabeh_dashboard'; // Audience
$payload['jti'] = bin2hex(random_bytes(16)); // JWT ID to prevent Replay Attacks
$payloadEncoded = self::base64UrlEncode(json_encode($payload));
$secret = self::getJwtSecret();
$signature = hash_hmac('sha256', "$header.$payloadEncoded", $secret, true);
$signatureEncoded = self::base64UrlEncode($signature);
return "$header.$payloadEncoded.$signatureEncoded";
}
/**
* Verify JWT Token and return payload if valid, false otherwise
*/
public static function verifyJWT(string $token)
{
$parts = explode('.', $token);
if (count($parts) !== 3) {
return false;
}
list($headerEncoded, $payloadEncoded, $signatureEncoded) = $parts;
$secret = self::getJwtSecret();
if (!$secret) {
return false;
}
$signature = self::base64UrlDecode($signatureEncoded);
$expectedSignature = hash_hmac('sha256', "$headerEncoded.$payloadEncoded", $secret, true);
if (!hash_equals($signature, $expectedSignature)) {
return false;
}
$payload = json_decode(self::base64UrlDecode($payloadEncoded), true);
if (!$payload || !isset($payload['exp']) || time() >= $payload['exp']) {
return false;
}
// Validate Issuer
$expectedIssuer = getenv('APP_URL');
if (isset($payload['iss']) && $payload['iss'] !== $expectedIssuer) {
return false;
}
return $payload;
}
private static function base64UrlEncode(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
private static function base64UrlDecode(string $data): string
{
return base64_decode(strtr($data, '-_', '+/') . str_repeat('=', (4 - strlen($data) % 4) % 4));
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace App\Core;
/**
* Core Validation Engine
* Handles data validation before processing.
*/
class Validator
{
private array $errors = [];
/**
* Validate an array of data against rules.
* Example rules: ['email' => 'required|email', 'password' => 'required|min:8']
*/
public function validate(array $data, array $rules): bool
{
$this->errors = [];
foreach ($rules as $field => $ruleString) {
$rulesArray = explode('|', $ruleString);
$value = $data[$field] ?? null;
foreach ($rulesArray as $rule) {
$this->applyRule($field, $value, $rule);
}
}
return empty($this->errors);
}
/**
* Get validation errors.
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* Apply a specific rule to a field's value.
*/
private function applyRule(string $field, $value, string $rule): void
{
// Parse rule with parameters (e.g., min:8)
$params = [];
if (strpos($rule, ':') !== false) {
list($rule, $paramStr) = explode(':', $rule, 2);
$params = explode(',', $paramStr);
}
switch ($rule) {
case 'required':
if ($value === null || trim((string)$value) === '') {
$this->addError($field, "The {$field} field is required.");
}
break;
case 'email':
if ($value && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$this->addError($field, "The {$field} must be a valid email address.");
}
break;
case 'min':
$min = (int)($params[0] ?? 0);
if ($value && strlen((string)$value) < $min) {
$this->addError($field, "The {$field} must be at least {$min} characters.");
}
break;
case 'max':
$max = (int)($params[0] ?? 0);
if ($value && strlen((string)$value) > $max) {
$this->addError($field, "The {$field} must not exceed {$max} characters.");
}
break;
case 'numeric':
if ($value && !is_numeric($value)) {
$this->addError($field, "The {$field} must be a number.");
}
break;
case 'strong_password':
// At least 8 chars, 1 uppercase, 1 lowercase, 1 number
if ($value && !preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/', $value)) {
$this->addError($field, "The {$field} must be at least 8 characters long and contain uppercase, lowercase, and a number.");
}
break;
}
}
private function addError(string $field, string $message): void
{
if (!isset($this->errors[$field])) {
$this->errors[$field] = [];
}
$this->errors[$field][] = $message;
}
}