Overview
RSA-AES Hybrid encryption combines RSA-2048 for secure key exchange and AES-256-CBC for high-performance data encryption, providing both the security of asymmetric encryption and the efficiency of symmetric encryption.
Key Features
- RSA-2048 for encrypting session keys
- AES-256-CBC with 256-bit session keys for data encryption
- SHA256withRSA digital signatures for integrity verification
- Per-request session keys for forward secrecy
How It Works
RSA-AES Hybrid encryption uses a two-layer approach — asymmetric encryption for securely exchanging a temporary key, and symmetric encryption for fast bulk data encryption. Below is a step-by-step breakdown of the entire process:
1. Key Exchange (One-Time Setup)
Both parties generate their own RSA-2048 key pairs and exchange only their public keys with each other. Private keys are never shared.
- An RSA-2048 key pair is generated on the server side, and the server’s public key is shared with the client.
- An RSA-2048 key pair is generated on the client side, and the client’s public key is shared with the server.
This one-time setup establishes the trust foundation for all subsequent encrypted communication.
2. Sending an Encrypted Request (Client → Server)
When an API request is sent:
- A random session key is generated — A fresh 32-byte AES key is created for each request, ensuring every request uses a unique encryption key.
- The request data is encrypted — The JSON payload is encrypted using AES-256-CBC with the session key. The first 16 bytes of the session key serve as the IV.
- The session key is encrypted — The session key itself is encrypted using the server’s public key with RSA. Only the server can decrypt it using its private key.
- The encrypted data is signed — The encrypted data is signed using the client’s private key. This proves the request originated from the client and has not been tampered with.
- The payload is sent — The encrypted session key, encrypted data, and signature are combined into a single JSON payload and transmitted to the server.
3. Receiving an Encrypted Response (Server → Client)
When a response is sent back:
- The response is encrypted by the server using an AES session key, and the session key is encrypted with the client’s public key — ensuring only the client can decrypt it.
- The encrypted data is signed with the server’s private key — allowing the client to verify its authenticity.
- The session key is decrypted by the client using the client’s private key.
- The signature is verified using the server’s public key to confirm authenticity.
- The response data is decrypted using the decrypted session key.
This ensures that every message is confidential (only the intended recipient can read it), authenticated (signed by the sender), and integrity-protected (any tampering invalidates the signature).
Key Setup
Step 1: Generate Client RSA Key Pair
An RSA-2048 key pair must be generated on the client side. The client’s public key is to be shared with the server. The client’s private key must be kept secret and securely stored.
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.util.Base64;
public class GenerateKeys {
public static void main(String[] args) throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048, SecureRandom.getInstanceStrong());
KeyPair keyPair = keyGen.generateKeyPair();
String publicKey = Base64.getEncoder().encodeToString(
keyPair.getPublic().getEncoded()
);
String privateKey = Base64.getEncoder().encodeToString(
keyPair.getPrivate().getEncoded()
);
// Share publicKey with the server
System.out.println("Client Public Key (share with server): " + publicKey);
// Keep privateKey secret
System.out.println("Client Private Key (keep secret): " + privateKey);
}
}
Step 2: Exchange Public Keys
| Action | Description |
|---|
| Provided by server | Server’s RSA public key — used by the client to encrypt session keys and verify server signatures |
| Provided by client | Client’s RSA public key — used by the server to encrypt response session keys and verify client signatures |
Private keys must never be shared with anyone. Only public keys are exchanged between parties.
Algorithms & Parameters
| Component | Detail |
|---|
| RSA Key Size | 2048 bits |
| RSA Transformation | RSA/ECB/PKCS1Padding |
| RSA Purpose | Encrypting/decrypting the AES session key |
| AES Key Size | 256 bits (32 bytes) |
| AES Transformation | AES/CBC/PKCS5Padding |
| AES IV | First 16 bytes of the session key |
| AES Purpose | Encrypting/decrypting the data payload |
| Signature Algorithm | SHA256withRSA |
| Signature Purpose | Signing and verifying data integrity |
| Base64 Encoding | Standard Base64 (not URL-safe) |
All encrypted requests and responses use the following JSON structure:
Request Payload
{
"encrypted_session_key": "<Base64 encoded RSA-encrypted session key>",
"encrypted_data": "<Base64 encoded AES-encrypted data>",
"signature": "<Base64 encoded digital signature>"
}
Response Payload
{
"encrypted_session_key": "c6+Ma3Crcqr3VGRGseTB9xQIS8fqQxgzUlLpXGEcBO005fKbx+/giMXbvrSPwf7bHHQeTfw6+Iz6TSRUW7Q/W2cqr4SpBvKl6nXqzvaQLYZQ8LPXIFmYuJgjsHUvUqMnbHKTuoxlw1EhCXH/RkutXjTOGUZAOQyqwmnr4iiVxWffI4Agfpc7wKaaobyBLKOYP5shh9YN08qmr9SF1v4ryNs+4fAD9WQIHZKnAf/9wDYf9tCo3wtPFk5tI6sAlZMSBRA0gKGnsv0mHbEh7YJhJghDA2RuN5soVdAe8ZfcgWIbcysJ5xy9YsXxHVwjAH7BBhAyD5XXOhe/yvuFCgzkUA==",
"encrypted_data": "0lLtSONcFxekoWDKy3d4iKoOnJ70X89T7wA/3XeHG1yvYWGP8Mtnv43eggsnM7NhtuM9pZWUHusaeeqWVx/mvTnC9xGFcqGTXqMesjoSGJTt38hY5LNznNgM6jHz69D/9E9uZ8uyVo30ywiwu8edE2Bb0C1r94JxThSnlcsohROuMx4EH1CaMyxhKfHkGasPj8L/rjqcweeOSzt6ART9RfCWsGggCpB63ajXsvaRBbznAdE4sD0qqN5s2mEzM7E48nemvnmEeuGQYETvnv/rbMxc+huFs4txlRHcM0nZgxt4W2Kf26Iy8+44i5rEUHpSdJeo7CYITKuPiqn44H1opAZrFii+3i4HAA7xGOnrQjMdhl9zuld/NkWx/iSvOpg+Io4y0PySf9lwAFUnzszF8/GDyYsCLI44IkU7hgS/AQQSGxbKcxnQWBh2qxD0ezGKnCoKdnVGUutjFpfzVfRzO0GOWRUB2CMsqEEUUmiNC0+Fr7XTFlQh1WJkXkvpUJFtwV3nQ6rLXDXqLndlRm/ORx9+M77/ESpEgelIvDlq6EP5CTYrHXkTTFtNHtFUc+gvc9nhf4Trp509caQ4gT5VraT5ttsCuAUqso/SzTsfWbgsip+lrYs5UDAtHE7lCVzmIKb8awdhClyOZmqJCdvykTGSe1UUh8krB5jOkXhzXZiNGhE+CT9T/qusjHr4d+zTDIzhyb751vQUr5w7cNwFvq3oz594uFxjVf2KpkmddRN14bKdgNw5ZxLI2pBulHEcOPj7yZzaUtkwDNJXg5d9kLoBwMyiK4kF5rWbx8LPZ/Nuks+2j2iQkvqkwQoSGAxYTjEaQB2p7mJ4JV6qlZFFsw==",
"signature": "gw5WO2U03M7hyE6+PluUWeo18B7lQCB7hWwwkum4C/ZtQboKwv51kv8KfK9xIQmL6dMKDnQObGHZtfHE0y2IpxlKekms0t6VjJCSagEdDU9NMOBQtaTuWxU4WExjGUbOgqR/nTA63x03/B70NEWx1mfVR5/4Ekr1orIWuDpKeqkzU0xPVbRDCGYU3OZLrRm1NsPS8gy39/0KV8BWRIvjjsKacXFCMg5pj8ORBYCBLnyXnJem025f8p7XKdpz/31SLYh6ilyght+07SMQwYOKLNPYnYCRNaZTzaYiC2vn8+m02NWSKnGfGqTOk/ShR8ShTP0W2sh0Zd3QXIuoGevGYQ=="
}
| Field | Description |
|---|
encrypted_session_key | The AES session key, encrypted with the client’s public key using RSA. Decrypted using the client’s private key. |
encrypted_data | The response data, encrypted with AES-256-CBC using the session key. |
signature | Digital signature of the encrypted_data string, signed with the server’s private key. Verified using the server’s public key. |
Encryption Flow
Implementation Examples
Encrypting a Request (Client → Server)
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
public class EncryptRequest {
private static final String RSA_ALGORITHM = "RSA";
private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
private static final String AES_ALGORITHM = "AES";
private static final String AES_TRANSFORMATION = "AES/CBC/PKCS5Padding";
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final int AES_KEY_SIZE = 32; // 256 bits
private static final int IV_SIZE = 16; // 128 bits
private static PublicKey loadPublicKey(String base64Key) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
return keyFactory.generatePublic(spec);
}
private static PrivateKey loadPrivateKey(String base64Key) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
return keyFactory.generatePrivate(spec);
}
public static void main(String[] args) {
// Server's public key (provided by server during key exchange)
String serverPublicKeyBase64 = "<SERVER_PUBLIC_KEY>";
// Client's private key (generated by client, kept secret)
String clientPrivateKeyBase64 = "<CLIENT_PRIVATE_KEY>";
String requestJson = "{\"your\": \"request data\"}";
try {
// 1. Generate random AES-256 session key
SecureRandom secureRandom = new SecureRandom();
byte[] sessionKey = new byte[AES_KEY_SIZE];
secureRandom.nextBytes(sessionKey);
// 2. Extract IV from first 16 bytes
byte[] iv = Arrays.copyOfRange(sessionKey, 0, IV_SIZE);
// 3. Encrypt data with AES-256-CBC
SecretKeySpec aesKeySpec = new SecretKeySpec(sessionKey, AES_ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher aesCipher = Cipher.getInstance(AES_TRANSFORMATION);
aesCipher.init(Cipher.ENCRYPT_MODE, aesKeySpec, ivSpec);
byte[] encryptedDataBytes = aesCipher.doFinal(
requestJson.getBytes(StandardCharsets.UTF_8)
);
String encryptedData = Base64.getEncoder().encodeToString(encryptedDataBytes);
// 4. Encrypt session key with server's public key
PublicKey serverPublicKey = loadPublicKey(serverPublicKeyBase64);
Cipher rsaCipher = Cipher.getInstance(RSA_TRANSFORMATION);
rsaCipher.init(Cipher.ENCRYPT_MODE, serverPublicKey);
byte[] encryptedSessionKey = rsaCipher.doFinal(sessionKey);
String sessionKeyBase64 = Base64.getEncoder().encodeToString(encryptedSessionKey);
// 5. Sign encrypted_data with client's private key
PrivateKey clientPrivateKey = loadPrivateKey(clientPrivateKeyBase64);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(clientPrivateKey);
signature.update(encryptedData.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = signature.sign();
String signBase64 = Base64.getEncoder().encodeToString(signatureBytes);
// 6. Build the encrypted payload
// {
// "encrypted_session_key": sessionKeyBase64,
// "encrypted_data": encryptedData,
// "signature": signBase64
// }
} catch (Exception e) {
System.err.println("Encryption failed: " + e.getMessage());
e.printStackTrace();
}
}
}
Steps Breakdown
- Generate Session Key: 32 random bytes are generated using a secure random generator
- Extract IV: The first 16 bytes of the session key are used as the IV
- Encrypt Data: The request JSON is encrypted with AES/CBC/PKCS5Padding using the session key and IV
- Encrypt Session Key: The session key is encrypted with the server’s public key using RSA/ECB/PKCS1Padding
- Sign: The
encrypted_data string (Base64 string, as UTF-8 bytes) is signed with the client’s private key using SHA256withRSA
- Send: All three fields are combined into the JSON payload
Decrypting a Response (Server → Client)
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
public class DecryptResponse {
private static final String RSA_ALGORITHM = "RSA";
private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
private static final String AES_ALGORITHM = "AES";
private static final String AES_TRANSFORMATION = "AES/CBC/PKCS5Padding";
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final int IV_SIZE = 16;
private static PublicKey loadPublicKey(String base64Key) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
return keyFactory.generatePublic(spec);
}
private static PrivateKey loadPrivateKey(String base64Key) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
return keyFactory.generatePrivate(spec);
}
public static void main(String[] args) {
// Server's public key (provided by server during key exchange)
String serverPublicKeyBase64 = "<SERVER_PUBLIC_KEY>";
// Client's private key (generated by client, kept secret)
String clientPrivateKeyBase64 = "<CLIENT_PRIVATE_KEY>";
// Response fields received from server
String encryptedSessionKey = "<encrypted_session_key from response>";
String encryptedData = "<encrypted_data from response>";
String signatureStr = "<signature from response>";
try {
// 1. Decrypt session key with client's private key
PrivateKey clientPrivateKey = loadPrivateKey(clientPrivateKeyBase64);
Cipher rsaCipher = Cipher.getInstance(RSA_TRANSFORMATION);
rsaCipher.init(Cipher.DECRYPT_MODE, clientPrivateKey);
byte[] sessionKey = rsaCipher.doFinal(
Base64.getDecoder().decode(encryptedSessionKey)
);
// 2. Verify signature with server's public key
PublicKey serverPublicKey = loadPublicKey(serverPublicKeyBase64);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(serverPublicKey);
signature.update(encryptedData.getBytes(StandardCharsets.UTF_8));
boolean isValid = signature.verify(
Base64.getDecoder().decode(signatureStr)
);
if (!isValid) {
throw new SecurityException("Signature verification failed!");
}
// 3. Extract IV from session key
byte[] iv = Arrays.copyOfRange(sessionKey, 0, IV_SIZE);
// 4. Decrypt data with AES-256-CBC
SecretKeySpec aesKeySpec = new SecretKeySpec(sessionKey, AES_ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher aesCipher = Cipher.getInstance(AES_TRANSFORMATION);
aesCipher.init(Cipher.DECRYPT_MODE, aesKeySpec, ivSpec);
byte[] decryptedBytes = aesCipher.doFinal(
Base64.getDecoder().decode(encryptedData)
);
String decryptedJson = new String(decryptedBytes, StandardCharsets.UTF_8);
System.out.println(decryptedJson);
} catch (Exception e) {
System.err.println("Decryption failed: " + e.getMessage());
e.printStackTrace();
}
}
}
Steps Breakdown
- Decrypt Session Key: The
encrypted_session_key is decrypted with the client’s private key using RSA/ECB/PKCS1Padding
- Verify Signature: The
signature is verified against the encrypted_data string using the server’s public key with SHA256withRSA. The response should be rejected if verification fails.
- Extract IV: The first 16 bytes of the decrypted session key are used as the IV
- Decrypt Data: The
encrypted_data is decrypted with AES/CBC/PKCS5Padding using the session key and IV
Important Notes
- All Base64 encoding uses standard Base64 (not URL-safe)
- The signature is computed over the Base64-encoded
encrypted_data string, not over the raw encrypted bytes
- The IV is derived from the session key (first 16 bytes) — it is not sent separately in the payload
- The session key is re-encrypted with the client’s public key in the response, allowing the client to decrypt it with its private key
- For POST requests, the server reuses the same AES session key from the request for encrypting the response
- For GET requests/callbacks, a new session key is generated by the server
Security Best Practices
Key Management
- Keys should be generated using cryptographically secure random number generators
- Keys must be stored securely and never logged or transmitted in plain text
- A request can be placed with the tech team to rotate keys
Implementation Guidelines
- The digital signature should always be verified before decrypting the data
- Proper error handling should be implemented to avoid information leakage
- Secure key storage mechanisms must be in place
- Private keys must never be shared or exposed