Initial V2 commit
This commit is contained in:
104
app/Services/PayloadCrypto.php
Normal file
104
app/Services/PayloadCrypto.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
/**
|
||||
* Payload Crypto Service — AES-256-GCM
|
||||
*
|
||||
* Dynamic encryption for all payloads between Flutter apps and the API.
|
||||
* Unlike LegacyEncryption which uses static IV, this generates a unique IV per request.
|
||||
*
|
||||
* Format: base64(IV + ciphertext + tag)
|
||||
* - IV: 12 bytes (random per encryption)
|
||||
* - Tag: 16 bytes (integrity verification)
|
||||
*/
|
||||
class PayloadCrypto
|
||||
{
|
||||
private string $key;
|
||||
private const CIPHER = 'aes-256-gcm';
|
||||
private const IV_LENGTH = 12;
|
||||
private const TAG_LENGTH = 16;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$keyPath = config('intaleq.legacy_enc_key_path');
|
||||
if (!file_exists($keyPath)) {
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$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
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user