From d64a423db94baaad031389df7f7725a00d17e470 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Wed, 22 Apr 2026 23:56:02 +0300 Subject: [PATCH] Initial V2 commit 4\5 --- app/Http/Middleware/HmacAuthMiddleware.php | 61 ++++++++++++++++------ app/Services/PayloadCrypto.php | 46 +++++++++++++--- 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/app/Http/Middleware/HmacAuthMiddleware.php b/app/Http/Middleware/HmacAuthMiddleware.php index af6e341..2d33ca7 100644 --- a/app/Http/Middleware/HmacAuthMiddleware.php +++ b/app/Http/Middleware/HmacAuthMiddleware.php @@ -7,6 +7,8 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Cache; +use App\Services\PayloadCrypto; + /** * وسيط التحقق من التوقيع الرقمي (HMAC Validation Middleware) * @@ -16,12 +18,18 @@ use Illuminate\Support\Facades\Cache; * كيفية العمل: * 1. يتطلب وجود "توقيع" (Signature) في رأس الطلب (Headers). * 2. يقوم الخادم بإعادة حساب التوقيع باستخدام مفتاح سري (API Secret) ومقارنته بالتوقيع المرسل. - * 3. يحمي من هجمات "إعادة الإرسال" (Replay Attacks) عن طريق التحقق من الـ Nonce والـ Timestamp. + * 3. حماية البيانات: إذا كانت البيانات مشفرة، يقوم بفك تشفيرها باستخدام نفس المفتاح السري (API Secret). * 4. يضمن أن البيانات لم تتغير في الطريق (Data Integrity). */ class HmacAuthMiddleware { private const ALGORITHM = 'sha256'; + private PayloadCrypto $crypto; + + public function __construct(PayloadCrypto $crypto) + { + $this->crypto = $crypto; + } public function handle(Request $request, Closure $next) { @@ -49,16 +57,12 @@ class HmacAuthMiddleware ], 401); } - // 3. Check nonce uniqueness (if provided) + // 3. Check nonce uniqueness if ($nonce) { $nonceKey = "nonce:{$nonce}"; if (Cache::has($nonceKey)) { - return response()->json([ - 'status' => 'failure', - 'message' => 'Duplicate request' - ], 401); + return response()->json(['status' => 'failure', 'message' => 'Duplicate request'], 401); } - // Store nonce for double the tolerance window Cache::put($nonceKey, true, $tolerance * 2); } @@ -66,10 +70,7 @@ class HmacAuthMiddleware $credentials = $this->getApiCredentials($apiKey); if (!$credentials) { - return response()->json([ - 'status' => 'failure', - 'message' => 'Invalid API key' - ], 401); + return response()->json(['status' => 'failure', 'message' => 'Invalid API key'], 401); } // 5. Reconstruct and verify HMAC signature @@ -78,13 +79,41 @@ class HmacAuthMiddleware $expectedSignature = hash_hmac(self::ALGORITHM, $message, $credentials->api_secret); if (!hash_equals($expectedSignature, $signature)) { - return response()->json([ - 'status' => 'failure', - 'message' => 'Invalid signature' - ], 401); + return response()->json(['status' => 'failure', 'message' => 'Invalid signature'], 401); } - // 6. Attach user info to request for controllers + // 6. Optional: Auto-Decrypt Payload if it's encrypted + // We assume if it's a non-JSON string, it might be encrypted + if (!empty($payload) && !str_starts_with(trim($payload), '{')) { + try { + $this->crypto->setKeyFromSecret($credentials->api_secret); + $decrypted = $this->crypto->decrypt($payload); + + if ($decrypted) { + // Replace request content with decrypted data + $request->initialize( + $request->query->all(), + $request->request->all(), + $request->attributes->all(), + $request->cookies->all(), + $request->files->all(), + $request->server->all(), + $decrypted + ); + + // Also merge decrypted JSON into request data + $jsonData = json_decode($decrypted, true); + if (is_array($jsonData)) { + $request->merge($jsonData); + } + } + } catch (\Exception $e) { + // If decryption fails, we might still want to proceed if it wasn't meant to be encrypted + // but usually, a failed signature check (above) would have caught tampering. + } + } + + // 7. Attach user info to request for controllers $request->merge([ '_auth_user_id' => $credentials->user_id, '_auth_user_type' => $credentials->user_type, diff --git a/app/Services/PayloadCrypto.php b/app/Services/PayloadCrypto.php index 5541813..142b919 100644 --- a/app/Services/PayloadCrypto.php +++ b/app/Services/PayloadCrypto.php @@ -15,20 +15,42 @@ namespace App\Services; */ class PayloadCrypto { - private string $key; + private ?string $key = null; private const CIPHER = 'aes-256-gcm'; private const IV_LENGTH = 12; private const TAG_LENGTH = 16; - public function __construct() + /** + * PayloadCrypto can be initialized with a specific key, + * or a key can be set later via setKeyFromSecret(). + */ + public function __construct(?string $rawKey = null) { - $keyPath = config('intaleq.legacy_enc_key_path'); - if (!file_exists($keyPath)) { - throw new \RuntimeException('Encryption key not found'); + if ($rawKey) { + $this->setKeyFromSecret($rawKey); } - // Derive a 32-byte key from the stored key using HKDF - $rawKey = trim(file_get_contents($keyPath)); - $this->key = hash_hkdf('sha256', $rawKey, 32, 'intaleq-v2-gcm'); + } + + /** + * Derives a 32-byte AES key from a raw secret (like api_secret) using HKDF. + */ + public function setKeyFromSecret(string $rawSecret): self + { + // Derive a 32-byte key from the secret using HKDF + $this->key = hash_hkdf('sha256', $rawSecret, 32, 'intaleq-v2-gcm'); + return $this; + } + + /** + * Set a 32-byte key directly. + */ + public function setRawKey(string $key): self + { + if (strlen($key) !== 32) { + throw new \InvalidArgumentException('Key must be exactly 32 bytes'); + } + $this->key = $key; + return $this; } /** @@ -39,6 +61,10 @@ class PayloadCrypto */ public function encrypt($data): string { + if (!$this->key) { + throw new \RuntimeException('Encryption key not set. Call setKeyFromSecret() first.'); + } + $plaintext = is_array($data) ? json_encode($data) : $data; $iv = random_bytes(self::IV_LENGTH); $tag = ''; @@ -70,6 +96,10 @@ class PayloadCrypto */ public function decrypt(string $encoded): ?string { + if (!$this->key) { + throw new \RuntimeException('Decryption key not set. Call setKeyFromSecret() first.'); + } + $raw = base64_decode($encoded, true); if ($raw === false || strlen($raw) < self::IV_LENGTH + self::TAG_LENGTH + 1) { return null;