Why My Game Score Rankings Went Crazy: Lessons from RSA & AES API Encryption
The article recounts a real‑world incident where a game’s leaderboard showed absurd scores due to insecure API parameters, then explains RSA and AES fundamentals, demonstrates how to combine asymmetric and symmetric encryption for secure request handling, and provides Java code, custom annotations, and AOP logic to automate decryption on the server side.
1. The Incident
Before the Chinese New Year, an H5 "Plane Battle" game stored users' infinite‑mode scores via a Body request, making the parameters visible. To protect the API, the client and frontend agreed to send a Base64‑encoded string containing the score, a secret number, and the user ID.
During the holiday, the operations team reported that the leaderboard was wrong: the second place had only about 10,000 points, while the first place showed over 400,000.
After checking the decryption code, the developer realized the user had tampered with the Base64 parameter, breaking the encryption.
2. RSA and AES Basics
2.1 Asymmetric Encryption (RSA)
RSA uses a public key and a private key. Data encrypted with the public key can only be decrypted with the matching private key.
2.2 Symmetric Encryption (AES)
AES uses the same key for encryption and decryption. It is fast but the key must be kept secret.
Common AES modes: AES/CBC/PKCS5Padding or AES/CBC/PKCS7Padding. The IV (initialization vector) should be random for each encryption.
2.3 RSA Padding Modes
ENCRYPTION_OAEP: most secure, recommended. ENCRYPTION_PKCS1: widely used, random padding. ENCRYPTION_NONE: rarely used.
3. Encryption Strategy
Combine RSA and AES to get the best of both worlds:
Encrypt request parameters with AES (fast for large payloads).
Encrypt the AES key, IV, and timestamp with RSA (secure key exchange).
Send both the AES ciphertext ("asy") and the RSA‑encrypted key data ("sym") in the request body.
3.1 Front‑end Process
Generate a random 16‑byte AES key and IV.
Encrypt the real parameters with AES to obtain asy.
Create a JSON object containing key, keyVI, and time, then encrypt it with the server’s RSA public key to obtain sym.
Send {"asy":..., "sym":...} as the request body.
{
"key":"0t7FtCDKofbEVpSZS",
"keyVI":"0t7WESMofbEVpSZS",
"time":211213232323323
}3.2 Back‑end Process
Add two fields to the controller method to receive asy and sym.
Use RequestDecryptionUtil.getRequestDecryption(sym, asy) to obtain the original parameters.
4. Automatic Server‑Side Decryption
Define a custom annotation @RequestRSA and an AOP aspect that intercepts methods annotated with it.
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestRSA {}The aspect extracts the request body, parses asy and sym, decrypts them, and replaces the original method arguments with the decrypted object.
@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 (annotation != null) {
Object data = getParameter(method, joinPoint.getArgs());
String body = JSONObject.toJSONString(data);
JSONObject jsonObject = JSONObject.parseObject(body);
String asy = jsonObject.getString("asy");
String sym = jsonObject.getString("sym");
JSONObject decryption = RequestDecryptionUtil.getRequestDecryption(sym, asy);
Class<?> paramClass = joinPoint.getArgs()[0].getClass();
Object o = JSONObject.parseObject(decryption.toJSONString(), paramClass);
return joinPoint.proceed(new Object[]{o});
}
return joinPoint.proceed();
}
private Object getParameter(Method method, Object[] args) {
// extract the argument annotated with @RequestBody
// (implementation omitted for brevity)
return args[0];
}
}5. Decryption Utilities
RequestDecryptionUtil decrypts the RSA‑encrypted key data, checks request timeout, then uses the extracted AES key and IV to decrypt asy.
public static <T> Object getRequestDecryption(String sym, String asy, Class<T> clazz) {
RSAPrivateKey privateKey = ActivityRSAUtil.getRSAPrivateKeyByString(privateKeyStr);
String rsaJson = ActivityRSAUtil.privateDecrypt(sym, privateKey);
RSADecodeData rsaData = JSONObject.parseObject(rsaJson, RSADecodeData.class);
if (!isWithinTimeout(rsaData.getTime())) {
throw new ServiceException("Request timed out, please try again.");
}
String aesJson = AES256Util.decode(rsaData.getKey(), asy, rsaData.getKeyVI());
return JSONObject.parseObject(aesJson, clazz);
}ActivityRSAUtil provides key‑pair generation, PEM conversion, and RSA encrypt/decrypt methods.
public static String privateDecrypt(String data, RSAPrivateKey privateKey) {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] bytes = rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.getDecoder().decode(data), privateKey.getModulus().bitLength());
return new String(bytes, CHARSET);
}AES256Util implements AES encryption/decryption with CBC mode and PKCS7 padding, supporting custom IVs.
public static String encode(String key, String content, String keyVI) {
SecretKey secretKey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(keyVI.getBytes()));
byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}6. Conclusion
By encrypting request payloads with AES and protecting the AES key with RSA, the API becomes resilient against parameter tampering. The custom @RequestRSA annotation and AOP aspect automate decryption, keeping controller code clean while ensuring security.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
