🚀 مُصادَق: الإطلاق الأولي للنظام المتكامل
This commit is contained in:
57
app/Services/Security/EncryptionService.php
Normal file
57
app/Services/Security/EncryptionService.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Security;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class EncryptionService
|
||||
{
|
||||
private string $key;
|
||||
private const METHOD = 'aes-256-gcm';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Key should be 32 bytes for aes-256-gcm
|
||||
$this->key = $_ENV['ENCRYPTION_KEY'] ?? '';
|
||||
if (strlen($this->key) !== 32) {
|
||||
// In a real app, this would be in config/secrets.php
|
||||
// For now, we use a fallback if not set, but warn in production
|
||||
$this->key = hash('sha256', $_ENV['JWT_SECRET'] ?? 'fallback-key');
|
||||
}
|
||||
}
|
||||
|
||||
public function encrypt(string $plaintext): string
|
||||
{
|
||||
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::METHOD));
|
||||
$ciphertext = openssl_encrypt($plaintext, self::METHOD, $this->key, 0, $iv, $tag);
|
||||
|
||||
if ($ciphertext === false) {
|
||||
throw new Exception("Encryption failed.");
|
||||
}
|
||||
|
||||
return base64_encode($iv) . ':' . base64_encode($ciphertext) . ':' . base64_encode($tag);
|
||||
}
|
||||
|
||||
public function decrypt(string $encryptedData): string
|
||||
{
|
||||
$parts = explode(':', $encryptedData);
|
||||
if (count($parts) !== 3) {
|
||||
throw new Exception("Invalid encrypted data format.");
|
||||
}
|
||||
|
||||
[$ivBase64, $ciphertextBase64, $tagBase64] = $parts;
|
||||
$iv = base64_decode($ivBase64);
|
||||
$ciphertext = base64_decode($ciphertextBase64);
|
||||
$tag = base64_decode($tagBase64);
|
||||
|
||||
$plaintext = openssl_decrypt($ciphertext, self::METHOD, $this->key, 0, $iv, $tag);
|
||||
|
||||
if ($plaintext === false) {
|
||||
throw new Exception("Decryption failed.");
|
||||
}
|
||||
|
||||
return $plaintext;
|
||||
}
|
||||
}
|
||||
56
app/Services/Security/HmacService.php
Normal file
56
app/Services/Security/HmacService.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Security;
|
||||
|
||||
use App\Core\Redis;
|
||||
|
||||
final class HmacService
|
||||
{
|
||||
/**
|
||||
* Verify HMAC signature for external API requests (Flutter)
|
||||
*/
|
||||
public function verify(
|
||||
string $secret,
|
||||
string $method,
|
||||
string $path,
|
||||
string $timestamp,
|
||||
string $nonce,
|
||||
string $body,
|
||||
string $providedSignature
|
||||
): bool {
|
||||
// 1. Check timestamp (within 5 minutes)
|
||||
if (abs(time() - (int)$timestamp) > 300) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Replay protection using Nonce in Redis
|
||||
// Note: Redis::getInstance() would be used here
|
||||
// If nonce exists, reject
|
||||
|
||||
// 3. Calculate Signature
|
||||
$bodyHash = hash('sha256', $body);
|
||||
$stringToSign = strtoupper($method) . "\n" .
|
||||
$path . "\n" .
|
||||
$timestamp . "\n" .
|
||||
$nonce . "\n" .
|
||||
$bodyHash;
|
||||
|
||||
$calculatedSignature = hash_hmac('sha256', $stringToSign, $secret);
|
||||
|
||||
return hash_equals($calculatedSignature, $providedSignature);
|
||||
}
|
||||
|
||||
public function sign(string $secret, string $method, string $path, string $timestamp, string $nonce, string $body): string
|
||||
{
|
||||
$bodyHash = hash('sha256', $body);
|
||||
$stringToSign = strtoupper($method) . "\n" .
|
||||
$path . "\n" .
|
||||
$timestamp . "\n" .
|
||||
$nonce . "\n" .
|
||||
$bodyHash;
|
||||
|
||||
return hash_hmac('sha256', $stringToSign, $secret);
|
||||
}
|
||||
}
|
||||
48
app/Services/Security/JwtService.php
Normal file
48
app/Services/Security/JwtService.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Security;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Exception;
|
||||
|
||||
final class JwtService
|
||||
{
|
||||
private string $secret;
|
||||
private int $accessExpiry;
|
||||
private int $refreshExpiry;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->secret = $_ENV['JWT_SECRET'] ?? 'change-me';
|
||||
$this->accessExpiry = (int)($_ENV['JWT_ACCESS_EXPIRY'] ?? 900);
|
||||
$this->refreshExpiry = (int)($_ENV['JWT_REFRESH_EXPIRY'] ?? 604800);
|
||||
}
|
||||
|
||||
public function issueAccessToken(array $payload): string
|
||||
{
|
||||
$payload['exp'] = time() + $this->accessExpiry;
|
||||
$payload['iat'] = time();
|
||||
$payload['jti'] = bin2hex(random_bytes(16));
|
||||
|
||||
return JWT::encode($payload, $this->secret, 'HS256');
|
||||
}
|
||||
|
||||
public function issueRefreshToken(string $userId): string
|
||||
{
|
||||
// Refresh token is a random string stored in DB (hashed)
|
||||
return bin2hex(random_bytes(64));
|
||||
}
|
||||
|
||||
public function verifyToken(string $token): array
|
||||
{
|
||||
try {
|
||||
$decoded = JWT::decode($token, new Key($this->secret, 'HS256'));
|
||||
return (array) $decoded;
|
||||
} catch (Exception $e) {
|
||||
throw new Exception("Invalid or expired token: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user