Files
intaleq_v2/app/Services/PayloadCrypto.php
2026-04-22 23:56:02 +03:00

136 lines
4.0 KiB
PHP

<?php
namespace App\Services;
/**
* خدمة تشفير حمولة البيانات (Payload Crypto Service)
*
* الغرض من الملف:
* تشفير وفك تشفير البيانات المتبادلة بين التطبيق (Flutter) والخادم (API) باستخدام تقنية AES-256-GCM.
*
* كيفية العمل:
* 1. يولد مفتاح تشفير ديناميكي وفريد لكل طلب (IV).
* 2. يضمن خصوصية البيانات (تشفير) وسلامتها (منع التلاعب عبر الـ Authentication Tag).
* 3. يختلف عن التشفير القديم (Legacy) بأنه أكثر أماناً ويستخدم معايير تشفير حديثة.
*/
class PayloadCrypto
{
private ?string $key = null;
private const CIPHER = 'aes-256-gcm';
private const IV_LENGTH = 12;
private const TAG_LENGTH = 16;
/**
* PayloadCrypto can be initialized with a specific key,
* or a key can be set later via setKeyFromSecret().
*/
public function __construct(?string $rawKey = null)
{
if ($rawKey) {
$this->setKeyFromSecret($rawKey);
}
}
/**
* 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;
}
/**
* Encrypt payload for sending to Flutter app
*
* @param array|string $data Data to encrypt
* @return string Base64 encoded (IV + ciphertext + tag)
*/
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 = '';
$ciphertext = openssl_encrypt(
$plaintext,
self::CIPHER,
$this->key,
OPENSSL_RAW_DATA,
$iv,
$tag,
'', // Additional Authenticated Data (AAD)
self::TAG_LENGTH
);
if ($ciphertext === false) {
throw new \RuntimeException('Encryption failed');
}
// Pack: IV (12) + ciphertext (variable) + tag (16)
return base64_encode($iv . $ciphertext . $tag);
}
/**
* Decrypt payload received from Flutter app
*
* @param string $encoded Base64 encoded (IV + ciphertext + tag)
* @return string|null Decrypted plaintext or null on failure
*/
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;
}
$iv = substr($raw, 0, self::IV_LENGTH);
$tag = substr($raw, -self::TAG_LENGTH);
$ciphertext = substr($raw, self::IV_LENGTH, -self::TAG_LENGTH);
$plaintext = openssl_decrypt(
$ciphertext,
self::CIPHER,
$this->key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
return $plaintext !== false ? $plaintext : null;
}
/**
* Decrypt and decode JSON payload
*/
public function decryptJson(string $encoded): ?array
{
$plaintext = $this->decrypt($encoded);
if (!$plaintext) return null;
$data = json_decode($plaintext, true);
return is_array($data) ? $data : null;
}
}