153 lines
4.2 KiB
PHP
153 lines
4.2 KiB
PHP
<?php
|
|
namespace App\Helpers;
|
|
|
|
/**
|
|
* Class Security
|
|
* Enterprise Security Utilities (JWT Encoder/Decoder, XSS Sanitizer, CSRF, CSP)
|
|
*
|
|
* @package App\Helpers
|
|
*/
|
|
class Security
|
|
{
|
|
/**
|
|
* Sanitize String against XSS (Cross-Site Scripting)
|
|
*
|
|
* @param string|null $input
|
|
* @return string
|
|
*/
|
|
public static function clean(?string $input): string
|
|
{
|
|
if ($input === null) return '';
|
|
return htmlspecialchars(trim($input), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
}
|
|
|
|
/**
|
|
* Recursive XSS Sanitize for Array or Object
|
|
*
|
|
* @param mixed $data
|
|
* @return mixed
|
|
*/
|
|
public static function cleanArray($data)
|
|
{
|
|
if (is_array($data)) {
|
|
return array_map([self::class, 'cleanArray'], $data);
|
|
}
|
|
if (is_string($data)) {
|
|
return self::clean($data);
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Generate CSRF Token and store in Session
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function generateCsrfToken(): string
|
|
{
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
if (empty($_SESSION['csrf_token'])) {
|
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
}
|
|
return $_SESSION['csrf_token'];
|
|
}
|
|
|
|
/**
|
|
* Verify CSRF Token
|
|
*
|
|
* @param string|null $token
|
|
* @return bool
|
|
*/
|
|
public static function verifyCsrfToken(?string $token): bool
|
|
{
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
if (empty($_SESSION['csrf_token']) || empty($token)) {
|
|
return false;
|
|
}
|
|
return hash_equals($_SESSION['csrf_token'], $token);
|
|
}
|
|
|
|
/**
|
|
* Generate JSON Web Token (JWT RFC7519)
|
|
*
|
|
* @param array $payload
|
|
* @param string $secret
|
|
* @param int $ttl Seconds
|
|
* @return string
|
|
*/
|
|
public static function generateJwt(array $payload, string $secret, int $ttl = 3600): string
|
|
{
|
|
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
|
|
|
|
$payload['iat'] = time();
|
|
$payload['exp'] = time() + $ttl;
|
|
$payload['iss'] = 'https://ttmqms.hospital.local';
|
|
|
|
$base64UrlHeader = self::base64UrlEncode($header);
|
|
$base64UrlPayload = self::base64UrlEncode(json_encode($payload));
|
|
|
|
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true);
|
|
$base64UrlSignature = self::base64UrlEncode($signature);
|
|
|
|
return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
|
|
}
|
|
|
|
/**
|
|
* Verify and Decode JWT Token
|
|
*
|
|
* @param string $token
|
|
* @param string $secret
|
|
* @return array|null Payload array if valid, null if invalid or expired
|
|
*/
|
|
public static function decodeJwt(string $token, string $secret): ?array
|
|
{
|
|
$parts = explode('.', $token);
|
|
if (count($parts) !== 3) return null;
|
|
|
|
[$header64, $payload64, $sig64] = $parts;
|
|
|
|
$signature = self::base64UrlDecode($sig64);
|
|
$expectedSig = hash_hmac('sha256', $header64 . "." . $payload64, $secret, true);
|
|
|
|
if (!hash_equals($expectedSig, $signature)) {
|
|
return null; // Signature mismatch (Tampered)
|
|
}
|
|
|
|
$payload = json_decode(self::base64UrlDecode($payload64), true);
|
|
if (!$payload || !isset($payload['exp']) || $payload['exp'] < time()) {
|
|
return null; // Token expired
|
|
}
|
|
|
|
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, '-_', '+/'));
|
|
}
|
|
|
|
/**
|
|
* Get Client IP Address (Supporting Proxy X-Forwarded-For)
|
|
*/
|
|
public static function getClientIp(): string
|
|
{
|
|
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
|
return $_SERVER['HTTP_CLIENT_IP'];
|
|
}
|
|
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
|
return trim($ips[0]);
|
|
}
|
|
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
|
}
|
|
}
|