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.

Programmer DD
Programmer DD
Programmer DD
Why My Game Score Rankings Went Crazy: Lessons from RSA & AES API Encryption

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.

Leaderboard screenshot
Leaderboard screenshot
Score discrepancy screenshot
Score discrepancy screenshot

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.

backendJavaRSAencryptionAPI securityAES
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.