← All tasks
javascriptcodex/javascript-t1 #14Not a task: already works

TOTP Generator (javascript, written by Codex)

envgap__codex__javascript-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
package.json
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/javascript-t1 #14 · read the task the agent was given
Codex wrote this javascript 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 JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include:
- Source code
- package.json 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

4 files, exactly as written, before any repair.

package-lock.json
{
  "name": "totp-generator",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "totp-generator",
      "version": "1.0.0",
      "dependencies": {},
      "engines": {
        "node": ">=20.0.0"
      }
    }
  }
}

package.json
{
  "name": "totp-generator",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "src/index.js",
  "description": "RFC 6238 TOTP Generator with encrypted local account storage",
  "scripts": {
    "start": "node src/index.js"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "dependencies": {}
}

README.md
# TOTP Generator (JavaScript)

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

## Requirements

- Ubuntu 22.04
- Node.js 20+ (LTS)

## Dependencies

- Direct: none
- Transitive: none

Pinned in `package.json` and `package-lock.json`.

## Setup

```bash
npm install
```

## Run

```bash
node src/index.js 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]
node src/index.js code --master "<password>" --account "Issuer:user@example.com" [--store ./totp_accounts.enc.json]
node src/index.js verify --master "<password>" --account "Issuer:user@example.com" --code 123456 [--drift 1] [--store ./totp_accounts.enc.json]
node src/index.js list --master "<password>" [--store ./totp_accounts.enc.json]
```

## Features

- Subcommands: `generate`, `code`, `verify`, `list`
- Cryptographically random base32 secrets
- Configurable digits/period/hash algorithm
- RFC 6238 code generation and drift-tolerant verification
- `otpauth://` URI generation for authenticator apps
- Encrypted JSON account storage using PBKDF2 + AES-256-GCM
- Duplicate account detection and wrong password handling
- No-args demo mode

src/index.js
import crypto from "crypto";
import fs from "fs";
import path from "path";

const DEFAULT_STORE = path.resolve("totp_accounts.enc.json");
const PBKDF2_ITERS = 150000;

function parseArgs(argv) {
  const options = {};
  const positional = [];
  for (let i = 0; i < argv.length; i += 1) {
    const token = argv[i];
    if (token.startsWith("--")) {
      const key = token.slice(2);
      const next = argv[i + 1];
      if (next && !next.startsWith("--")) {
        options[key] = next;
        i += 1;
      } else {
        options[key] = true;
      }
    } else {
      positional.push(token);
    }
  }
  return { options, positional };
}

function normalizeAlgorithm(input) {
  const raw = String(input || "SHA-1").toUpperCase();
  const map = { "SHA-1": "sha1", "SHA-256": "sha256", "SHA-512": "sha512" };
  if (!map[raw]) throw new Error("Unsupported algorithm. Use SHA-1, SHA-256, or SHA-512.");
  return { label: raw, node: map[raw] };
}

function parseDigits(input) {
  const v = Number.parseInt(String(input || "6"), 10);
  if (![6, 8].includes(v)) throw new Error("Digits must be 6 or 8.");
  return v;
}

function parsePeriod(input) {
  const v = Number.parseInt(String(input || "30"), 10);
  if (![30, 60].includes(v)) throw new Error("Period must be 30 or 60.");
  return v;
}

function parseDrift(input) {
  const v = Number.parseInt(String(input || "1"), 10);
  if (Number.isNaN(v) || v < 0 || v > 10) throw new Error("Drift must be between 0 and 10.");
  return v;
}

function validateLabel(label) {
  if (!label || !label.includes(":")) throw new Error("Account label must be in issuer:username format.");
  return label;
}

function base32Encode(buffer) {
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  let bits = 0;
  let value = 0;
  let out = "";
  for (const byte of buffer) {
    value = (value << 8) | byte;
    bits += 8;
    while (bits >= 5) {
      out += alphabet[(value >>> (bits - 5)) & 31];
      bits -= 5;
    }
  }
  if (bits > 0) out += alphabet[(value << (5 - bits)) & 31];
  return out;
}

function base32Decode(text) {
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  const clean = text.toUpperCase().replace(/=+$/g, "").replace(/\s+/g, "");
  if (!clean) throw new Error("Invalid base32 secret: empty.");
  let bits = 0;
  let value = 0;
  const out = [];
  for (const ch of clean) {
    const idx = alphabet.indexOf(ch);
    if (idx < 0) throw new Error("Invalid base32 secret.");
    value = (value << 5) | idx;
    bits += 5;
    if (bits >= 8) {
      out.push((value >>> (bits - 8)) & 0xff);
      bits -= 8;
    }
  }
  return Buffer.from(out);
}

function leftPadCode(code, digits) {
  return String(code).padStart(digits, "0");
}

function totpAtCounter(secretBase32, digits, algorithmNode, counter) {
  const secret = base32Decode(secretBase32);
  const counterBuf = Buffer.alloc(8);
  counterBuf.writeBigUInt64BE(BigInt(counter));
  const hmac = crypto.createHmac(algorithmNode, secret).update(counterBuf).digest();
  const offset = hmac[hmac.length - 1] & 0x0f;
  const binCode = ((hmac[offset] & 0x7f) << 24)
    | ((hmac[offset + 1] & 0xff) << 16)
    | ((hmac[offset + 2] & 0xff) << 8)
    | (hmac[offset + 3] & 0xff);
  const mod = 10 ** digits;
  return leftPadCode(binCode % mod, digits);
}

function totpNow(secretBase32, digits, period, algorithmNode, epochSeconds = Math.floor(Date.now() / 1000)) {
  const counter = Math.floor(epochSeconds / period);
  const remaining = period - (epochSeconds % period);
  return {
    current: totpAtCounter(secretBase32, digits, algorithmNode, counter),
    next: totpAtCounter(secretBase32, digits, algorithmNode, counter + 1),
    remaining,
    counter,
  };
}

function verifyTotp(secretBase32, code, digits, period, algorithmNode, drift, epochSeconds = Math.floor(Date.now() / 1000)) {
  if (!/^\d{6,8}$/.test(code)) throw new Error("Code must be 6 or 8 digits.");
  const counter = Math.floor(epochSeconds / period);
  for (let w = -drift; w <= drift; w += 1) {
    const expected = totpAtCounter(secretBase32, digits, algorithmNode, counter + w);
    if (expected === code) return { valid: true, windowOffset: w };
  }
  return { valid: false, windowOffset: null };
}

function makeOtpAuthUri(accountLabel, secretBase32, digits, period, algorithmLabel) {
  const [issuerRaw, usernameRaw] = accountLabel.split(":", 2);
  const issuer = issuerRaw.trim();
  const username = usernameRaw.trim();
  const label = encodeURIComponent(`${issuer}:${username}`);
  const query = new URLSearchParams({
    secret: secretBase32,
    issuer,
    algorithm: algorithmLabel.replace("-", ""),
    digits: String(digits),
    period: String(period),
  });
  return `otpauth://totp/${label}?${query.toString()}`;
}

function deriveKey(masterPassword, salt) {
  return crypto.pbkdf2Sync(masterPassword, salt, PBKDF2_ITERS, 32, "sha256");
}

function encryptJson(jsonText, masterPassword) {
  const salt = crypto.randomBytes(16);
  const iv = crypto.randomBytes(12);
  const key = deriveKey(masterPassword, salt);
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
  const ciphertext = Buffer.concat([cipher.update(jsonText, "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();
  return {
    version: 1,
    kdf: "PBKDF2-SHA256",
    iterations: PBKDF2_ITERS,
    salt: salt.toString("base64"),
    iv: iv.toString("base64"),
    tag: tag.toString("base64"),
    data: ciphertext.toString("base64"),
  };
}

function decryptJson(blob, masterPassword) {
  try {
    const salt = Buffer.from(blob.salt, "base64");
    const iv = Buffer.from(blob.iv, "base64");
    const tag = Buffer.from(blob.tag, "base64");
    const data = Buffer.from(blob.data, "base64");
    const key = crypto.pbkdf2Sync(masterPassword, salt, blob.iterations, 32, "sha256");
    const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
    decipher.setAuthTag(tag);
    const plaintext = Buffer.concat([decipher.update(data), decipher.final()]);
    return plaintext.toString("utf8");
  } catch {
    throw new Error("Wrong master password or corrupted encrypted store.");
  }
}

function loadStore(storePath, masterPassword) {
  if (!fs.existsSync(storePath)) return { version: 1, accounts: [] };
  const raw = JSON.parse(fs.readFileSync(storePath, "utf8"));
  const plain = decryptJson(raw, masterPassword);
  const parsed = JSON.parse(plain);
  if (!Array.isArray(parsed.accounts)) throw new Error("Encrypted store is malformed.");
  return parsed;
}

function saveStore(storePath, masterPassword, data) {
  const encrypted = encryptJson(JSON.stringify(data, null, 2), masterPassword);
  fs.mkdirSync(path.dirname(storePath), { recursive: true });
  fs.writeFileSync(storePath, `${JSON.stringify(encrypted, null, 2)}\n`, "utf8");
}

function requireMaster(options) {
  if (!options.master) throw new Error("Missing --master <password> for encrypted storage.");
  return String(options.master);
}

function getAccount(store, label) {
  const account = store.accounts.find((a) => a.label === label);
  if (!account) throw new Error(`Account not found: ${label}`);
  return account;
}

function cmdGenerate(options) {
  const master = requireMaster(options);
  const storePath = path.resolve(options.store || DEFAULT_STORE);
  const accountLabel = validateLabel(options.account || "");
  const length = Number.parseInt(String(options.length || "20"), 10);
  if (Number.isNaN(length) || length < 10 || length > 128) throw new Error("Length must be between 10 and 128 bytes.");
  const digits = parseDigits(options.digits || "6");
  const period = parsePeriod(options.period || "30");
  const algo = normalizeAlgorithm(options.algorithm || "SHA-1");

  const store = loadStore(storePath, master);
  if (store.accounts.some((a) => a.label === accountLabel)) throw new Error(`Duplicate account: ${accountLabel}`);
  const secret = base32Encode(crypto.randomBytes(length));
  const account = {
    label: accountLabel,
    secret,
    digits,
    period,
    algorithm: algo.label,
    createdAt: new Date().toISOString(),
  };
  store.accounts.push(account);
  saveStore(storePath, master, store);

  const uri = makeOtpAuthUri(accountLabel, secret, digits, period, algo.label);
  const now = totpNow(secret, digits, period, algo.node);
  console.log(`Account added: ${accountLabel}`);
  console.log(`Store file    : ${storePath}`);
  console.log(`Secret (base32): ${secret}`);
  console.log(`otpauth URI   : ${uri}`);
  console.log(`Current code  : ${now.current}`);
  console.log(`Next code     : ${now.next}`);
  console.log(`Expires in    : ${now.remaining}s`);
}

function cmdCode(options) {
  const master = requireMaster(options);
  const storePath = path.resolve(options.store || DEFAULT_STORE);
  const accountLabel = validateLabel(options.account || "");
  const store = loadStore(storePath, master);
  const account = getAccount(store, accountLabel);
  const algo = normalizeAlgorithm(account.algorithm);
  const now = totpNow(account.secret, account.digits, account.period, algo.node);
  console.log(`Account       : ${account.label}`);
  console.log(`Current code  : ${now.current}`);
  console.log(`Next code     : ${now.next}`);
  console.log(`Expires in    : ${now.remaining}s`);
}

function cmdVerify(options) {
  const master = requireMaster(options);
  const storePath = path.resolve(options.store || DEFAULT_STORE);
  const accountLabel = validateLabel(options.account || "");
  const code = String(options.code || "");
  const drift = parseDrift(options.drift || "1");
  const store = loadStore(storePath, master);
  const account = getAccount(store, accountLabel);
  const algo = normalizeAlgorithm(account.algorithm);
  const result = verifyTotp(account.secret, code, account.digits, account.period, algo.node, drift);
  if (!result.valid) {
    console.log("Verification: INVALID (expired or incorrect code)");
    process.exitCode = 2;
    return;
  }
  const position = result.windowOffset === 0 ? "current window" : `window offset ${result.windowOffset}`;
  console.log(`Verification: VALID (${position})`);
}

function cmdList(options) {
  const master = requireMaster(options);
  const storePath = path.resolve(options.store || DEFAULT_STORE);
  const store = loadStore(storePath, master);
  if (store.accounts.length === 0) {
    console.log("No accounts stored.");
    return;
  }
  console.log(`Accounts in ${storePath}:`);
  for (const account of store.accounts) {
    console.log(`- ${account.label} | ${account.algorithm} | digits=${account.digits} | period=${account.period}s`);
  }
}

function demo() {
  const demoStore = path.resolve("totp_demo.enc.json");
  const master = "demo-master-password";
  const accountLabel = "DemoIssuer:demo.user@example.com";
  if (fs.existsSync(demoStore)) fs.unlinkSync(demoStore);

  cmdGenerate({
    master,
    store: demoStore,
    account: accountLabel,
    length: "20",
    digits: "6",
    period: "30",
    algorithm: "SHA-1",
  });
  const store = loadStore(demoStore, master);
  const account = getAccount(store, accountLabel);
  const algo = normalizeAlgorithm(account.algorithm);
  const now = totpNow(account.secret, account.digits, account.period, algo.node);
  console.log(`\nDemo verify with correct code (${now.current})`);
  cmdVerify({ master, store: demoStore, account: accountLabel, code: now.current, drift: "1" });
  console.log("\nDemo verify with incorrect code (000000)");
  cmdVerify({ master, store: demoStore, account: accountLabel, code: "000000", drift: "1" });
}

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

function main() {
  const { options, positional } = parseArgs(process.argv.slice(2));
  if (positional.length === 0) {
    demo();
    return;
  }
  const cmd = positional[0].toLowerCase();
  if (cmd === "generate") cmdGenerate(options);
  else if (cmd === "code") cmdCode(options);
  else if (cmd === "verify") cmdVerify(options);
  else if (cmd === "list") cmdList(options);
  else throw new Error(`Unknown subcommand: ${cmd}\n${usage()}`);
}

try {
  main();
} catch (error) {
  const message = error instanceof Error ? error.message : String(error);
  console.error(`Error: ${message}`);
  process.exit(1);
}