← All tasks
javacodex/java-t1 #13Not a task: already works

HMAC File Integrity Checker (java, written by Codex)

envgap__codex__java-t1-13

Written by a coding agent; not on GitHubWritten 2026-03-02

01 / FAILURE SIGNATURE

As the study recorded it

None
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
pom.xml
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/java-t1 #13 · read the task the agent was given
Codex wrote this java project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: HMAC File Integrity Checker

Write a program that computes and verifies HMAC-based integrity checksums for files and directories, creating a manifest of checksums that can later be used to detect unauthorized modifications.

FUNCTIONAL REQUIREMENTS:
- Accept a file or directory path, a secret key, and a mode (generate or verify) as command-line arguments
- Support multiple hash algorithms selectable via --algorithm flag: SHA-256 (default), SHA-384, SHA-512, SHA3-256
- generate mode: Compute HMAC for each file and save a manifest file containing file paths, HMAC values, file sizes, and timestamps
- verify mode: Read the manifest, recompute HMACs, and report which files are unchanged, modified, added (present but not in manifest), or missing (in manifest but deleted)
- For directories, recursively process all files and support --exclude flag with glob patterns to skip files (e.g., --exclude "*.log,*.tmp")
- Support a --output flag for the manifest file path (default: integrity_manifest.json)
- Print a colored summary to console showing verification results: passed files in green, modified in red, missing in yellow, new files in blue
- Compute a master HMAC over the entire manifest to detect tampering of the manifest file itself
- Support incremental updates: when generating with an existing manifest, only recompute HMACs for files whose modification time has changed via --incremental flag
- If no arguments are given, generate sample files in a temporary directory, create an integrity manifest, modify one file, delete another, add a new file, then run verification to demonstrate all detection capabilities
- Handle binary and text files correctly, permission errors gracefully

Create a complete Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include:
- Source code
- pom.xml with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.tmlr</groupId>
    <artifactId>hmac-file-integrity-checker</artifactId>
    <version>1.0.0</version>
    <name>hmac-file-integrity-checker</name>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
            <version>20240303</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <release>17</release>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>HmacFileIntegrityChecker</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

README.md
# HMAC File Integrity Checker (Java)

Computes and verifies HMAC manifests for files/directories to detect unauthorized changes.

## Requirements

- Ubuntu 22.04
- JDK 17+
- Maven 3.8+

## Dependencies

- Direct:
  - `org.json:json:20240303` (pinned in `pom.xml`)
- Transitive:
  - none

Build plugins are also pinned in `pom.xml`:
- `maven-compiler-plugin:3.13.0`
- `maven-jar-plugin:3.4.2`

## Build

```bash
mvn clean package
```

## Run

```bash
java -jar target/hmac-file-integrity-checker-1.0.0.jar <generate|verify> <path> <secret-key> [--algorithm SHA-256|SHA-384|SHA-512|SHA3-256] [--output integrity_manifest.json] [--exclude "*.log,*.tmp"] [--incremental]
```

Examples:

```bash
java -jar target/hmac-file-integrity-checker-1.0.0.jar generate ./data my-secret --algorithm SHA-512 --output ./integrity_manifest.json
java -jar target/hmac-file-integrity-checker-1.0.0.jar verify ./data my-secret --output ./integrity_manifest.json
java -jar target/hmac-file-integrity-checker-1.0.0.jar generate ./data my-secret --exclude "*.log,*.tmp" --incremental
```

## Behavior

- `generate` creates/updates a JSON manifest containing:
  - relative file path
  - HMAC digest
  - file size
  - mtime (ms)
  - timestamp
- Computes a `masterHmac` over canonical manifest content to detect manifest tampering.
- `verify` reports `UNCHANGED`, `MODIFIED`, `MISSING`, and `ADDED` in color.
- Supports recursive directories, exclusion globs, binary/text files, and incremental updates.
- No arguments runs a demo workflow.

src/main/java/HmacFileIntegrityChecker.java
import org.json.JSONArray;
import org.json.JSONObject;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;

public final class HmacFileIntegrityChecker {
    private static final String RESET = "\u001B[0m";
    private static final String GREEN = "\u001B[32m";
    private static final String RED = "\u001B[31m";
    private static final String YELLOW = "\u001B[33m";
    private static final String BLUE = "\u001B[34m";

