Implementing RSA and AES Hybrid Encryption for API Security in Java
This article describes a real‑world API security incident, explains the fundamentals of asymmetric RSA and symmetric AES encryption, and provides a complete Java implementation—including a hybrid encryption strategy, custom @RequestRSA annotation, Spring AOP decryption aspect, and utility classes—to securely transmit and automatically decrypt request parameters.
The author recounts an incident where a game’s leaderboard showed incorrect scores because a user tampered with the Base64‑encoded request parameters, highlighting the need for stronger API protection.
To address this, a hybrid encryption scheme is introduced: AES is used for fast symmetric encryption of the actual payload, while RSA encrypts the AES key, IV, and timestamp, combining the speed of symmetric cryptography with the security of asymmetric cryptography.
RSA and AES basics
RSA (asymmetric) uses a public key for encryption and a private key for decryption; it is secure but slow. AES (symmetric) uses the same key for encryption and decryption; it is fast but the key must be kept secret.
Common RSA padding modes are ENCRYPTION_OAEP , ENCRYPTION_PKCS1 and ENCRYPTION_NONE . Java’s default implementation is RSA/None/PKCS1Padding . For AES, typical modes include AES/CBC/PKCS5Padding or AES/CBC/PKCS7Padding , with block‑cipher modes such as ECB, CBC, CTR, CFB, OFB.
Encryption strategy
When the request contains many parameters, encrypt the payload with AES to avoid RSA’s performance penalty.
Encrypt the AES key, IV and timestamp with RSA and send the ciphertext (named sym ) together with the AES‑encrypted payload (named asy ).
The client sends both sym and asy in the request body.
Client‑side implementation (JSON example)
{
"key":"0t7FtCDKofbEVpSZS",
"keyVI":"0t7WESMofbEVpSZS",
"time":211213232323323
}
// Convert to JSON string before encryptionServer‑side decryption workflow
Define a custom annotation @RequestRSA to mark endpoints that require decryption.
Create an AOP aspect ( RequestRSAAspect ) that intercepts annotated methods, extracts sym and asy from the request body, and calls RequestDecryptionUtil.getRequestDecryption to obtain the original parameters.
Replace the original method arguments with the decrypted object and proceed with the method execution.
Custom annotation
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestRSA {}AOP aspect
import com.alibaba.fastjson.JSONObject;
import app.activity.common.interceptor.RequestRSA;
import app.activity.util.RequestDecryptionUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Aspect
@Component
@Order(2)
@Slf4j
public class RequestRSAAspect {
@Pointcut("execution(public * app.activity.controller.*.*(..))")
public void requestRAS() {}
@Around("requestRAS()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
RequestRSA annotation = method.getAnnotation(RequestRSA.class);
if (Objects.nonNull(annotation)) {
Object data = getParameter(method, joinPoint.getArgs());
String body = JSONObject.toJSONString(data);
JSONObject jsonObject = JSONObject.parseObject(body);
String asy = jsonObject.get("asy").toString();
String sym = jsonObject.get("sym").toString();
JSONObject decryption = RequestDecryptionUtil.getRequestDecryption(sym, asy);
Class
paramClass = joinPoint.getArgs()[0].getClass();
Object obj = JSONObject.parseObject(decryption.toJSONString(), paramClass);
return joinPoint.proceed(new Object[]{obj});
}
return joinPoint.proceed();
}
private Object getParameter(Method method, Object[] args) {
List
argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
if (requestBody != null) {
argList.add(args[i]);
}
}
if (argList.isEmpty()) return null;
return argList.size() == 1 ? argList.get(0) : argList;
}
}Decryption utility
public class RequestDecryptionUtil {
private static final String publicKey = "RSA生成的公钥";
private static final String privateKey = "RSA生成的私钥";
private static final Integer timeout = 60000;
public static
Object getRequestDecryption(String sym, String asy, Class
clazz) {
try {
RSAPrivateKey rsaPrivateKey = ActivityRSAUtil.getRSAPrivateKeyByString(privateKey);
String RSAJson = ActivityRSAUtil.privateDecrypt(sym, rsaPrivateKey);
RSADecodeData rsaDecodeData = JSONObject.parseObject(RSAJson, RSADecodeData.class);
boolean isTimeout = Objects.nonNull(rsaDecodeData) && Objects.nonNull(rsaDecodeData.getTime())
&& System.currentTimeMillis() - rsaDecodeData.getTime() < timeout;
if (!isTimeout) {
throw new ServiceException("Request timed out, please try again.");
}
String AESJson = AES256Util.decode(rsaDecodeData.getKey(), asy, rsaDecodeData.getKeyVI());
return JSONObject.parseObject(AESJson, clazz);
} catch (Exception e) {
throw new RuntimeException("RSA decryption Exception: " + e.getMessage());
}
}
public static JSONObject getRequestDecryption(String sym, String asy) {
// similar logic without generic type
...
}
}RSA utility class (ActivityRSAUtil)
public class ActivityRSAUtil {
public static final String CHARSET = "UTF-8";
public static KeyPair getKeyPair(int keyLength) { /* generate RSA key pair */ }
public static byte[] getPublicKey(KeyPair kp) { return ((RSAPublicKey) kp.getPublic()).getEncoded(); }
public static byte[] getPrivateKey(KeyPair kp) { return ((RSAPrivateKey) kp.getPrivate()).getEncoded(); }
public static String publicEncrypt(String data, RSAPublicKey pub) { /* RSA encrypt */ }
public static String privateDecrypt(String data, RSAPrivateKey priv) { /* RSA decrypt */ }
// Additional methods for key conversion and split‑codec handling
...
}AES utility class (AES256Util)
public class AES256Util {
private static final String AES = "AES";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
static { Security.setProperty("crypto.policy", "unlimited"); Security.addProvider(new BouncyCastleProvider()); }
public static String encode(String key, String content, String iv) { /* AES encrypt */ }
public static String decode(String key, String content, String iv) { /* AES decrypt */ }
// Additional ECB mode helpers omitted for brevity
...
}By combining RSA and AES as described, the API achieves both confidentiality and integrity while maintaining acceptable performance, and the server can automatically decrypt incoming requests using the custom annotation and AOP mechanism.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.