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

TOTP Generator (java, written by Codex)

envgap__codex__java-t1-14

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 #14 · 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: TOTP Generator

Write a program that generates and validates Time-based One-Time Passwords (TOTP) compatible with RFC 6238, supporting secret key management, QR code URI generation, and multi-account storage.

FUNCTIONAL REQUIREMENTS:
- Support subcommands: generate (create a new TOTP secret), code (show current OTP code), verify (check if a code is valid), and list (show all stored accounts)
- generate: Create a cryptographically random base32-encoded secret key of configurable length (--length flag, default 20 bytes)
- code: Display the current 6-digit TOTP code for a given account, with a countdown timer showing seconds until the code expires (30-second default period)
- verify: Accept a 6-digit code and check if it matches the current or adjacent time windows (configurable drift tolerance via --drift flag, default 1 window)
- Support configurable TOTP parameters: digit count (6 or 8), time period (30 or 60 seconds), and hash algorithm (SHA-1, SHA-256, SHA-512)
- Generate otpauth:// URIs compatible with authenticator apps (Google Authenticator, Authy)
- Store account secrets in an encrypted local JSON file using a master password
- Support multiple accounts with labels (--account flag with issuer:username format)
- Print the current code, remaining seconds, and next code to console
- If no arguments are given, generate a demo account with a random secret, display the current code and the otpauth:// URI, verify the current code, then show what happens with an incorrect code
- Handle errors: invalid base32 secrets, expired codes, duplicate account names, wrong master password

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>totp-generator</artifactId>
    <version>1.0.0</version>
    <name>totp-generator</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>TotpGenerator</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

README.md
# TOTP Generator (Java)

RFC 6238-compatible TOTP generator/verifier with encrypted multi-account storage.

## Requirements

- Ubuntu 22.04
- JDK 17+
- Maven 3.8+

## Dependencies

- Direct:
  - `org.json:json:20240303`
- Transitive:
  - none

Build plugins (pinned):
- `maven-compiler-plugin:3.13.0`
- `maven-jar-plugin:3.4.2`

## Build

```bash
mvn clean package
```

## Run

```bash
java -jar target/totp-generator-1.0.0.jar generate --master "<password>" --account "Issuer:user@example.com" [--length 20] [--digits 6|8] [--period 30|60] [--algorithm SHA-1|SHA-256|SHA-512] [--store ./totp_accounts.enc.json]
java -jar target/totp-generator-1.0.0.jar code --master "<password>" --account "Issuer:user@example.com" [--store ./totp_accounts.enc.json]
java -jar target/totp-generator-1.0.0.jar verify --master "<password>" --account "Issuer:user@example.com" --code 123456 [--drift 1] [--store ./totp_accounts.enc.json]
java -jar target/totp-generator-1.0.0.jar list --master "<password>" [--store ./totp_accounts.enc.json]
```

## Features