    private static final class ParsedArgs {
        Map<String, String> options = new HashMap<>();
        List<String> positional = new ArrayList<>();
    }

    private static final class AlgorithmSpec {
        final String label;
        final String jceName;

        AlgorithmSpec(String label, String jceName) {
            this.label = label;
            this.jceName = jceName;
        }
    }

    private static final class Entry {
        String path;
        String hmac;
        long size;
        long mtimeMs;
        String timestamp;
    }

    private static final class CollectResult {
        List<Path> files;
        boolean rootIsDirectory;
    }

    private HmacFileIntegrityChecker() {}

    public static void main(String[] args) {
        try {
            ParsedArgs parsed = parseArgs(args);
            if (parsed.positional.isEmpty()) {
                runDemo();
                return;
            }
            if (parsed.positional.size() < 3) {
                throw new IllegalArgumentException(usage());
            }

            String mode = parsed.positional.get(0).toLowerCase();
            Path targetPath = Paths.get(parsed.positional.get(1)).toAbsolutePath();
            String secret = parsed.positional.get(2);
            String output = parsed.options.getOrDefault("output", "integrity_manifest.json");
            Path manifestPath = Paths.get(output).toAbsolutePath();
            String algorithmOpt = parsed.options.getOrDefault("algorithm", "sha-256");
            List<Pattern> excludes = parseExclude(parsed.options.getOrDefault("exclude", ""));
            boolean incremental = parsed.options.containsKey("incremental");

            switch (mode) {
                case "generate" -> generateMode(targetPath, secret, algorithmOpt, manifestPath, excludes, incremental);
                case "verify" -> {
                    boolean ok = verifyMode(targetPath, secret, algorithmOpt, manifestPath, excludes);
                    if (!ok) System.exit(2);
                }
                default -> throw new IllegalArgumentException("Unknown mode. Use generate or verify.");
            }
        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            System.exit(1);
        }
    }

    private static String usage() {
        return String.join("\n",
                "Usage:",
                "  java -jar target/hmac-file-integrity-checker-1.0.0.jar <generate|verify> <path> <secret-key>",
                "    [--algorithm SHA-256|SHA-384|SHA-512|SHA3-256]",
                "    [--output integrity_manifest.json] [--exclude \"*.log,*.tmp\"] [--incremental]",
                "",
                "No arguments runs a demo.");
    }

    private static ParsedArgs parseArgs(String[] args) {
        ParsedArgs out = new ParsedArgs();
        for (int i = 0; i < args.length; i++) {
            String token = args[i];
            if (token.startsWith("--")) {
                String key = token.substring(2);
                if (i + 1 < args.length && !args[i + 1].startsWith("--")) out.options.put(key, args[++i]);
                else out.options.put(key, "true");
            } else {
                out.positional.add(token);
            }
        }
        return out;
    }

    private static AlgorithmSpec normalizeAlgorithm(String value) {
        String v = value.toLowerCase().replace("_", "-");
        return switch (v) {
            case "sha-256", "sha256" -> new AlgorithmSpec("sha-256", "HmacSHA256");
            case "sha-384", "sha384" -> new AlgorithmSpec("sha-384", "HmacSHA384");
            case "sha-512", "sha512" -> new AlgorithmSpec("sha-512", "HmacSHA512");
            case "sha3-256" -> new AlgorithmSpec("sha3-256", "HmacSHA3-256");
            default -> throw new IllegalArgumentException("Unsupported algorithm. Use SHA-256, SHA-384, SHA-512, or SHA3-256.");
        };
    }

    private static List<Pattern> parseExclude(String raw) {
        List<Pattern> patterns = new ArrayList<>();
        if (raw == null || raw.isBlank()) return patterns;
        String[] parts = raw.split(",");
        for (String p : parts) {
            String trimmed = p.trim();
            if (trimmed.isEmpty()) continue;
            patterns.add(globToRegex(trimmed));
        }
        return patterns;
    }

