← All tasks
javaclaude-code/java-t2 #14Lite task

TOTP Generator (java, written by Claude Code)

envgap__claude-code__java-t2-14

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

01 / FAILURE SIGNATURE

Captured in a clean container

error: no classes were compiled

02 / ENVIRONMENT RECIPE

Base commit
d748741ee5360b4e8867bb36b9fcec8e8d2d5c98
Manifest
pom.xml
Reproduce
mvn -B -q dependency:copy-dependencies -DoutputDirectory=target/dependency -DincludeScope=runtime && cp=$(ls target/dependency/*.jar 2>/dev/null | tr '\n' ':'); test -d target/classes || { echo 'error: no classes were compiled'; exit 1; }; python3 -c 'import hashlib, os, subprocess, sys tracked = [p for p in subprocess.run(["git", "ls-files", "-z", "--", "*.java"], capture_output=True).stdout.decode().split("\0") if p] digest = lambda p: hashlib.sha256(open(p, "rb").read()).hexdigest() own = {digest(p) for p in tracked if os.path.isfile(p)} names = {os.path.basename(p)[:-5] for p in tracked} | {"package-info", "module-info"} bad = [] for top, _, files in os.walk("target"): for name in files: path = os.path.join(top, name) if name.endswith(".java") and digest(path) not in own: bad.append(path) elif top.startswith(os.path.join("target", "classes")) and name.endswith(".class") and name[:-6].split("$")[0] not in names: bad.append(path) if bad: print("\n".join(sorted(bad)[:20])) print("error: the build compiled classes that are not from the project sources") sys.exit(1)' || exit 1; jd=$(jdeps --multi-release 17 -verbose:class -cp "${cp}target/classes" target/classes 2>&1) && st=0 || st=$?; missing=$(printf '%s\n' "$jd" | grep 'not found' || true); if [ $st -ne 0 ]; then printf '%s\n' "$jd" | tail -n 20; echo 'error: jdeps could not read the classes'; exit 1; fi; if [ -n "$missing" ]; then printf '%s\n' "$missing"; echo 'error: classes the program uses are missing from the class path it runs with'; exit 1; fi
Run under trace
rc=0; out=$(timeout 60 java -cp 'target/dependency/*:target/classes' TotpGenerator < /dev/null 2>&1 | { head -c 1000000; cat > /dev/null; }; exit ${PIPESTATUS[0]}) || rc=$?; printf '%s\n' "$out"; env_error='(ModuleNotFoundError|ImportError|No module named|cannot open shared object file|DLL load failed|shared library|cannot load library|Library not loaded|Cannot find module|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ERR_REQUIRE_ESM|compiled against a different Node|Could not find or load main class|ClassNotFoundException|NoClassDefFoundError|UnsupportedClassVersionError|UnsatisfiedLinkError|NoSuchMethodError|NoSuchFieldError|AbstractMethodError|IncompatibleClassChangeError|IllegalAccessError|ServiceConfigurationError|error while loading shared libraries|symbol lookup error|version `[^'"'"']*'"'"' not found|command not found)'; asked='(^| )[[:blank:]]*usage:|the following arguments are required|missing (required )?(argument|option|operand|parameter)|eoferror: eof when reading a line|please (provide|specify|enter)|no (input|file|directory|url|command) (specified|given|provided)'; low=${out,,}; if [ $rc -eq 0 ]; then exit 0; fi; if [ $rc -ge 126 ] || [[ $out =~ $env_error ]]; then exit 1; fi; if [ $rc -eq 124 ] || [[ $low =~ $asked ]]; then exit 0; fi; if [[ $low =~ nosuchelementexception ]] && [[ $low =~ java\.util\.scanner ]]; then exit 0; fi; exit 1
Reference environment fix used for admission
--- /dev/null
+++ b/src/main/java/TotpGenerator.java
@@ -0,0 +1,308 @@
+import java.io.*;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.warrenstrange.googleauth.GoogleAuthenticator;
+import com.warrenstrange.googleauth.GoogleAuthenticatorConfig;
+import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
+import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
+
+/**
+ * TOTP Generator - Generates and validates RFC 6238 TOTP codes with
+ * multi-account storage and otpauth:// URI generation.
+ *
+ * Dependencies: googleauth, Jackson
+ */
+public class TotpGenerator {
+
+    private static final String ACCOUNTS_FILE = "totp_accounts.json";
+    private static final ObjectMapper mapper = new ObjectMapper()
+            .enable(SerializationFeature.INDENT_OUTPUT);
+
+    /**
+     * Represents a stored TOTP account.
+     */
+    static class Account {
+        public String name;
+        public String issuer;
+        public String secret;
+        public int digits;
+        public int interval;
+
+        public Account() {}
+
+        Account(String name, String issuer, String secret, int digits, int interval) {
+            this.name = name;
+            this.issuer = issuer;
+            this.secret = secret;
+            this.digits = digits;
+            this.interval = interval;
+        }
+    }
+
+    /**
+     * Load accounts from the JSON storage file.
+     */
+    private static Map<String, Account> loadAccounts() {
+        File file = new File(ACCOUNTS_FILE);
+        if (!file.exists()) {
+            return new HashMap<>();
+        }
+        try {
+            return mapper.readValue(file, new TypeReference<Map<String, Account>>() {});
+        } catch (IOException e) {
+            System.err.println("Error loading accounts: " + e.getMessage());
+            return new HashMap<>();
+        }
+    }
+
+    /**
+     * Save accounts to the JSON storage file.
+     */
+    private static void saveAccounts(Map<String, Account> accounts) {
+        try {
+            mapper.writeValue(new File(ACCOUNTS_FILE), accounts);
+        } catch (IOException e) {
+            System.err.println("Error saving accounts: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Create a GoogleAuthenticator instance with custom configuration.
+     */
+    private static GoogleAuthenticator createAuthenticator(int digits, int interval) {
+        GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder configBuilder =
+                new GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder();
+        configBuilder.setCodeDigits(digits);
+        configBuilder.setTimeStepSizeInMillis(interval * 1000L);
+        configBuilder.setWindowSize(3);
+        return new GoogleAuthenticator(configBuilder.build());
+    }
+
+    /**
+     * Add a new TOTP account.
+     */
+    public static Account addAccount(String name, String issuer, String secret,
+                                     int digits, int interval) {
+        Map<String, Account> accounts = loadAccounts();
+
+        if (secret == null || secret.isEmpty()) {
+            GoogleAuthenticator gAuth = createAuthenticator(digits, interval);
+            GoogleAuthenticatorKey key = gAuth.createCredentials();
+            secret = key.getKey();
+        }
+
+        Account account = new Account(name, issuer, secret, digits, interval);
+        String key = (issuer != null && !issuer.isEmpty()) ? issuer + ":" + name : name;
+        accounts.put(key, account);
+        saveAccounts(accounts);
+        System.out.println("Account '" + key + "' added successfully.");
+        return account;
+    }
+
+    /**
+     * Generate the current TOTP code for the given account.
+     */
+    public static String generateTotp(String accountKey) {
+        Map<String, Account> accounts = loadAccounts();
+        if (!accounts.containsKey(accountKey)) {
+            throw new IllegalArgumentException("Account '" + accountKey + "' not found.");
+        }
+        Account acct = accounts.get(accountKey);
+        GoogleAuthenticator gAuth = createAuthenticator(acct.digits, acct.interval);
+
+        int code = gAuth.getTotpPassword(acct.secret);
+        String codeStr = String.format("%0" + acct.digits + "d", code);
+
+        long currentTime = System.currentTimeMillis() / 1000;
+        long remaining = acct.interval - (currentTime % acct.interval);
+        System.out.println("TOTP for '" + accountKey + "': " + codeStr
+                + "  (valid for " + remaining + "s)");
+        return codeStr;
+    }
+
+    /**
+     * Validate a TOTP code for the given account.
+     */
+    public static boolean validateTotp(String accountKey, String code) {
+        Map<String, Account> accounts = loadAccounts();
+        if (!accounts.containsKey(accountKey)) {
+            throw new IllegalArgumentException("Account '" + accountKey + "' not found.");
+        }
+        Account acct = accounts.get(accountKey);
+        GoogleAuthenticator gAuth = createAuthenticator(acct.digits, acct.interval);
+
+        int codeInt;
+        try {
+            codeInt = Integer.parseInt(code);
+        } catch (NumberFormatException e) {
+            System.out.println("Code '" + code + "' for '" + accountKey + "' is INVALID.");
+            return false;
+        }
+
+        boolean valid = gAuth.authorize(acct.secret, codeInt);
+        String status = valid ? "VALID" : "INVALID";
+        System.out.println("Code '" + code + "' for '" + accountKey + "' is " + status + ".");
+        return valid;
+    }
+
+    /**
+     * Generate an otpauth:// URI for the given account.
+     */
+    public static String getOtpauthUri(String accountKey) throws Exception {
+        Map<String, Account> accounts = loadAccounts();
+        if (!accounts.containsKey(accountKey)) {
+            throw new IllegalArgumentException("Account '" + accountKey + "' not found.");
+        }
+        Account acct = accounts.get(accountKey);
+
+        String uri = GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL(
+                acct.issuer != null ? acct.issuer : "",
+                acct.name,
+                new GoogleAuthenticatorKey.Builder(acct.secret).build()
+        );
+
+        System.out.println("otpauth URI: " + uri);
+        return uri;
+    }
+
+    /**
+     * List all stored accounts.
+     */
+    public static void listAccounts() {
+        Map<String, Account> accounts = loadAccounts();
+        if (accounts.isEmpty()) {
+            System.out.println("No accounts stored.");
+            return;
+        }
+        System.out.printf("\n%-35s %-20s %-8s %-10s%n",
+                "Account Key", "Issuer", "Digits", "Interval");
+        System.out.println("-".repeat(75));
+        for (Map.Entry<String, Account> entry : accounts.entrySet()) {
+            Account a = entry.getValue();
+            System.out.printf("%-35s %-20s %-8d %-10d%n",
+                    entry.getKey(),
+                    a.issuer != null ? a.issuer : "",
+                    a.digits, a.interval);
+        }
+        System.out.println();
+    }
+
+    /**
+     * Remove an account from storage.
+     */
+    public static boolean removeAccount(String accountKey) {
+        Map<String, Account> accounts = loadAccounts();
+        if (!accounts.containsKey(accountKey)) {
+            System.out.println("Account '" + accountKey + "' not found.");
+            return false;
+        }
+        accounts.remove(accountKey);
+        saveAccounts(accounts);
+        System.out.println("Account '" + accountKey + "' removed.");
+        return true;
+    }
+
+    /**
+     * Export an account's data as JSON string.
+     */
+    public static String exportAccount(String accountKey) {
+        Map<String, Account> accounts = loadAccounts();
+        if (!accounts.containsKey(accountKey)) {
+            throw new IllegalArgumentException("Account '" + accountKey + "' not found.");
+        }
+        try {
+            String json = mapper.writeValueAsString(accounts.get(accountKey));
+            System.out.println("Exported: " + json);
+            return json;
+        } catch (IOException e) {
+            throw new RuntimeException("Export failed: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Interactive CLI entry point.
+     */
+    public static void main(String[] args) {
+        Scanner scanner = new Scanner(System.in);
+        while (true) {
+            System.out.println("\n=== TOTP Generator ===");
+            System.out.println("1. Add account");
+            System.out.println("2. Generate TOTP code");
+            System.out.println("3. Validate TOTP code");
+            System.out.println("4. Show otpauth URI");
+            System.out.println("5. List accounts");
+            System.out.println("6. Remove account");
+            System.out.println("7. Export account");
+            System.out.println("8. Exit");
+            System.out.print("\nSelect option: ");
+
+            String choice = scanner.nextLine().trim();
+
+            try {
+                switch (choice) {
+                    case "1":
+                        System.out.print("Account name: ");
+                        String name = scanner.nextLine().trim();
+                        System.out.print("Issuer: ");
+                        String issuer = scanner.nextLine().trim();
+                        System.out.print("Secret (blank to auto-generate): ");
+                        String secret = scanner.nextLine().trim();
+                        System.out.print("Digits (default 6): ");
+                        String digitsStr = scanner.nextLine().trim();
+                        int digits = digitsStr.isEmpty() ? 6 : Integer.parseInt(digitsStr);
+                        System.out.print("Interval (default 30): ");
+                        String intervalStr = scanner.nextLine().trim();
+                        int interval = intervalStr.isEmpty() ? 30 : Integer.parseInt(intervalStr);
+                        addAccount(name, issuer, secret, digits, interval);
+                        break;
+                    case "2":
+                        System.out.print("Account key: ");
+                        generateTotp(scanner.nextLine().trim());
+                        break;
+                    case "3":
+                        System.out.print("Account key: ");
+                        String vKey = scanner.nextLine().trim();
+                        System.out.print("TOTP code: ");
+                        String code = scanner.nextLine().trim();
+                        validateTotp(vKey, code);
+                        break;
+                    case "4":
+                        System.out.print("Account key: ");
+                        getOtpauthUri(scanner.nextLine().trim());
+                        break;
+                    case "5":
+                        listAccounts();
+                        break;
+                    case "6":
+                        System.out.print("Account key: ");
+                        removeAccount(scanner.nextLine().trim());
+                        break;
+                    case "7":
+                        System.out.print("Account key: ");
+                        exportAccount(scanner.nextLine().trim());
+                        break;
+                    case "8":
+                        System.out.println("Goodbye.");
+                        scanner.close();
+                        return;
+                    default:
+                        System.out.println("Invalid option.");
+                }
+            } catch (Exception e) {
+                System.err.println("Error: " + e.getMessage());
+            }
+        }
+    }
+}

03 / TASK AND FAILURE

claude-code/java-t2 #14 · read the task the agent was given
Claude Code wrote this java project from the task below. It does not run 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 checked by running the task · needs human review

misspecification
Label rules and the text that matched
[
  {
    "category": "misspecification",
    "rule": "signature.build_layout_mismatch",
    "source": "failure_signature",
    "excerpt": "error: no classes were compiled"
  }
]

Written by Claude Code (study run M1T2P14L2). It failed as written and was repaired by changing only its environment.

Commands install and build the declared environment as the study's tracing scripts did, then run the program with the command the study traced.

Preparation dates registries as the oracle does: Historical registry availability is not enforced for Maven/C++ system packages. Maven updatePolicy controls refresh frequency, not publication date.

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>totp-generator</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>TOTP Generator</name>
    <description>RFC 6238 TOTP code generator and validator with multi-account storage</description>

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

    <dependencies>
        <dependency>
            <groupId>com.warrenstrange</groupId>
            <artifactId>googleauth</artifactId>
            <version>1.5.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.16.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>TotpGenerator</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
README.md
# TOTP Generator - Java (Trial 2)

A TOTP (Time-based One-Time Password) generator and validator implementing RFC 6238, with multi-account storage and otpauth:// URI generation.

## Dependencies

- **googleauth** (1.5.0) - Google Authenticator server-side library for TOTP generation and validation
- **Jackson** (2.16.1) - JSON serialization/deserialization for account storage

## Setup

```bash
mvn clean compile
```

## Usage

Run the interactive CLI:

```bash
mvn exec:java -Dexec.mainClass="TotpGenerator"
```

Or build and run the JAR:

```bash
mvn clean package
java -jar target/totp-generator-1.0.0.jar
```

### Features

- Add TOTP accounts with auto-generated or custom secrets via Google Authenticator library
- Generate current TOTP codes
- Validate TOTP codes with window-based tolerance
- Generate otpauth:// URIs using GoogleAuthenticatorQRGenerator
- Export account data as JSON
- Multi-account JSON file storage
- List and remove stored accounts
TotpGenerator.java
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorConfig;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;

/**
 * TOTP Generator - Generates and validates RFC 6238 TOTP codes with
 * multi-account storage and otpauth:// URI generation.
 *
 * Dependencies: googleauth, Jackson
 */
public class TotpGenerator {

    private static final String ACCOUNTS_FILE = "totp_accounts.json";
    private static final ObjectMapper mapper = new ObjectMapper()
            .enable(SerializationFeature.INDENT_OUTPUT);

    /**
     * Represents a stored TOTP account.
     */
    static class Account {
        public String name;
        public String issuer;
        public String secret;
        public int digits;
        public int interval;

        public Account() {}

        Account(String name, String issuer, String secret, int digits, int interval) {
            this.name = name;
            this.issuer = issuer;
            this.secret = secret;
            this.digits = digits;
            this.interval = interval;
        }
    }

    /**
     * Load accounts from the JSON storage file.
     */
    private static Map<String, Account> loadAccounts() {
        File file = new File(ACCOUNTS_FILE);
        if (!file.exists()) {
            return new HashMap<>();
        }
        try {
            return mapper.readValue(file, new TypeReference<Map<String, Account>>() {});
        } catch (IOException e) {
            System.err.println("Error loading accounts: " + e.getMessage());
            return new HashMap<>();
        }
    }

    /**
     * Save accounts to the JSON storage file.
     */
    private static void saveAccounts(Map<String, Account> accounts) {
        try {
            mapper.writeValue(new File(ACCOUNTS_FILE), accounts);
        } catch (IOException e) {
            System.err.println("Error saving accounts: " + e.getMessage());
        }
    }

    /**
     * Create a GoogleAuthenticator instance with custom configuration.
     */
    private static GoogleAuthenticator createAuthenticator(int digits, int interval) {
        GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder configBuilder =
                new GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder();
        configBuilder.setCodeDigits(digits);
        configBuilder.setTimeStepSizeInMillis(interval * 1000L);
        configBuilder.setWindowSize(3);
        return new GoogleAuthenticator(configBuilder.build());
    }

    /**
     * Add a new TOTP account.
     */
    public static Account addAccount(String name, String issuer, String secret,
                                     int digits, int interval) {
        Map<String, Account> accounts = loadAccounts();

        if (secret == null || secret.isEmpty()) {
            GoogleAuthenticator gAuth = createAuthenticator(digits, interval);
            GoogleAuthenticatorKey key = gAuth.createCredentials();
            secret = key.getKey();
        }

        Account account = new Account(name, issuer, secret, digits, interval);
        String key = (issuer != null && !issuer.isEmpty()) ? issuer + ":" + name : name;
        accounts.put(key, account);
        saveAccounts(accounts);
        System.out.println("Account '" + key + "' added successfully.");
        return account;
    }

    /**
     * Generate the current TOTP code for the given account.
     */
    public static String generateTotp(String accountKey) {
        Map<String, Account> accounts = loadAccounts();
        if (!accounts.containsKey(accountKey)) {
            throw new IllegalArgumentException("Account '" + accountKey + "' not found.");
        }
        Account acct = accounts.get(accountKey);
        GoogleAuthenticator gAuth = createAuthenticator(acct.digits, acct.interval);

        int code = gAuth.getTotpPassword(acct.secret);
        String codeStr = String.format("%0" + acct.digits + "d", code);

        long currentTime = System.currentTimeMillis() / 1000;
        long remaining = acct.interval - (currentTime % acct.interval);
        System.out.println("TOTP for '" + accountKey + "': " + codeStr
                + "  (valid for " + remaining + "s)");
        return codeStr;
    }

    /**
     * Validate a TOTP code for the given account.
     */
    public static boolean validateTotp(String accountKey, String code) {
        Map<String, Account> accounts = loadAccounts();
        if (!accounts.containsKey(accountKey)) {
            throw new IllegalArgumentException("Account '" + accountKey + "' not found.");
        }
        Account acct = accounts.get(accountKey);
        GoogleAuthenticator gAuth = createAuthenticator(acct.digits, acct.interval);

        int codeInt;
        try {
            codeInt = Integer.parseInt(code);
        } catch (NumberFormatException e) {
            System.out.println("Code '" + code + "' for '" + accountKey + "' is INVALID.");
            return false;
        }

        boolean valid = gAuth.authorize(acct.secret, codeInt);
        String status = valid ? "VALID" : "INVALID";
        System.out.println("Code '" + code + "' for '" + accountKey + "' is " + status + ".");
        return valid;
    }

    /**
     * Generate an otpauth:// URI for the given account.
     */
    public static String getOtpauthUri(String accountKey) throws Exception {
        Map<String, Account> accounts = loadAccounts();
        if (!accounts.containsKey(accountKey)) {
            throw new IllegalArgumentException("Account '" + accountKey + "' not found.");
        }
        Account acct = accounts.get(accountKey);

        String uri = GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL(
                acct.issuer != null ? acct.issuer : "",
                acct.name,
                new GoogleAuthenticatorKey.Builder(acct.secret).build()
        );

        System.out.println("otpauth URI: " + uri);
        return uri;
    }

    /**
     * List all stored accounts.
     */
    public static void listAccounts() {
        Map<String, Account> accounts = loadAccounts();
        if (accounts.isEmpty()) {
            System.out.println("No accounts stored.");
            return;
        }
        System.out.printf("\n%-35s %-20s %-8s %-10s%n",
                "Account Key", "Issuer", "Digits", "Interval");
        System.out.println("-".repeat(75));
        for (Map.Entry<String, Account> entry : accounts.entrySet()) {
            Account a = entry.getValue();
            System.out.printf("%-35s %-20s %-8d %-10d%n",
                    entry.getKey(),
                    a.issuer != null ? a.issuer : "",
                    a.digits, a.interval);
        }
        System.out.println();
    }

    /**
     * Remove an account from storage.
     */
    public static boolean removeAccount(String accountKey) {
        Map<String, Account> accounts = loadAccounts();
        if (!accounts.containsKey(accountKey)) {
            System.out.println("Account '" + accountKey + "' not found.");
            return false;
        }
        accounts.remove(accountKey);
        saveAccounts(accounts);
        System.out.println("Account '" + accountKey + "' removed.");
        return true;
    }

    /**
     * Export an account's data as JSON string.
     */
    public static String exportAccount(String accountKey) {
        Map<String, Account> accounts = loadAccounts();
        if (!accounts.containsKey(accountKey)) {
            throw new IllegalArgumentException("Account '" + accountKey + "' not found.");
        }
        try {
            String json = mapper.writeValueAsString(accounts.get(accountKey));
            System.out.println("Exported: " + json);
            return json;
        } catch (IOException e) {
            throw new RuntimeException("Export failed: " + e.getMessage(), e);
        }
    }

    /**
     * Interactive CLI entry point.
     */
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("\n=== TOTP Generator ===");
            System.out.println("1. Add account");
            System.out.println("2. Generate TOTP code");
            System.out.println("3. Validate TOTP code");
            System.out.println("4. Show otpauth URI");
            System.out.println("5. List accounts");
            System.out.println("6. Remove account");
            System.out.println("7. Export account");
            System.out.println("8. Exit");
            System.out.print("\nSelect option: ");

            String choice = scanner.nextLine().trim();

            try {
                switch (choice) {
                    case "1":
                        System.out.print("Account name: ");
                        String name = scanner.nextLine().trim();
                        System.out.print("Issuer: ");
                        String issuer = scanner.nextLine().trim();
                        System.out.print("Secret (blank to auto-generate): ");
                        String secret = scanner.nextLine().trim();
                        System.out.print("Digits (default 6): ");
                        String digitsStr = scanner.nextLine().trim();
                        int digits = digitsStr.isEmpty() ? 6 : Integer.parseInt(digitsStr);
                        System.out.print("Interval (default 30): ");
                        String intervalStr = scanner.nextLine().trim();
                        int interval = intervalStr.isEmpty() ? 30 : Integer.parseInt(intervalStr);
                        addAccount(name, issuer, secret, digits, interval);
                        break;
                    case "2":
                        System.out.print("Account key: ");
                        generateTotp(scanner.nextLine().trim());
                        break;
                    case "3":
                        System.out.print("Account key: ");
                        String vKey = scanner.nextLine().trim();
                        System.out.print("TOTP code: ");
                        String code = scanner.nextLine().trim();
                        validateTotp(vKey, code);
                        break;
                    case "4":
                        System.out.print("Account key: ");
                        getOtpauthUri(scanner.nextLine().trim());
                        break;
                    case "5":
                        listAccounts();
                        break;
                    case "6":
                        System.out.print("Account key: ");
                        removeAccount(scanner.nextLine().trim());
                        break;
                    case "7":
                        System.out.print("Account key: ");
                        exportAccount(scanner.nextLine().trim());
                        break;
                    case "8":
                        System.out.println("Goodbye.");
                        scanner.close();
                        return;
                    default:
                        System.out.println("Invalid option.");
                }
            } catch (Exception e) {
                System.err.println("Error: " + e.getMessage());
            }
        }
    }
}