TOTP Generator (cpp, written by Codex)
envgap__codex__cpp-t1-14
Written by a coding agent; not on GitHubWritten 2026-03-02
01 / FAILURE SIGNATURE
As the study recorded it
mbedtls 3.6.0 ZIP missing framework submodule
Not a benchmark task.
- It was made to work, but its repair cannot be rebuilt from the saved files (the saved copy shows no change, or not all of the changes the study's notes describe), so there is no fix to score against.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
CMakeLists.txt- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/cpp-t1 #14 · read the task the agent was given
Codex wrote this cpp 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 C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include: - Source code - CMakeLists.txt 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.
CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(totp_generator_cpp VERSION 1.0.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
include(FetchContent)
# Pinned mbedTLS
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
mbedtls
URL https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/v3.6.0.zip
)
# Pinned nlohmann/json
FetchContent_Declare(
nlohmann_json
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
)
FetchContent_MakeAvailable(mbedtls nlohmann_json)
add_executable(totp_tool src/main.cpp)
target_link_libraries(totp_tool PRIVATE mbedtls mbedx509 mbedcrypto nlohmann_json::nlohmann_json)
target_include_directories(totp_tool PRIVATE ${mbedtls_SOURCE_DIR}/include)
README.md
# TOTP Generator (C++) RFC 6238-compatible TOTP generator/verifier with encrypted multi-account storage. ## Requirements - Ubuntu 22.04 - G++ 12+ - CMake 3.22+ ## Dependencies (Pinned) - Direct: - `mbedTLS v3.6.0` - `nlohmann/json v3.11.3` - Transitive: - `mbedcrypto`, `mbedx509` from pinned mbedTLS source ## Build ```bash cmake -S . -B build cmake --build build --config Release ``` ## Run ```bash ./build/totp_tool 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] ./build/totp_tool code --master "<password>" --account "Issuer:user@example.com" [--store ./totp_accounts.enc.json] ./build/totp_tool verify --master "<password>" --account "Issuer:user@example.com" --code 123456 [--drift 1] [--store ./totp_accounts.enc.json] ./build/totp_tool list --master "<password>" [--store ./totp_accounts.enc.json] ``` ## Features - Subcommands: `generate`, `code`, `verify`, `list` - Base32 secret generation and validation - TOTP with SHA-1/SHA-256/SHA-512 and 6/8-digit modes - Time-window drift verification - `otpauth://` URI output - Encrypted local JSON storage with PBKDF2-SHA256 + AES-256-GCM - Duplicate account and wrong password handling - No-args demo workflow
src/main.cpp
#include <algorithm>
#include <chrono>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#include <mbedtls/base64.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/entropy.h>
#include <mbedtls/error.h>
#include <mbedtls/gcm.h>
#include <mbedtls/md.h>
#include <mbedtls/pkcs5.h>
#include <nlohmann/json.hpp>
namespace {
const std::filesystem::path DEFAULT_STORE = std::filesystem::absolute("totp_accounts.enc.json");
const int PBKDF2_ITERS = 150000;
struct ParsedArgs {
std::map<std::string, std::string> options;
std::vector<std::string> positional;
};
struct AlgoSpec {
std::string label;
mbedtls_md_type_t mdType;
};
struct TotpView {
std::string current;
std::string next;
int remaining;
};
ParsedArgs parseArgs(int argc, char** argv) {
ParsedArgs out;
for (int i = 1; i < argc; i++) {
std::string token = argv[i];
if (token.rfind("--", 0) == 0) {
std::string key = token.substr(2);
if (i + 1 < argc && std::string(argv[i + 1]).rfind("--", 0) != 0) out.options[key] = argv[++i];
else out.options[key] = "true";
} else {
out.positional.push_back(token);
}
}
return out;
}
[[noreturn]] void throwMbed(int rc, const std::string& where) {
char buf[256];
mbedtls_strerror(rc, buf, sizeof(buf));
throw std::runtime_error(where + ": " + std::string(buf));
}
struct Rng {
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr;
Rng() {
mbedtls_entropy_init(&entropy);
mbedtls_ctr_drbg_init(&ctr);
const std::string pers = "totp-generator-cpp";
const int rc = mbedtls_ctr_drbg_seed(
&ctr,
mbedtls_entropy_func,
&entropy,
reinterpret_cast<const unsigned char*>(pers.data()),
pers.size()
);
if (rc != 0) throwMbed(rc, "mbedtls_ctr_drbg_seed");
}
~Rng() {
mbedtls_ctr_drbg_free(&ctr);
mbedtls_entropy_free(&entropy);
}
std::vector<unsigned char> bytes(std::size_t n) {
std::vector<unsigned char> out(n);
const int rc = mbedtls_ctr_drbg_random(&ctr, out.data(), out.size());
if (rc != 0) throwMbed(rc, "mbedtls_ctr_drbg_random");
return out;
}
};
std::string readText(const std::filesystem::path& p) {
std::ifstream in(p, std::ios::binary);
if (!in.is_open()) throw std::runtime_error("Failed to open file: " + p.string());
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}
void writeText(const std::filesystem::path& p, const std::string& text) {
if (!p.parent_path().empty()) std::filesystem::create_directories(p.parent_path());
std::ofstream out(p, std::ios::binary);
if (!out.is_open()) throw std::runtime_error("Failed to write file: " + p.string());
out << text;
}
std::string base64Encode(const std::vector<unsigned char>& data) {
std::vector<unsigned char> out((data.size() * 4 / 3) + 8);
std::size_t olen = 0;
const int rc = mbedtls_base64_encode(out.data(), out.size(), &olen, data.data(), data.size());
if (rc != 0) throwMbed(rc, "mbedtls_base64_encode");
return std::string(reinterpret_cast<const char*>(out.data()), olen);
}
std::vector<unsigned char> base64Decode(const std::string& text) {
std::vector<unsigned char> out(text.size() + 8);
std::size_t olen = 0;
const int rc = mbedtls_base64_decode(
out.data(),
out.size(),
&olen,
reinterpret_cast<const unsigned char*>(text.data()),
text.size()
);
if (rc != 0) throwMbed(rc, "mbedtls_base64_decode");
out.resize(olen);
return out;
}
AlgoSpec normalizeAlgorithm(const std::string& raw) {
std::string value = raw;
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
return static_cast<char>(std::toupper(c));
});
if (value == "SHA-1") return {"SHA-1", MBEDTLS_MD_SHA1};
if (value == "SHA-256") return {"SHA-256", MBEDTLS_MD_SHA256};
if (value == "SHA-512") return {"SHA-512", MBEDTLS_MD_SHA512};
throw std::runtime_error("Unsupported algorithm. Use SHA-1, SHA-256, or SHA-512.");
}
int parseDigits(const std::string& raw) {
int v = std::stoi(raw);
if (v != 6 && v != 8) throw std::runtime_error("Digits must be 6 or 8.");
return v;
}
int parsePeriod(const std::string& raw) {
int v = std::stoi(raw);
if (v != 30 && v != 60) throw std::runtime_error("Period must be 30 or 60.");
return v;
}
int parseDrift(const std::string& raw) {
int v = std::stoi(raw);
if (v < 0 || v > 10) throw std::runtime_error("Drift must be in range 0..10.");
return v;
}
std::string requireOption(const std::map<std::string, std::string>& options, const std::string& key) {
const auto it = options.find(key);
if (it == options.end() || it->second.empty()) throw std::runtime_error("Missing --" + key);
return it->second;
}
std::string validateAccount(const std::string& value) {
if (value.find(':') == std::string::npos) throw std::runtime_error("Account must be issuer:username format.");
return value;
}
std::string base32Encode(const std::vector<unsigned char>& data) {
static const std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
int bits = 0;
int value = 0;
std::string out;
for (unsigned char b : data) {
value = (value << 8) | b;
bits += 8;
while (bits >= 5) {
out.push_back(alphabet[(value >> (bits - 5)) & 31]);
bits -= 5;
}
}
if (bits > 0) out.push_back(alphabet[(value << (5 - bits)) & 31]);
return out;
}
std::vector<unsigned char> base32Decode(const std::string& text) {
static const std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
std::string clean;
for (char c : text) {
if (std::isspace(static_cast<unsigned char>(c)) || c == '=') continue;
clean.push_back(static_cast<char>(std::toupper(static_cast<unsigned char>(c))));
}
if (clean.empty()) throw std::runtime_error("Invalid base32 secret.");
int bits = 0;
int value = 0;
std::vector<unsigned char> out;
for (char c : clean) {
std::size_t idx = alphabet.find(c);
if (idx == std::string::npos) throw std::runtime_error("Invalid base32 secret.");
value = (value << 5) | static_cast<int>(idx);
bits += 5;
if (bits >= 8) {
out.push_back(static_cast<unsigned char>((value >> (bits - 8)) & 0xFF));
bits -= 8;
}
}
return out;
}
std::vector<unsigned char> hmacBytes(
const std::vector<unsigned char>& key,
const std::vector<unsigned char>& msg,
mbedtls_md_type_t mdType
) {
const mbedtls_md_info_t* mdInfo = mbedtls_md_info_from_type(mdType);
if (!mdInfo) throw std::runtime_error("Hash algorithm unavailable.");
std::vector<unsigned char> out(mbedtls_md_get_size(mdInfo));
const int rc = mbedtls_md_hmac(mdInfo, key.data(), key.size(), msg.data(), msg.size(), out.data());
if (rc != 0) throwMbed(rc, "mbedtls_md_hmac");
return out;
}
std::string formatCode(std::uint32_t value, int digits) {
const std::uint32_t mod = digits == 6 ? 1000000u : 100000000u;
std::ostringstream out;
out << std::setw(digits) << std::setfill('0') << (value % mod);
return out.str();
}
std::string totpAt(
const std::string& secretB32,
int digits,
mbedtls_md_type_t mdType,
std::uint64_t counter
) {
const auto secret = base32Decode(secretB32);
std::vector<unsigned char> msg(8);
for (int i = 7; i >= 0; i--) {
msg[i] = static_cast<unsigned char>(counter & 0xFFu);
counter >>= 8;
}
const auto digest = hmacBytes(secret, msg, mdType);
const int offset = digest.back() & 0x0F;
std::uint32_t binary = ((digest[offset] & 0x7F) << 24)
| ((digest[offset + 1] & 0xFF) << 16)
| ((digest[offset + 2] & 0xFF) << 8)
| (digest[offset + 3] & 0xFF);
return formatCode(binary, digits);
}
TotpView totpNow(
const std::string& secretB32,
int digits,
int period,
mbedtls_md_type_t mdType
) {
const auto now = static_cast<std::uint64_t>(std::time(nullptr));
const std::uint64_t counter = now / static_cast<std::uint64_t>(period);
const int remaining = period - static_cast<int>(now % static_cast<std::uint64_t>(period));
return {
totpAt(secretB32, digits, mdType, counter),
totpAt(secretB32, digits, mdType, counter + 1),
remaining
};
}
std::optional<int> verifyTotp(
const std::string& secretB32,
const std::string& code,
int digits,
int period,
mbedtls_md_type_t mdType,
int drift
) {
if (code.size() != static_cast<std::size_t>(digits)
|| !std::all_of(code.begin(), code.end(), [](char c) { return std::isdigit(static_cast<unsigned char>(c)); })) {
throw std::runtime_error("Code must be a numeric string matching account digits.");
}
const auto now = static_cast<std::uint64_t>(std::time(nullptr));
const std::int64_t counter = static_cast<std::int64_t>(now / static_cast<std::uint64_t>(period));
for (int d = -drift; d <= drift; d++) {
const std::int64_t c = counter + d;
if (c < 0) continue;
if (totpAt(secretB32, digits, mdType, static_cast<std::uint64_t>(c)) == code) return d;
}
return std::nullopt;
}
std::string percentEncode(const std::string& s) {
std::ostringstream out;
for (unsigned char c : s) {
if ((c >= 'A' && c <= 'Z')
|| (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9')
|| c == '-' || c == '_' || c == '.' || c == '~') {
out << c;
} else {
out << '%' << std::uppercase << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(c)
<< std::nouppercase << std::dec;
}
}
return out.str();
}
std::string otpauthUri(
const std::string& account,
const std::string& secret,
int digits,
int period,
const std::string& algoLabel
) {
const auto pos = account.find(':');
const std::string issuer = account.substr(0, pos);
const std::string username = account.substr(pos + 1);
std::ostringstream out;
out << "otpauth://totp/" << percentEncode(issuer + ":" + username)
<< "?secret=" << percentEncode(secret)
<< "&issuer=" << percentEncode(issuer)
<< "&algorithm=" << percentEncode(std::string(algoLabel).erase(algoLabel.find('-'), 1))
<< "&digits=" << digits
<< "&period=" << period;
return out.str();
}
std::vector<unsigned char> deriveKey(const std::string& master, const std::vector<unsigned char>& salt) {
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
const mbedtls_md_info_t* mdInfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
if (!mdInfo) throw std::runtime_error("SHA-256 unavailable for PBKDF2.");
int rc = mbedtls_md_setup(&ctx, mdInfo, 1);
if (rc != 0) {
mbedtls_md_free(&ctx);
throwMbed(rc, "mbedtls_md_setup");
}
std::vector<unsigned char> key(32);
rc = mbedtls_pkcs5_pbkdf2_hmac(
&ctx,
reinterpret_cast<const unsigned char*>(master.data()),
master.size(),
salt.data(),
salt.size(),
PBKDF2_ITERS,
key.size(),
key.data()
);
mbedtls_md_free(&ctx);
if (rc != 0) throwMbed(rc, "mbedtls_pkcs5_pbkdf2_hmac");
return key;
}
std::vector<unsigned char> aesGcmEncrypt(
const std::vector<unsigned char>& plaintext,
const std::vector<unsigned char>& key,
const std::vector<unsigned char>& nonce,
std::vector<unsigned char>& tagOut
) {
mbedtls_gcm_context gcm;
mbedtls_gcm_init(&gcm);
int rc = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key.data(), static_cast<unsigned int>(key.size() * 8));
if (rc != 0) {
mbedtls_gcm_free(&gcm);
throwMbed(rc, "mbedtls_gcm_setkey");
}
std::vector<unsigned char> ciphertext(plaintext.size());
tagOut.resize(16);
rc = mbedtls_gcm_crypt_and_tag(
&gcm,
MBEDTLS_GCM_ENCRYPT,
plaintext.size(),
nonce.data(),
nonce.size(),
nullptr,
0,
plaintext.data(),
ciphertext.data(),
tagOut.size(),
tagOut.data()
);
mbedtls_gcm_free(&gcm);
if (rc != 0) throwMbed(rc, "mbedtls_gcm_crypt_and_tag");
return ciphertext;
}
std::vector<unsigned char> aesGcmDecrypt(
const std::vector<unsigned char>& ciphertext,
const std::vector<unsigned char>& key,
const std::vector<unsigned char>& nonce,
const std::vector<unsigned char>& tag
) {
mbedtls_gcm_context gcm;
mbedtls_gcm_init(&gcm);
int rc = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key.data(), static_cast<unsigned int>(key.size() * 8));
if (rc != 0) {
mbedtls_gcm_free(&gcm);
throwMbed(rc, "mbedtls_gcm_setkey");
}
std::vector<unsigned char> plaintext(ciphertext.size());
rc = mbedtls_gcm_auth_decrypt(
&gcm,
ciphertext.size(),
nonce.data(),
nonce.size(),
nullptr,
0,
tag.data(),
tag.size(),
ciphertext.data(),
plaintext.data()
);
mbedtls_gcm_free(&gcm);
if (rc != 0) throw std::runtime_error("Wrong master password or corrupted encrypted store.");
return plaintext;
}
nlohmann::json encryptDb(const nlohmann::json& db, const std::string& master, Rng& rng) {
const auto salt = rng.bytes(16);
const auto nonce = rng.bytes(12);
const auto key = deriveKey(master, salt);
const std::string plain = db.dump(2);
const std::vector<unsigned char> plainBytes(plain.begin(), plain.end());
std::vector<unsigned char> tag;
const auto ciphertext = aesGcmEncrypt(plainBytes, key, nonce, tag);
return nlohmann::json{
{"version", 1},
{"kdf", "PBKDF2-SHA256"},
{"iterations", PBKDF2_ITERS},
{"salt", base64Encode(salt)},
{"nonce", base64Encode(nonce)},
{"tag", base64Encode(tag)},
{"data", base64Encode(ciphertext)}
};
}
nlohmann::json decryptDb(const nlohmann::json& enc, const std::string& master) {
const auto salt = base64Decode(enc.at("salt").get<std::string>());
const auto nonce = base64Decode(enc.at("nonce").get<std::string>());
const auto tag = base64Decode(enc.at("tag").get<std::string>());
const auto data = base64Decode(enc.at("data").get<std::string>());
const auto key = deriveKey(master, salt);
const auto plain = aesGcmDecrypt(data, key, nonce, tag);
return nlohmann::json::parse(std::string(plain.begin(), plain.end()));
}
nlohmann::json loadStore(const std::filesystem::path& storePath, const std::string& master) {
if (!std::filesystem::exists(storePath)) {
return nlohmann::json{{"version", 1}, {"accounts", nlohmann::json::array()}};
}
const auto enc = nlohmann::json::parse(readText(storePath));
auto db = decryptDb(enc, master);
if (!db.contains("accounts") || !db["accounts"].is_array()) {
throw std::runtime_error("Encrypted store is malformed.");
}
return db;
}
void saveStore(const std::filesystem::path& storePath, const std::string& master, const nlohmann::json& db, Rng& rng) {
const auto enc = encryptDb(db, master, rng);
writeText(storePath, enc.dump(2) + "\n");
}
nlohmann::json& accountByLabel(nlohmann::json& db, const std::string& label) {
for (auto& item : db["accounts"]) {
if (item.value("label", "") == label) return item;
}
throw std::runtime_error("Account not found: " + label);
}
int commandGenerate(const ParsedArgs& args, Rng& rng) {
const std::string master = requireOption(args.options, "master");
const std::string account = validateAccount(requireOption(args.options, "account"));
const int length = args.options.count("length") ? std::stoi(args.options.at("length")) : 20;
if (length < 10 || length > 128) throw std::runtime_error("Length must be between 10 and 128.");
const int digits = parseDigits(args.options.count("digits") ? args.options.at("digits") : "6");
const int period = parsePeriod(args.options.count("period") ? args.options.at("period") : "30");
const AlgoSpec algo = normalizeAlgorithm(args.options.count("algorithm") ? args.options.at("algorithm") : "SHA-1");
const auto storePath = std::filesystem::absolute(args.options.count("store") ? args.options.at("store") : DEFAULT_STORE.string());
nlohmann::json db = loadStore(storePath, master);
for (const auto& item : db["accounts"]) {
if (item.value("label", "") == account) throw std::runtime_error("Duplicate account: " + account);
}
const std::string secret = base32Encode(rng.bytes(static_cast<std::size_t>(length)));
db["accounts"].push_back({
{"label", account},
{"secret", secret},
{"digits", digits},
{"period", period},
{"algorithm", algo.label},
{"createdAt", std::to_string(std::time(nullptr))}
});
saveStore(storePath, master, db, rng);
const auto uri = otpauthUri(account, secret, digits, period, algo.label);
const auto view = totpNow(secret, digits, period, algo.mdType);
std::cout << "Account added : " << account << "\n";
std::cout << "Store file : " << storePath << "\n";
std::cout << "Secret (base32): " << secret << "\n";
std::cout << "otpauth URI : " << uri << "\n";
std::cout << "Current code : " << view.current << "\n";
std::cout << "Next code : " << view.next << "\n";
std::cout << "Expires in : " << view.remaining << "s\n";
return 0;
}
int commandCode(const ParsedArgs& args) {
const std::string master = requireOption(args.options, "master");
const std::string account = validateAccount(requireOption(args.options, "account"));
const auto storePath = std::filesystem::absolute(args.options.count("store") ? args.options.at("store") : DEFAULT_STORE.string());
nlohmann::json db = loadStore(storePath, master);
auto& acc = accountByLabel(db, account);
const AlgoSpec algo = normalizeAlgorithm(acc.value("algorithm", "SHA-1"));
const auto view = totpNow(acc.at("secret").get<std::string>(), acc.at("digits").get<int>(), acc.at("period").get<int>(), algo.mdType);
std::cout << "Account : " << acc.at("label").get<std::string>() << "\n";
std::cout << "Current code : " << view.current << "\n";
std::cout << "Next code : " << view.next << "\n";
std::cout << "Expires in : " << view.remaining << "s\n";
return 0;
}
int commandVerify(const ParsedArgs& args) {
const std::string master = requireOption(args.options, "master");
const std::string account = validateAccount(requireOption(args.options, "account"));
const std::string code = requireOption(args.options, "code");
const int drift = parseDrift(args.options.count("drift") ? args.options.at("drift") : "1");
const auto storePath = std::filesystem::absolute(args.options.count("store") ? args.options.at("store") : DEFAULT_STORE.string());
nlohmann::json db = loadStore(storePath, master);
auto& acc = accountByLabel(db, account);
const AlgoSpec algo = normalizeAlgorithm(acc.value("algorithm", "SHA-1"));
const auto result = verifyTotp(
acc.at("secret").get<std::string>(),
code,
acc.at("digits").get<int>(),
acc.at("period").get<int>(),
algo.mdType,
drift
);
if (!result.has_value()) {
std::cout << "Verification: INVALID (expired or incorrect code)\n";
return 2;
}
if (*result == 0) std::cout << "Verification: VALID (current window)\n";
else std::cout << "Verification: VALID (window offset " << *result << ")\n";
return 0;
}
int commandList(const ParsedArgs& args) {
const std::string master = requireOption(args.options, "master");
const auto storePath = std::filesystem::absolute(args.options.count("store") ? args.options.at("store") : DEFAULT_STORE.string());
nlohmann::json db = loadStore(storePath, master);
if (db["accounts"].empty()) {
std::cout << "No accounts stored.\n";
return 0;
}
std::cout << "Accounts in " << storePath << ":\n";
for (const auto& acc : db["accounts"]) {
std::cout << "- " << acc.value("label", "")
<< " | " << acc.value("algorithm", "")
<< " | digits=" << acc.value("digits", 6)
<< " | period=" << acc.value("period", 30) << "s\n";
}
return 0;
}
int runDemo(Rng& rng) {
ParsedArgs g;
g.positional = {"generate"};
g.options["master"] = "demo-master-password";
g.options["store"] = std::filesystem::absolute("totp_demo.enc.json").string();
g.options["account"] = "DemoIssuer:demo.user@example.com";
g.options["length"] = "20";
g.options["digits"] = "6";
g.options["period"] = "30";
g.options["algorithm"] = "SHA-1";
if (std::filesystem::exists(g.options["store"])) std::filesystem::remove(g.options["store"]);
std::cout << "Running demo...\n\n";
commandGenerate(g, rng);
ParsedArgs c;
c.positional = {"code"};
c.options["master"] = g.options["master"];
c.options["store"] = g.options["store"];
c.options["account"] = g.options["account"];
nlohmann::json db = loadStore(c.options["store"], c.options["master"]);
auto& acc = accountByLabel(db, c.options["account"]);
const AlgoSpec algo = normalizeAlgorithm(acc.value("algorithm", "SHA-1"));
const auto view = totpNow(acc.at("secret").get<std::string>(), acc.at("digits").get<int>(), acc.at("period").get<int>(), algo.mdType);
ParsedArgs v1;
v1.positional = {"verify"};
v1.options["master"] = g.options["master"];
v1.options["store"] = g.options["store"];
v1.options["account"] = g.options["account"];
v1.options["code"] = view.current;
v1.options["drift"] = "1";
std::cout << "\nDemo verify with correct code (" << view.current << ")\n";
commandVerify(v1);
ParsedArgs v2 = v1;
v2.options["code"] = "000000";
std::cout << "\nDemo verify with incorrect code (000000)\n";
return commandVerify(v2);
}
std::string usage() {
return
"Usage:\n"
" ./totp_tool generate --master <password> --account issuer:username [--length 20] [--digits 6|8] "
"[--period 30|60] [--algorithm SHA-1|SHA-256|SHA-512] [--store path]\n"
" ./totp_tool code --master <password> --account issuer:username [--store path]\n"
" ./totp_tool verify --master <password> --account issuer:username --code 123456 [--drift 1] [--store path]\n"
" ./totp_tool list --master <password> [--store path]\n"
"\nNo arguments runs a demo workflow.";
}
} // namespace
int main(int argc, char** argv) {
try {
Rng rng;
ParsedArgs args = parseArgs(argc, argv);
if (args.positional.empty()) return runDemo(rng);
const std::string cmd = args.positional[0];
if (cmd == "generate") return commandGenerate(args, rng);
if (cmd == "code") return commandCode(args);
if (cmd == "verify") return commandVerify(args);
if (cmd == "list") return commandList(args);
throw std::runtime_error("Unknown subcommand.\n" + usage());
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
return 1;
}
}