136 lines
4.0 KiB
PHP
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;
|
|
}
|
|
}
|