- Subcommands: `generate`, `code`, `verify`, `list`
- Configurable TOTP parameters (digits, period, SHA-1/256/512)
- `otpauth://` URI generation for authenticator apps
- Encrypted JSON storage with PBKDF2-SHA256 + AES-GCM
- Duplicate account and wrong master password handling
- No-args demo mode

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

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public final class TotpGenerator {
    private static final Path DEFAULT_STORE = Paths.get("totp_accounts.enc.json").toAbsolutePath();
    private static final int PBKDF2_ITERS = 150_000;
    private static final SecureRandom RNG = new SecureRandom();
    private static final String BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

    private record ParsedArgs(Map<String, String> options, List<String> positional) {}

    private record AlgoSpec(String label, String hmacJce) {}

    private TotpGenerator() {}

    public static void main(String[] args) {
        try {
            ParsedArgs parsed = parseArgs(args);
            if (parsed.positional.isEmpty()) {
                runDemo();
                return;
            }
            String cmd = parsed.positional.get(0).toLowerCase();
            switch (cmd) {
                case "generate" -> commandGenerate(parsed.options);
                case "code" -> commandCode(parsed.options);
                case "verify" -> commandVerify(parsed.options);
                case "list" -> commandList(parsed.options);
                default -> throw new IllegalArgumentException("Unknown subcommand.\n" + usage());
            }
        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            System.exit(1);
        }
    }

    private static String usage() {
        return String.join("\n",
                "Usage:",
                "  generate --master <password> --account issuer:username [--length 20] [--digits 6|8] [--period 30|60] [--algorithm SHA-1|SHA-256|SHA-512] [--store path]",
                "  code --master <password> --account issuer:username [--store path]",
                "  verify --master <password> --account issuer:username --code 123456 [--drift 1] [--store path]",
                "  list --master <password> [--store path]",
                "",
                "No arguments runs a demo workflow.");
    }

    private static ParsedArgs parseArgs(String[] args) {
        Map<String, String> options = new HashMap<>();
        List<String> positional = new ArrayList<>();
        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("--")) options.put(key, args[++i]);
                else options.put(key, "true");
            } else {
                positional.add(token);
            }
        }
        return new ParsedArgs(options, positional);
    }

    private static String requireOption(Map<String, String> options, String key) {
        String value = options.get(key);
        if (value == null || value.isBlank()) throw new IllegalArgumentException("Missing --" + key);
        return value;
    }

    private static AlgoSpec normalizeAlgorithm(String raw) {
        String value = raw.toUpperCase();
        return switch (value) {
            case "SHA-1" -> new AlgoSpec("SHA-1", "HmacSHA1");
            case "SHA-256" -> new AlgoSpec("SHA-256", "HmacSHA256");
            case "SHA-512" -> new AlgoSpec("SHA-512", "HmacSHA512");
            default -> throw new IllegalArgumentException("Unsupported algorithm. Use SHA-1, SHA-256, or SHA-512.");
        };
    }

    private static int parseDigits(String raw) {
        int v = Integer.parseInt(raw);
        if (v != 6 && v != 8) throw new IllegalArgumentException("Digits must be 6 or 8.");
        return v;
    }

    private static int parsePeriod(String raw) {
        int v = Integer.parseInt(raw);
        if (v != 30 && v != 60) throw new IllegalArgumentException("Period must be 30 or 60.");
        return v;
    }

    private static int parseDrift(String raw) {
        int v = Integer.parseInt(raw);
        if (v < 0 || v > 10) throw new IllegalArgumentException("Drift must be in range 0..10.");
        return v;
    }

    private static String validateAccountLabel(String account) {
        if (!account.contains(":")) throw new IllegalArgumentException("Account label must be issuer:username.");
        return account;
    }

    private static String base32Encode(byte[] bytes) {
        int bits = 0;
        int value = 0;
        StringBuilder out = new StringBuilder();
        for (byte b : bytes) {
            value = (value << 8) | (b & 0xFF);
            bits += 8;
            while (bits >= 5) {
                out.append(BASE32_ALPHABET.charAt((value >>> (bits - 5)) & 31));
                bits -= 5;
            }
        }
        if (bits > 0) out.append(BASE32_ALPHABET.charAt((value << (5 - bits)) & 31));
        return out.toString();
    }

    private static byte[] base32Decode(String secret) {
        String clean = secret.toUpperCase().replace("=", "").replaceAll("\\s+", "");
        if (clean.isEmpty()) throw new IllegalArgumentException("Invalid base32 secret.");
        int bits = 0;
        int value = 0;
        List<Byte> out = new ArrayList<>();
        for (char c : clean.toCharArray()) {
            int idx = BASE32_ALPHABET.indexOf(c);
            if (idx < 0) throw new IllegalArgumentException("Invalid base32 secret.");
            value = (value << 5) | idx;
            bits += 5;
            if (bits >= 8) {
                out.add((byte) ((value >>> (bits - 8)) & 0xFF));
                bits -= 8;
            }
        }
        byte[] result = new byte[out.size()];
        for (int i = 0; i < out.size(); i++) result[i] = out.get(i);
        return result;
    }

    private static String formatCode(int code, int digits) {
        return String.format("%0" + digits + "d", code);
    }

    private static String totpAt(String secretB32, int digits, String hmacJce, long counter) throws Exception {
        byte[] secret = base32Decode(secretB32);
        byte[] msg = new byte[8];
        for (int i = 7; i >= 0; i--) {
            msg[i] = (byte) (counter & 0xFF);
            counter >>>= 8;
        }
        Mac mac = Mac.getInstance(hmacJce);
        mac.init(new SecretKeySpec(secret, hmacJce));
        byte[] digest = mac.doFinal(msg);
        int offset = digest[digest.length - 1] & 0x0F;
        int binary = ((digest[offset] & 0x7F) << 24)
                | ((digest[offset + 1] & 0xFF) << 16)
                | ((digest[offset + 2] & 0xFF) << 8)
                | (digest[offset + 3] & 0xFF);
        return formatCode(binary % (int) Math.pow(10, digits), digits);
    }

    private static JSONObject totpNow(String secret, int digits, int period, String hmacJce) throws Exception {
        long now = Instant.now().getEpochSecond();
        long counter = now / period;
        int remaining = (int) (period - (now % period));
        JSONObject out = new JSONObject();
        out.put("current", totpAt(secret, digits, hmacJce, counter));
        out.put("next", totpAt(secret, digits, hmacJce, counter + 1));
        out.put("remaining", remaining);
        out.put("counter", counter);
        return out;
    }

    private static Optional<Integer> verifyTotp(
            String secret,
            String code,
            int digits,
            int period,
            String hmacJce,
            int drift
    ) throws Exception {
        if (!code.matches("^\\d{6,8}$")) throw new IllegalArgumentException("Code must be 6 or 8 digits.");
        long now = Instant.now().getEpochSecond();
        long counter = now / period;
        for (int d = -drift; d <= drift; d++) {
            if (totpAt(secret, digits, hmacJce, counter + d).equals(code)) return Optional.of(d);
        }
        return Optional.empty();
    }

    private static String otpauthUri(String account, String secret, int digits, int period, String algoLabel) {
        String[] parts = account.split(":", 2);
        String issuer = parts[0];
        String username = parts[1];
        String label = urlEncode(issuer + ":" + username);
        String query = "secret=" + urlEncode(secret)
                + "&issuer=" + urlEncode(issuer)
                + "&algorithm=" + urlEncode(algoLabel.replace("-", ""))
                + "&digits=" + digits
                + "&period=" + period;
        return "otpauth://totp/" + label + "?" + query;
    }

    private static String urlEncode(String value) {
        return java.net.URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
    }

    private static SecretKey deriveKey(char[] master, byte[] salt) throws GeneralSecurityException {
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        PBEKeySpec spec = new PBEKeySpec(master, salt, PBKDF2_ITERS, 256);
        byte[] key = factory.generateSecret(spec).getEncoded();
        return new SecretKeySpec(key, "AES");
    }

    private static JSONObject encryptDb(String plainJson, String master) throws Exception {
        byte[] salt = new byte[16];
        byte[] nonce = new byte[12];
        RNG.nextBytes(salt);
        RNG.nextBytes(nonce);
        SecretKey key = deriveKey(master.toCharArray(), salt);
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, nonce));
        byte[] ciphertext = cipher.doFinal(plainJson.getBytes(StandardCharsets.UTF_8));

        JSONObject out = new JSONObject();
        out.put("version", 1);
        out.put("kdf", "PBKDF2-SHA256");
        out.put("iterations", PBKDF2_ITERS);
        out.put("salt", Base64.getEncoder().encodeToString(salt));
        out.put("nonce", Base64.getEncoder().encodeToString(nonce));
        out.put("data", Base64.getEncoder().encodeToString(ciphertext));
        return out;
    }

    private static String decryptDb(JSONObject blob, String master) throws Exception {
        try {
            byte[] salt = Base64.getDecoder().decode(blob.getString("salt"));
            byte[] nonce = Base64.getDecoder().decode(blob.getString("nonce"));
            byte[] data = Base64.getDecoder().decode(blob.getString("data"));
            SecretKey key = deriveKey(master.toCharArray(), salt);
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, nonce));
            return new String(cipher.doFinal(data), StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new IllegalArgumentException("Wrong master password or corrupted encrypted store.");
        }
    }

    private static JSONObject loadStore(Path store, String master) throws Exception {
        if (!Files.exists(store)) {
            JSONObject db = new JSONObject();
            db.put("version", 1);
            db.put("accounts", new JSONArray());
            return db;
        }
        String raw = Files.readString(store, StandardCharsets.UTF_8);
        JSONObject enc = new JSONObject(raw);
        JSONObject db = new JSONObject(decryptDb(enc, master));
        if (!db.has("accounts") || !db.get("accounts").getClass().equals(JSONArray.class)) {
            throw new IllegalArgumentException("Encrypted store is malformed.");
        }
        return db;
    }

    private static void saveStore(Path store, String master, JSONObject db) throws Exception {
        JSONObject enc = encryptDb(db.toString(2), master);
        Files.createDirectories(store.getParent());
        Files.writeString(store, enc.toString(2) + "\n", StandardCharsets.UTF_8);
    }

    private static JSONObject getAccount(JSONObject db, String label) {
        JSONArray arr = db.getJSONArray("accounts");
        for (int i = 0; i < arr.length(); i++) {
            JSONObject acc = arr.getJSONObject(i);
            if (label.equals(acc.getString("label"))) return acc;
        }
        throw new IllegalArgumentException("Account not found: " + label);
    }

    private static void commandGenerate(Map<String, String> options) throws Exception {
        String master = requireOption(options, "master");
        String account = validateAccountLabel(requireOption(options, "account"));
        int length = Integer.parseInt(options.getOrDefault("length", "20"));
        if (length < 10 || length > 128) throw new IllegalArgumentException("Length must be between 10 and 128 bytes.");
        int digits = parseDigits(options.getOrDefault("digits", "6"));
        int period = parsePeriod(options.getOrDefault("period", "30"));
        AlgoSpec algo = normalizeAlgorithm(options.getOrDefault("algorithm", "SHA-1"));
        Path store = Paths.get(options.getOrDefault("store", DEFAULT_STORE.toString())).toAbsolutePath();

        JSONObject db = loadStore(store, master);
        JSONArray accounts = db.getJSONArray("accounts");
        for (int i = 0; i < accounts.length(); i++) {
            if (account.equals(accounts.getJSONObject(i).getString("label"))) {
                throw new IllegalArgumentException("Duplicate account: " + account);
            }
        }

        byte[] secretBytes = new byte[length];
        RNG.nextBytes(secretBytes);
        String secret = base32Encode(secretBytes);
        JSONObject item = new JSONObject();
        item.put("label", account);
        item.put("secret", secret);
        item.put("digits", digits);
        item.put("period", period);
        item.put("algorithm", algo.label);
        item.put("createdAt", Instant.now().toString());
        accounts.put(item);
        saveStore(store, master, db);

        String uri = otpauthUri(account, secret, digits, period, algo.label);
        JSONObject view = totpNow(secret, digits, period, algo.hmacJce);
        System.out.println("Account added : " + account);
        System.out.println("Store file    : " + store);
        System.out.println("Secret (base32): " + secret);
        System.out.println("otpauth URI   : " + uri);
        System.out.println("Current code  : " + view.getString("current"));
        System.out.println("Next code     : " + view.getString("next"));
        System.out.println("Expires in    : " + view.getInt("remaining") + "s");
    }

    private static void commandCode(Map<String, String> options) throws Exception {
        String master = requireOption(options, "master");
        String account = validateAccountLabel(requireOption(options, "account"));
        Path store = Paths.get(options.getOrDefault("store", DEFAULT_STORE.toString())).toAbsolutePath();
        JSONObject db = loadStore(store, master);
        JSONObject acc = getAccount(db, account);
        AlgoSpec algo = normalizeAlgorithm(acc.getString("algorithm"));
        JSONObject view = totpNow(acc.getString("secret"), acc.getInt("digits"), acc.getInt("period"), algo.hmacJce);
        System.out.println("Account       : " + acc.getString("label"));
        System.out.println("Current code  : " + view.getString("current"));
        System.out.println("Next code     : " + view.getString("next"));
        System.out.println("Expires in    : " + view.getInt("remaining") + "s");
    }

    private static void commandVerify(Map<String, String> options) throws Exception {
        String master = requireOption(options, "master");
        String account = validateAccountLabel(requireOption(options, "account"));
        String code = requireOption(options, "code");
        int drift = parseDrift(options.getOrDefault("drift", "1"));
        Path store = Paths.get(options.getOrDefault("store", DEFAULT_STORE.toString())).toAbsolutePath();
        JSONObject db = loadStore(store, master);
        JSONObject acc = getAccount(db, account);
        AlgoSpec algo = normalizeAlgorithm(acc.getString("algorithm"));
        Optional<Integer> offset = verifyTotp(
                acc.getString("secret"),
                code,
                acc.getInt("digits"),
                acc.getInt("period"),
                algo.hmacJce,
                drift
        );
        if (offset.isEmpty()) {
            System.out.println("Verification: INVALID (expired or incorrect code)");
            System.exit(2);
        }
        if (offset.get() == 0) System.out.println("Verification: VALID (current window)");
        else System.out.println("Verification: VALID (window offset " + offset.get() + ")");
    }

    private static void commandList(Map<String, String> options) throws Exception {
        String master = requireOption(options, "master");
        Path store = Paths.get(options.getOrDefault("store", DEFAULT_STORE.toString())).toAbsolutePath();
        JSONObject db = loadStore(store, master);
        JSONArray accounts = db.getJSONArray("accounts");
        if (accounts.length() == 0) {
            System.out.println("No accounts stored.");
            return;
        }
        System.out.println("Accounts in " + store + ":");
        for (int i = 0; i < accounts.length(); i++) {
            JSONObject acc = accounts.getJSONObject(i);
            System.out.println("- " + acc.getString("label")
                    + " | " + acc.getString("algorithm")
                    + " | digits=" + acc.getInt("digits")
                    + " | period=" + acc.getInt("period") + "s");
        }
    }

    private static void runDemo() throws Exception {
        Path store = Paths.get("totp_demo.enc.json").toAbsolutePath();
        if (Files.exists(store)) Files.delete(store);
        String master = "demo-master-password";
        String account = "DemoIssuer:demo.user@example.com";

        Map<String, String> gen = new HashMap<>();
        gen.put("master", master);
        gen.put("store", store.toString());
        gen.put("account", account);
        gen.put("length", "20");
        gen.put("digits", "6");
        gen.put("period", "30");
        gen.put("algorithm", "SHA-1");
        commandGenerate(gen);

        JSONObject db = loadStore(store, master);
        JSONObject acc = getAccount(db, account);
        AlgoSpec algo = normalizeAlgorithm(acc.getString("algorithm"));
        String current = totpNow(acc.getString("secret"), acc.getInt("digits"), acc.getInt("period"), algo.hmacJce).getString("current");

        System.out.println("\nDemo verify with correct code (" + current + ")");
        Map<String, String> verifyOk = new HashMap<>();
        verifyOk.put("master", master);
        verifyOk.put("store", store.toString());
        verifyOk.put("account", account);
        verifyOk.put("code", current);
        verifyOk.put("drift", "1");
        commandVerify(verifyOk);

        System.out.println("\nDemo verify with incorrect code (000000)");
        Map<String, String> verifyBad = new HashMap<>();
        verifyBad.put("master", master);
        verifyBad.put("store", store.toString());
        verifyBad.put("account", account);
        verifyBad.put("code", "000000");
        verifyBad.put("drift", "1");
        commandVerify(verifyBad);
    }
}