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);
}
}