Skip to content

OTP Code Generator — C++ source

Generate time-based one-time passwords (RFC 6238 TOTP) from a Base32 secret, with selectable algorithm, digit count, and period - updating live, entirely in your browser.

This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// otp-code-generator — C++ port: TOTP (RFC 6238) / HOTP (RFC 4226) generator.
//
// Self-contained display port of the CosmoDev OTP Code Generator tool — same
// contract as cli/otp-code-generator/otp-code-generator.go (the live Go twin)
// and src/lib/otp.ts (canonical TypeScript, which wraps `otpauth`). SHA1, HMAC
// and base32 are hand-rolled (standard library only), matching the Rust port
// in this directory. Only the SHA1 path is ported — the TS default and the
// algorithm every published RFC 6238/4226 test vector uses; SHA256/512 follow
// the same HMAC construction in the Go twin.
#include <array>
#include <cstdint>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <optional>
#include <string>
#include <vector>

namespace otp {

constexpr std::size_t kSha1Digest = 20;
constexpr std::size_t kSha1Block = 64;

namespace detail {

inline uint32_t rol32(uint32_t v, int s) { return (v << s) | (v >> (32 - s)); }

// SHA1 (FIPS 180-4): compress one 64-byte block into the running state.
inline void sha1_block(uint32_t h[5], const uint8_t* p) {
    uint32_t w[80];
    for (int i = 0; i < 16; i++)
        w[i] = (static_cast<uint32_t>(p[4 * i]) << 24) | (static_cast<uint32_t>(p[4 * i + 1]) << 16) |
               (static_cast<uint32_t>(p[4 * i + 2]) << 8) | static_cast<uint32_t>(p[4 * i + 3]);
    for (int i = 16; i < 80; i++)
        w[i] = rol32(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
    uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
    for (int i = 0; i < 80; i++) {
        uint32_t f, k;
        if (i < 20)      { f = (b & c) | (~b & d);          k = 0x5A827999u; }
        else if (i < 40) { f = b ^ c ^ d;                   k = 0x6ED9EBA1u; }
        else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDCu; }
        else             { f = b ^ c ^ d;                   k = 0xCA62C1D6u; }
        const uint32_t t = rol32(a, 5) + f + e + k + w[i];
        e = d; d = c; c = rol32(b, 30); b = a; a = t;
    }
    h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e;
}

// SHA1 over msg: full blocks straight from the input, then 0x80 + zero
// padding + the 64-bit big-endian bit length (tail may straddle 2 blocks).
inline void sha1(const uint8_t* msg, std::size_t len, uint8_t digest[kSha1Digest]) {
    uint32_t h[5] = {0x67452301u, 0xEFCDAB89u, 0x98BADCFEu, 0x10325476u, 0xC3D2E1F0u};
    const uint64_t bits = static_cast<uint64_t>(len) * 8;
    std::size_t i = 0;
    while (i + kSha1Block <= len) {
        sha1_block(h, msg + i);
        i += kSha1Block;
    }
    uint8_t tail[128] = {0};
    const std::size_t rem = len - i;
    std::memcpy(tail, msg + i, rem);
    tail[rem] = 0x80;
    const std::size_t n = (rem + 9 > kSha1Block) ? 128 : kSha1Block;
    for (int j = 0; j < 8; j++) tail[n - 1 - j] = static_cast<uint8_t>(bits >> (8 * j));
    sha1_block(h, tail);
    if (n == 128) sha1_block(h, tail + kSha1Block);
    for (int j = 0; j < 5; j++) {
        digest[4 * j] = static_cast<uint8_t>(h[j] >> 24);
        digest[4 * j + 1] = static_cast<uint8_t>(h[j] >> 16);
        digest[4 * j + 2] = static_cast<uint8_t>(h[j] >> 8);
        digest[4 * j + 3] = static_cast<uint8_t>(h[j]);
    }
}

// HMAC-SHA1 (RFC 2104) with the fixed 8-byte counter message of RFC 4226
// §5.2. Keys longer than the 64-byte block are hashed first — like crypto/hmac.
inline void hmac_sha1_counter(const uint8_t* key, std::size_t keylen, const uint8_t counter[8],
                              uint8_t out[kSha1Digest]) {
    uint8_t k[kSha1Block] = {0}, inner[72], outer[84], ih[kSha1Digest];
    if (keylen > kSha1Block)
        sha1(key, keylen, k);
    else
        std::memcpy(k, key, keylen);
    for (int i = 0; i < 64; i++) inner[i] = static_cast<uint8_t>(k[i] ^ 0x36);
    std::memcpy(inner + 64, counter, 8);
    sha1(inner, sizeof inner, ih);
    for (int i = 0; i < 64; i++) outer[i] = static_cast<uint8_t>(k[i] ^ 0x5c);
    std::memcpy(outer + 64, ih, kSha1Digest);
    sha1(outer, sizeof outer, out);
}

// Base32 (RFC 4648) decode: A-Z2-7, 5 bits per char, msb-first.
inline std::optional<std::vector<uint8_t>> base32_decode(const std::string& in) {
    std::vector<uint8_t> out;
    uint32_t acc = 0;
    int bits = 0;
    for (char ch : in) {
        const uint8_t c = static_cast<uint8_t>(ch);
        int v;
        if (c >= 'A' && c <= 'Z') v = c - 'A';
        else if (c >= '2' && c <= '7') v = c - '2' + 26;
        else return std::nullopt;
        acc = ((acc << 5) | static_cast<uint32_t>(v)) & 0x1fff;
        bits += 5;
        if (bits >= 8) {
            bits -= 8;
            out.push_back(static_cast<uint8_t>(acc >> bits));
        }
    }
    return out;
}

} // namespace detail

struct Options {
    std::string secret; // Base32 (RFC 4648); spaces/case tolerated
    int digits = 6;     // <= 0 falls back to 6 (withDefaults in the Go twin)
    int period = 30;    // seconds, <= 0 falls back to 30
};

// decodeSecret() in the Go twin: strip whitespace, uppercase, strip trailing
// '=' padding, then base32-decode. nullopt when the secret is invalid.
inline std::optional<std::vector<uint8_t>> decodeSecret(const std::string& secret) {
    std::string norm;
    for (char ch : secret) {
        if (std::isspace(static_cast<unsigned char>(ch))) continue;
        norm += static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
    }
    while (!norm.empty() && norm.back() == '=') norm.pop_back();
    return detail::base32_decode(norm);
}

// TOTP at timestamp_ms — Generate() in the Go twin (RFC 6238: counter =
// ts/1000/period, HMAC-SHA1 over the 8-byte big-endian counter, dynamic
// truncation, mod 10^digits, zero-padded). nullopt on an invalid secret,
// mirroring generateTotp() returning null in src/lib/otp.ts.
inline std::optional<std::string> generate(const Options& opts, int64_t timestamp_ms) {
    Options o = opts;
    if (o.digits <= 0) o.digits = 6;
    if (o.period <= 0) o.period = 30;
    const auto key = decodeSecret(o.secret);
    if (!key) return std::nullopt;

    const uint64_t counter = static_cast<uint64_t>(timestamp_ms) / 1000 / static_cast<uint64_t>(o.period);
    std::array<uint8_t, 8> msg{};
    for (int i = 0; i < 8; i++) msg[i] = static_cast<uint8_t>(counter >> (8 * (7 - i)));
    std::array<uint8_t, kSha1Digest> mac{};
    detail::hmac_sha1_counter(key->data(), key->size(), msg.data(), mac.data());

    const int off = mac[19] & 0x0f; // dynamic truncation, RFC 4226 §5.4
    const uint32_t bin = (static_cast<uint32_t>(mac[off] & 0x7f) << 24) |
                         (static_cast<uint32_t>(mac[off + 1]) << 16) |
                         (static_cast<uint32_t>(mac[off + 2]) << 8) |
                         static_cast<uint32_t>(mac[off + 3]);

    uint64_t mod = 1;
    for (int i = 0; i < o.digits; i++) mod *= 10;
    std::string token(static_cast<std::size_t>(o.digits), '0');
    uint64_t code = bin % mod;
    for (int i = o.digits - 1; i >= 0; i--) {
        token[static_cast<std::size_t>(i)] = static_cast<char>('0' + code % 10);
        code /= 10;
    }
    return token;
}

// Validate() in the Go twin: the current period and ±1 adjacent periods
// (otpauth's default window=1). Plain == compare — the Go twin uses the
// constant-time hmac.Equal for this compare.
inline bool validate(const std::string& token, const Options& opts, int64_t timestamp_ms) {
    const int period = opts.period > 0 ? opts.period : 30;
    const int64_t window = static_cast<int64_t>(period) * 1000;
    for (int64_t ts : {timestamp_ms, timestamp_ms - window, timestamp_ms + window}) {
        const auto candidate = generate(opts, ts);
        if (candidate && *candidate == token) return true;
    }
    return false;
}

} // namespace otp

int main() {
    // RFC 6238 appendix-B vectors (SHA1, 8 digits, 30s):
    // secret = base32("12345678901234567890").
    const otp::Options o8{"GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 8, 30};
    const std::pair<int64_t, const char*> vectors[] = {
        {59000, "94287082"},
        {1111111109000LL, "07081804"},
        {1234567890000LL, "89005924"},
        {2000000000000LL, "69279037"},
    };
    for (const auto& [ts, want] : vectors)
        std::printf("ts=%-13lld token=%s want=%s\n", static_cast<long long>(ts),
                    otp::generate(o8, ts).value_or("invalid").c_str(), want);

    // Tool defaults (6 digits) + whitespace/case-tolerant secret entry.
    const otp::Options o6{"gezd gnbv gy3t qojq gezd gnbv gy3t qojq"};
    const std::string token = otp::generate(o6, 59000).value_or("invalid");
    std::printf("6-digit=%s validate(now)=%d validate(+1 period)=%d validate(wrong)=%d\n",
                token.c_str(), otp::validate(token, o6, 59000) ? 1 : 0,
                otp::validate(token, o6, 89000) ? 1 : 0, otp::validate("123789", o6, 59000) ? 1 : 0);
    return 0;
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →