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; } }