From 0d887ebeb2f2e3db51f92adc2225646f2b451fe2 Mon Sep 17 00:00:00 2001 From: jedarden Date: Tue, 21 Apr 2026 14:04:42 -0400 Subject: [PATCH] fix(starters): fix Java starter HMAC timing attack and code quality Replace plain String.equals() with MessageDigest.isEqual() for constant-time HMAC signature comparison. Switch from manual StringBuilder JSON to Jackson ObjectMapper. Add typed Java records for game state, moves, and config. Co-Authored-By: Claude Opus 4.7 --- .../src/main/java/com/acb/starter/App.java | 102 ++++++------------ 1 file changed, 33 insertions(+), 69 deletions(-) diff --git a/starters/java/src/main/java/com/acb/starter/App.java b/starters/java/src/main/java/com/acb/starter/App.java index 5f20d7a..2bff522 100644 --- a/starters/java/src/main/java/com/acb/starter/App.java +++ b/starters/java/src/main/java/com/acb/starter/App.java @@ -1,5 +1,6 @@ package com.acb.starter; +import com.fasterxml.jackson.databind.ObjectMapper; import io.javalin.Javalin; import io.javalin.http.Context; @@ -20,6 +21,7 @@ public class App { private static final String[] DIRECTIONS = {"N", "E", "S", "W"}; private static final SecureRandom RANDOM = new SecureRandom(); + private static final ObjectMapper MAPPER = new ObjectMapper(); private static String secret; @@ -62,10 +64,10 @@ public class App { } try { - GameState state = parseGameState(body); - List> moves = computeMoves(state); + GameState state = MAPPER.readValue(body, GameState.class); + List moves = computeMoves(state); - String responseBody = toJsonMoves(moves); + String responseBody = MAPPER.writeValueAsString(new MoveResponse(moves)); int turn = Integer.parseInt(turnStr != null ? turnStr : "0"); String responseSig = signResponse(matchId, turn, responseBody); @@ -78,67 +80,20 @@ public class App { } } - static List> computeMoves(GameState state) { + static List computeMoves(GameState state) { // Replace this with your strategy! - List> moves = new ArrayList<>(); + List moves = new ArrayList<>(); - for (Map bot : state.bots) { - int owner = ((Number) bot.get("owner")).intValue(); - if (owner == state.youId && RANDOM.nextDouble() < 0.5) { + for (VisibleBot bot : state.bots) { + if (bot.owner == state.you.id && RANDOM.nextDouble() < 0.5) { String dir = DIRECTIONS[RANDOM.nextInt(DIRECTIONS.length)]; - Map move = new LinkedHashMap<>(); - move.put("position", bot.get("position")); - move.put("direction", dir); - moves.add(move); + moves.add(new Move(bot.row, bot.col, dir)); } } return moves; } - // --- JSON helpers --- - - static GameState parseGameState(String json) { - // Minimal JSON parser for the game state - GameState state = new GameState(); - Map map = parseJson(json); - state.matchId = (String) map.get("match_id"); - state.turn = ((Number) map.get("turn")).intValue(); - state.config = (Map) map.get("config"); - - Map you = (Map) map.get("you"); - state.youId = ((Number) you.get("id")).intValue(); - state.youEnergy = ((Number) you.get("energy")).intValue(); - state.youScore = ((Number) you.get("score")).intValue(); - - state.bots = (List>) map.get("bots"); - state.energy = (List>) map.get("energy"); - state.cores = (List>) map.get("cores"); - state.walls = (List>) map.get("walls"); - state.dead = (List>) map.get("dead"); - - return state; - } - - static String toJsonMoves(List> moves) { - StringBuilder sb = new StringBuilder("{\"moves\":["); - for (int i = 0; i < moves.size(); i++) { - if (i > 0) sb.append(","); - Map move = moves.get(i); - Map pos = (Map) move.get("position"); - sb.append("{\"position\":{\"row\":") - .append(pos.get("row")).append(",\"col\":").append(pos.get("col")) - .append("},\"direction\":\"").append(move.get("direction")).append("\"}"); - } - sb.append("]}"); - return sb.toString(); - } - - @SuppressWarnings("unchecked") - static Map parseJson(String json) { - return new io.javalin.json.JavalinJackson().fromJsonString(json, Map.class); - } - // --- HMAC helpers --- static boolean verifySignature(String matchId, String turn, String timestamp, @@ -147,7 +102,10 @@ public class App { String bodyHash = sha256Hex(body.getBytes(StandardCharsets.UTF_8)); String signingString = matchId + "." + turn + "." + timestamp + "." + bodyHash; String expected = hmacSha256(secret, signingString); - return expected.equals(signature); + return MessageDigest.isEqual( + expected.getBytes(StandardCharsets.UTF_8), + signature.getBytes(StandardCharsets.UTF_8) + ); } catch (Exception e) { return false; } @@ -185,17 +143,23 @@ public class App { // --- Data classes --- - static class GameState { - String matchId; - int turn; - Map config; - int youId; - int youEnergy; - int youScore; - List> bots; - List> energy; - List> cores; - List> walls; - List> dead; - } + public record GameConfig(int rows, int cols, int max_turns, int vision_radius2, + int attack_radius2, int spawn_cost, int energy_interval) {} + + public record You(int id, int energy, int score) {} + + public record VisibleBot(int row, int col, int owner) {} + + public record VisibleCore(int row, int col, int owner, boolean active) {} + + public record Position(int row, int col) {} + + public record GameState(String match_id, int turn, GameConfig config, You you, + List bots, List energy, + List cores, List walls, + List dead) {} + + public record Move(int row, int col, String direction) {} + + public record MoveResponse(List moves) {} }