Deploy: 2026-05-21 01:26:06
This commit is contained in:
198
backend/app/Core/Security.php
Normal file
198
backend/app/Core/Security.php
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user