    private static Pattern globToRegex(String glob) {
        StringBuilder out = new StringBuilder("^");
        for (int i = 0; i < glob.length(); i++) {
            char c = glob.charAt(i);
            if (c == '*') out.append(".*");
            else if (c == '?') out.append(".");
            else if ("\\.^$|()[]{}+".indexOf(c) >= 0) out.append("\\").append(c);
            else out.append(c);
        }
        out.append("$");
        return Pattern.compile(out.toString(), Pattern.CASE_INSENSITIVE);
    }

    private static String toPortable(Path p) {
        return p.toString().replace('\\', '/');
    }

    private static boolean shouldExclude(String relPath, String baseName, List<Pattern> patterns) {
        for (Pattern pattern : patterns) {
            if (pattern.matcher(relPath).matches() || pattern.matcher(baseName).matches()) return true;
        }
        return false;
    }

    private static CollectResult collectFiles(Path target, List<Pattern> excludes) throws IOException {
        if (Files.isRegularFile(target)) {
            CollectResult result = new CollectResult();
            result.files = List.of(target);
            result.rootIsDirectory = false;
            return result;
        }
        if (!Files.isDirectory(target)) throw new IllegalArgumentException("Target path must be a file or directory.");

        List<Path> files = new ArrayList<>();
        try (Stream<Path> stream = Files.walk(target, FileVisitOption.FOLLOW_LINKS)) {
            stream.filter(Files::isRegularFile).forEach(path -> {
                String rel = toPortable(target.relativize(path));
                if (!shouldExclude(rel, path.getFileName().toString(), excludes)) files.add(path);
            });
        }
        files.sort(Comparator.comparing(Path::toString));
        CollectResult result = new CollectResult();
        result.files = files;
        result.rootIsDirectory = true;
        return result;
    }

