When designing network request security, we face a fundamental challenge: symmetric encryption offers speed but requires secure key exchange, while asymmetric encryption provides secure key distribution but operates too slowly for bulk data encryption. This article demonstrates how to combine both approaches, following the same principles used in HTTPS, to achieve secure, tamper-proof data transmission for your applications.
Symmetric Encryption Characteristics
Symmetric encryption algorithms like AES provide significant advantages for data protection. They are publicly known, computationally efficient, and deliver fast encryption speeds suitable for processing large amounts of data. However, the critical weakness lies in key management: both parties must agree on and securely exchange a secret key before any communication begins. If an attacker compromises this key, all encrypted communications become vulnerable. Managing numerous keys across different clients and sessions compounds this complexity, making large-scale key distribution impractical.
Asymmetric Encryption Characteristics
Asymmetric encryption algorithms such as RSA offer a different trade-off. Their mathematical complexity makes brute-force attacks computationally infeasible, providing strong security guarantees. However, the computational overhead of asymmetric operations means they cannot match the throughput of symmetric algorithms, making them unsuitable for encrypting large data payloads directly.
The Hybrid Encryption Solution
The optimal strategy combines both encryption types, leveraging their respective strengths. This hybrid approach mirrors the TLS handshake protocol: use asymmetric encryption to securely exchange a symmetric key, then use that symmetric key for efficient bulk data encryption. The following implementation demonstrates this pattern for securing API communications.
Request Encryption Flow
When the client initiates a request, the encryption process follows these steps:
- Generate a random 16-character alphanumeric string to serve as the AES session key
- Encrypt the request payload using AES-128-ECB with this session key
- Encrypt the session key itself using the server's RSA public key
- Base64-encode a JSON structure containing both the encrypted key and encrypted data
- Transmit the encoded string as the request body
{
"payload": "Base64-encoded JSON containing encryptedKey and encryptedData"
}
Response Encryption Flow
The server processes requests using a symmetric flow for responses:
- Generate a fresh random AES session key for each response
- Encrypt the response data using this session key
- Encrypt the session key with the server's RSA private key (for client decryption)
- Package both encrypted components and transmit
Implementation in PHP
AES Encryption Utility
<?php
declare(strict_types=1);
namespace App\Security;
class AesCipher
{
public static function encrypt(string $plaintext, string $key): string
{
$encrypted = openssl_encrypt(
$plaintext,
'AES-128-ECB',
$key,
OPENSSL_RAW_DATA
);
return base64_encode($encrypted);
}
public static function decrypt(string $ciphertext, string $key): string
{
$decoded = base64_decode($ciphertext);
return openssl_decrypt(
$decoded,
'AES-128-ECB',
$key,
OPENSSL_RAW_DATA
);
}
}
RSA Key Management Service
<?php
declare(strict_types=1);
namespace App\Security;
use RuntimeException;
class RsaKeyManager
{
private $publicKey;
private $privateKey;
private const PUBLIC_KEY_PATH = '/etc/keys/rsa_public.pem';
private const PRIVATE_KEY_PATH = '/etc/keys/rsa_private_pkcs8.pem';
public function __construct()
{
$this->publicKey = $this->loadPublicKey();
$this->privateKey = $this->loadPrivateKey();
}
private function loadPublicKey()
{
$keyData = file_get_contents(self::PUBLIC_KEY_PATH);
return openssl_pkey_get_public($keyData);
}
private function loadPrivateKey()
{
$keyData = file_get_contents(self::PRIVATE_KEY_PATH);
return openssl_pkey_get_private($keyData);
}
public function encryptWithPublicKey(string $data): string
{
$encrypted = '';
$success = openssl_public_encrypt($data, $encrypted, $this->publicKey);
if (!$success) {
throw new RuntimeException('Public key encryption failed');
}
return $encrypted;
}
public function decryptWithPrivateKey(string $encrypted): string
{
$decrypted = '';
$success = openssl_private_decrypt($encrypted, $decrypted, $this->privateKey);
if (!$success) {
throw new RuntimeException('Private key decryption failed');
}
return $decrypted;
}
}
Secure Response Builder
<?php
declare(strict_types=1);
namespace App\Http;
use App\Security\AesCipher;
use App\Security\RsaKeyManager;
class SecureResponse
{
private RsaKeyManager $rsaManager;
public function __construct(RsaKeyManager $rsaManager)
{
$this->rsaManager = $rsaManager;
}
public function createEncryptedResponse(array $data): array
{
$sessionKey = $this->generateSessionKey(16);
$encryptedData = AesCipher::encrypt(
json_encode($data),
$sessionKey
);
$encryptedKey = $this->rsaManager->encryptWithPublicKey($sessionKey);
$package = [
'key' => base64_encode($encryptedKey),
'data' => $encryptedData,
];
return [
'content' => base64_encode(json_encode($package)),
'status' => 200,
'headers' => [],
];
}
private function generateSessionKey(int $length): string
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$key = '';
for ($i = 0; $i < $length; $i++) {
$key .= $characters[random_int(0, strlen($characters) - 1)];
}
return $key;
}
}
Secure Request Handler
<?php
declare(strict_types=1);
namespace App\Http;
use App\Security\AesCipher;
use App\Security\RsaKeyManager;
use Exception;
class SecureRequestProcessor
{
private RsaKeyManager $rsaManager;
public function __construct(RsaKeyManager $rsaManager)
{
$this->rsaManager = $rsaManager;
}
public function decryptRequest(string $encryptedPackage): array
{
$decoded = json_decode(
base64_decode($encryptedPackage),
true
);
$encryptedKey = base64_decode($decoded['key']);
$encryptedData = $decoded['data'];
$sessionKey = $this->rsaManager->decryptWithPrivateKey($encryptedKey);
$plaintext = AesCipher::decrypt($encryptedData, $sessionKey);
$payload = json_decode($plaintext, true);
$this->validateTimestamp($payload);
return $payload;
}
private function validateTimestamp(array $payload): void
{
if (!isset($payload['timestamp'])) {
return;
}
$age = time() - $payload['timestamp'];
if ($age > 30) {
throw new Exception('Request timestamp expired');
}
}
}
Controller Integration
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Http\SecureRequestProcessor;
use App\Http\SecureResponse;
class AuthenticationController
{
private SecureRequestProcessor $processor;
private SecureResponse $responseBuilder;
public function handleLogin(array $requestData): array
{
$decrypted = $this->processor->decryptRequest(
$requestData['payload']
);
$username = $decrypted['username'] ?? '';
$password = $decrypted['password'] ?? '';
// Authentication logic proceeds here
// ...
return $this->responseBuilder->createEncryptedResponse([
'status' => 'authenticated',
'userId' => 12345,
]);
}
}
Security Considerations
The timestamp validation mechanism prevents replay attacks by ensuring requests are processed within a short time window. This 30-second threshold effectively blocks intercepted requests from being replayed after expiration. The hybrid approach ensures that even if an attacker captures network traffic, they cannot decrypt the content without access to the RSA private key, and they cannot forge valid requests without generating new cryptographic signatures.
Session keys should never be reused across different requests or sessions. Each request generates a fresh random key, limiting the impact of any potential key compromise to a single transaction rather than exposing all historical or future communications.