Initial V2 commit 4\5
This commit is contained in:
@@ -7,6 +7,8 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
use App\Services\PayloadCrypto;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* وسيط التحقق من التوقيع الرقمي (HMAC Validation Middleware)
|
* وسيط التحقق من التوقيع الرقمي (HMAC Validation Middleware)
|
||||||
*
|
*
|
||||||
@@ -16,12 +18,18 @@ use Illuminate\Support\Facades\Cache;
|
|||||||
* كيفية العمل:
|
* كيفية العمل:
|
||||||
* 1. يتطلب وجود "توقيع" (Signature) في رأس الطلب (Headers).
|
* 1. يتطلب وجود "توقيع" (Signature) في رأس الطلب (Headers).
|
||||||
* 2. يقوم الخادم بإعادة حساب التوقيع باستخدام مفتاح سري (API Secret) ومقارنته بالتوقيع المرسل.
|
* 2. يقوم الخادم بإعادة حساب التوقيع باستخدام مفتاح سري (API Secret) ومقارنته بالتوقيع المرسل.
|
||||||
* 3. يحمي من هجمات "إعادة الإرسال" (Replay Attacks) عن طريق التحقق من الـ Nonce والـ Timestamp.
|
* 3. حماية البيانات: إذا كانت البيانات مشفرة، يقوم بفك تشفيرها باستخدام نفس المفتاح السري (API Secret).
|
||||||
* 4. يضمن أن البيانات لم تتغير في الطريق (Data Integrity).
|
* 4. يضمن أن البيانات لم تتغير في الطريق (Data Integrity).
|
||||||
*/
|
*/
|
||||||
class HmacAuthMiddleware
|
class HmacAuthMiddleware
|
||||||
{
|
{
|
||||||
private const ALGORITHM = 'sha256';
|
private const ALGORITHM = 'sha256';
|
||||||
|
private PayloadCrypto $crypto;
|
||||||
|
|
||||||
|
public function __construct(PayloadCrypto $crypto)
|
||||||
|
{
|
||||||
|
$this->crypto = $crypto;
|
||||||
|
}
|
||||||
|
|
||||||
public function handle(Request $request, Closure $next)
|
public function handle(Request $request, Closure $next)
|
||||||
{
|
{
|
||||||
@@ -49,16 +57,12 @@ class HmacAuthMiddleware
|
|||||||
], 401);
|
], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check nonce uniqueness (if provided)
|
// 3. Check nonce uniqueness
|
||||||
if ($nonce) {
|
if ($nonce) {
|
||||||
$nonceKey = "nonce:{$nonce}";
|
$nonceKey = "nonce:{$nonce}";
|
||||||
if (Cache::has($nonceKey)) {
|
if (Cache::has($nonceKey)) {
|
||||||
return response()->json([
|
return response()->json(['status' => 'failure', 'message' => 'Duplicate request'], 401);
|
||||||
'status' => 'failure',
|
|
||||||
'message' => 'Duplicate request'
|
|
||||||
], 401);
|
|
||||||
}
|
}
|
||||||
// Store nonce for double the tolerance window
|
|
||||||
Cache::put($nonceKey, true, $tolerance * 2);
|
Cache::put($nonceKey, true, $tolerance * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,10 +70,7 @@ class HmacAuthMiddleware
|
|||||||
$credentials = $this->getApiCredentials($apiKey);
|
$credentials = $this->getApiCredentials($apiKey);
|
||||||
|
|
||||||
if (!$credentials) {
|
if (!$credentials) {
|
||||||
return response()->json([
|
return response()->json(['status' => 'failure', 'message' => 'Invalid API key'], 401);
|
||||||
'status' => 'failure',
|
|
||||||
'message' => 'Invalid API key'
|
|
||||||
], 401);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Reconstruct and verify HMAC signature
|
// 5. Reconstruct and verify HMAC signature
|
||||||
@@ -78,13 +79,41 @@ class HmacAuthMiddleware
|
|||||||
$expectedSignature = hash_hmac(self::ALGORITHM, $message, $credentials->api_secret);
|
$expectedSignature = hash_hmac(self::ALGORITHM, $message, $credentials->api_secret);
|
||||||
|
|
||||||
if (!hash_equals($expectedSignature, $signature)) {
|
if (!hash_equals($expectedSignature, $signature)) {
|
||||||
return response()->json([
|
return response()->json(['status' => 'failure', 'message' => 'Invalid signature'], 401);
|
||||||
'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([
|
$request->merge([
|
||||||
'_auth_user_id' => $credentials->user_id,
|
'_auth_user_id' => $credentials->user_id,
|
||||||
'_auth_user_type' => $credentials->user_type,
|
'_auth_user_type' => $credentials->user_type,
|
||||||
|
|||||||
@@ -15,20 +15,42 @@ namespace App\Services;
|
|||||||
*/
|
*/
|
||||||
class PayloadCrypto
|
class PayloadCrypto
|
||||||
{
|
{
|
||||||
private string $key;
|
private ?string $key = null;
|
||||||
private const CIPHER = 'aes-256-gcm';
|
private const CIPHER = 'aes-256-gcm';
|
||||||
private const IV_LENGTH = 12;
|
private const IV_LENGTH = 12;
|
||||||
private const TAG_LENGTH = 16;
|
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 ($rawKey) {
|
||||||
if (!file_exists($keyPath)) {
|
$this->setKeyFromSecret($rawKey);
|
||||||
throw new \RuntimeException('Encryption key not found');
|
|
||||||
}
|
}
|
||||||
// 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
|
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;
|
$plaintext = is_array($data) ? json_encode($data) : $data;
|
||||||
$iv = random_bytes(self::IV_LENGTH);
|
$iv = random_bytes(self::IV_LENGTH);
|
||||||
$tag = '';
|
$tag = '';
|
||||||
@@ -70,6 +96,10 @@ class PayloadCrypto
|
|||||||
*/
|
*/
|
||||||
public function decrypt(string $encoded): ?string
|
public function decrypt(string $encoded): ?string
|
||||||
{
|
{
|
||||||
|
if (!$this->key) {
|
||||||
|
throw new \RuntimeException('Decryption key not set. Call setKeyFromSecret() first.');
|
||||||
|
}
|
||||||
|
|
||||||
$raw = base64_decode($encoded, true);
|
$raw = base64_decode($encoded, true);
|
||||||
if ($raw === false || strlen($raw) < self::IV_LENGTH + self::TAG_LENGTH + 1) {
|
if ($raw === false || strlen($raw) < self::IV_LENGTH + self::TAG_LENGTH + 1) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user