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 →