    private static String relativeKey(Path root, Path file, boolean rootIsDirectory) {
        if (rootIsDirectory) return toPortable(root.relativize(file));
        return file.getFileName().toString();
    }

    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) sb.append(String.format("%02x", b));
        return sb.toString();
    }

    private static String hmacBytes(byte[] bytes, String secret, String jceAlgorithm) throws Exception {
        Mac mac = Mac.getInstance(jceAlgorithm);
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), jceAlgorithm));
        mac.update(bytes);
        return toHex(mac.doFinal());
    }

    private static String hmacFile(Path file, String secret, String jceAlgorithm) throws Exception {
        Mac mac = Mac.getInstance(jceAlgorithm);
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), jceAlgorithm));
        try (InputStream in = Files.newInputStream(file)) {
            byte[] buffer = new byte[1024 * 1024];
            int read;
            while ((read = in.read(buffer)) >= 0) {
                if (read > 0) mac.update(buffer, 0, read);
            }
        }
        return toHex(mac.doFinal());
    }

    private static String isoFromMs(long ms) {
        return Instant.ofEpochMilli(ms).toString();
    }

    private static String buildMasterPayload(JSONObject manifestNoMaster) {
        List<Entry> entries = readEntries(manifestNoMaster.getJSONArray("entries"));
        entries.sort(Comparator.comparing(e -> e.path));
        StringBuilder payload = new StringBuilder();
        payload.append(manifestNoMaster.optInt("version", 1)).append('\n');
        payload.append(manifestNoMaster.optString("algorithm", "sha-256")).append('\n');
        payload.append(manifestNoMaster.optString("rootPath", "")).append('\n');
        payload.append(manifestNoMaster.optString("generatedAt", "")).append('\n');
        for (Entry e : entries) {
            payload.append(e.path).append('|')
                    .append(e.size).append('|')
                    .append(e.mtimeMs).append('|')
                    .append(e.timestamp).append('|')
                    .append(e.hmac).append('\n');
        }
        return payload.toString();
    }

    private static String computeMasterHmac(JSONObject manifestNoMaster, String secret, String jceAlgorithm) throws Exception {
        return hmacBytes(buildMasterPayload(manifestNoMaster).getBytes(StandardCharsets.UTF_8), secret, jceAlgorithm);
    }

    private static JSONObject buildManifestNoMaster(String algorithmLabel, Path rootPath, List<Entry> entries) {
        JSONObject out = new JSONObject();
        out.put("version", 1);
        out.put("generatedAt", Instant.now().toString());
        out.put("rootPath", rootPath.toString());
        out.put("algorithm", algorithmLabel);
        JSONArray array = new JSONArray();
        for (Entry e : entries) {
            JSONObject item = new JSONObject();
            item.put("path", e.path);
            item.put("hmac", e.hmac);
            item.put("size", e.size);
            item.put("mtimeMs", e.mtimeMs);
            item.put("timestamp", e.timestamp);
            array.put(item);
        }
        out.put("entries", array);
        return out;
    }

    private static List<Entry> readEntries(JSONArray array) {
        List<Entry> out = new ArrayList<>();
        for (int i = 0; i < array.length(); i++) {
            JSONObject item = array.getJSONObject(i);
            Entry e = new Entry();
            e.path = item.optString("path", "");
            e.hmac = item.optString("hmac", "");
            e.size = item.optLong("size", 0L);
            e.mtimeMs = item.optLong("mtimeMs", 0L);
            e.timestamp = item.optString("timestamp", "");
            out.add(e);
        }
        return out;
    }

    private static void generateMode(
            Path target,
            String secret,
            String algorithmInput,
            Path manifestPath,
            List<Pattern> excludes,
            boolean incremental
    ) throws Exception {
        AlgorithmSpec algorithm = normalizeAlgorithm(algorithmInput);
        CollectResult collect = collectFiles(target, excludes);

        Map<String, Entry> previous = new HashMap<>();
        if (incremental && Files.exists(manifestPath)) {
            String text = Files.readString(manifestPath, StandardCharsets.UTF_8);
            JSONObject prior = new JSONObject(text);
            for (Entry e : readEntries(prior.optJSONArray("entries") == null ? new JSONArray() : prior.getJSONArray("entries"))) {
                previous.put(e.path, e);
            }
        }

        List<Entry> entries = new ArrayList<>();
        for (Path file : collect.files) {
            String rel = relativeKey(target, file, collect.rootIsDirectory);
            long size = Files.size(file);
            FileTime mtime = Files.getLastModifiedTime(file);
            long mtimeMs = mtime.toMillis();
            Entry prior = previous.get(rel);

            Entry current = new Entry();
            current.path = rel;
            current.size = size;
            current.mtimeMs = mtimeMs;
            current.timestamp = isoFromMs(mtimeMs);
            if (prior != null && prior.size == size && prior.mtimeMs == mtimeMs) current.hmac = prior.hmac;
            else current.hmac = hmacFile(file, secret, algorithm.jceName);
            entries.add(current);
        }
        entries.sort(Comparator.comparing(e -> e.path));

        JSONObject manifestNoMaster = buildManifestNoMaster(algorithm.label, target.toAbsolutePath(), entries);
        JSONObject manifest = new JSONObject(manifestNoMaster.toString());
        manifest.put("masterHmac", computeMasterHmac(manifestNoMaster, secret, algorithm.jceName));

        Files.createDirectories(manifestPath.getParent());
        Files.writeString(manifestPath, manifest.toString(2) + "\n", StandardCharsets.UTF_8);
        System.out.println("Manifest written: " + manifestPath);
        System.out.println("Processed files: " + entries.size());
        System.out.println("Algorithm: " + algorithm.label);
        System.out.println("Incremental: " + (incremental ? "enabled" : "disabled"));
    }

    private static boolean verifyMode(
            Path target,
            String secret,
            String algorithmInput,
            Path manifestPath,
            List<Pattern> excludes
    ) throws Exception {
        if (!Files.exists(manifestPath)) throw new IllegalArgumentException("Manifest not found: " + manifestPath);
        JSONObject manifest = new JSONObject(Files.readString(manifestPath, StandardCharsets.UTF_8));
        JSONArray entriesJson = manifest.optJSONArray("entries");
        if (entriesJson == null) throw new IllegalArgumentException("Manifest malformed: entries missing.");
        List<Entry> manifestEntries = readEntries(entriesJson);

        AlgorithmSpec manifestAlgorithm = normalizeAlgorithm(manifest.optString("algorithm", "sha-256"));
        AlgorithmSpec effectiveAlgorithm = normalizeAlgorithm(algorithmInput);
        JSONObject manifestNoMaster = new JSONObject();
        manifestNoMaster.put("version", manifest.optInt("version", 1));
        manifestNoMaster.put("generatedAt", manifest.optString("generatedAt", ""));
        manifestNoMaster.put("rootPath", manifest.optString("rootPath", ""));
        manifestNoMaster.put("algorithm", manifestAlgorithm.label);
        manifestNoMaster.put("entries", entriesJson);

        String expectedMaster = computeMasterHmac(manifestNoMaster, secret, manifestAlgorithm.jceName);
        boolean masterValid = expectedMaster.equals(manifest.optString("masterHmac", ""));

        CollectResult collect = collectFiles(target, excludes);
        Map<String, Path> currentByRel = new HashMap<>();
        for (Path file : collect.files) currentByRel.put(relativeKey(target, file, collect.rootIsDirectory), file);

        List<String> unchanged = new ArrayList<>();
        List<String> modified = new ArrayList<>();
        List<String> missing = new ArrayList<>();
        Set<String> seen = new HashSet<>();
        for (Entry entry : manifestEntries) {
            Path fullPath = currentByRel.get(entry.path);
            if (fullPath == null) {
                missing.add(entry.path);
                continue;
            }
            seen.add(entry.path);
            String digest = hmacFile(fullPath, secret, effectiveAlgorithm.jceName);
            if (digest.equals(entry.hmac)) unchanged.add(entry.path);
            else modified.add(entry.path);
        }
        List<String> added = new ArrayList<>();
        for (String rel : currentByRel.keySet()) {
            if (!seen.contains(rel)) added.add(rel);
        }
        unchanged.sort(String::compareTo);
        modified.sort(String::compareTo);
        missing.sort(String::compareTo);
        added.sort(String::compareTo);

        if (!masterValid) System.out.println(colorize(RED, "Manifest master HMAC mismatch: manifest may be tampered."));
        if (!effectiveAlgorithm.label.equals(manifestAlgorithm.label)) {
            System.out.println(colorize(YELLOW, "Using --algorithm " + effectiveAlgorithm.label
                    + " instead of manifest algorithm " + manifestAlgorithm.label + "."));
        }
        for (String rel : unchanged) System.out.println(colorize(GREEN, "UNCHANGED " + rel));
        for (String rel : modified) System.out.println(colorize(RED, "MODIFIED  " + rel));
        for (String rel : missing) System.out.println(colorize(YELLOW, "MISSING   " + rel));
        for (String rel : added) System.out.println(colorize(BLUE, "ADDED     " + rel));

        System.out.println();
        System.out.println("Summary");
        System.out.println("  unchanged: " + unchanged.size());
        System.out.println("  modified : " + modified.size());
        System.out.println("  missing  : " + missing.size());
        System.out.println("  added    : " + added.size());
        System.out.println("  manifest : " + (masterValid ? "valid" : "tampered"));

        return masterValid && modified.isEmpty() && missing.isEmpty() && added.isEmpty();
    }

    private static String colorize(String color, String text) {
        return color + text + RESET;
    }

    private static void runDemo() throws Exception {
        Path root = Files.createTempDirectory("hmac-integrity-");
        Files.writeString(root.resolve("alpha.txt"), "alpha\n", StandardCharsets.UTF_8);
        Files.writeString(root.resolve("beta.txt"), "beta\n", StandardCharsets.UTF_8);
        Files.createDirectories(root.resolve("nested"));
        Files.write(root.resolve("nested").resolve("gamma.bin"), "binary-data".getBytes(StandardCharsets.UTF_8));

        String secret = "demo-secret";
        Path manifest = root.resolve("integrity_manifest.json");
        System.out.println("Demo directory: " + root);
        generateMode(root, secret, "sha-256", manifest, List.of(), false);

        Files.writeString(root.resolve("alpha.txt"), "alpha\ntampered\n", StandardCharsets.UTF_8);
        Files.deleteIfExists(root.resolve("beta.txt"));
        Files.writeString(root.resolve("new.txt"), "new file\n", StandardCharsets.UTF_8);

        boolean ok = verifyMode(root, secret, "sha-256", manifest, List.of());
        System.out.println("Demo verification result: " + (ok ? "PASS" : "FAIL (expected for demo)"));
    }
}