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 from raw bytes, matching the dependency-free
* 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 <stdint.h>
#include <stdio.h>
#include <string.h>
static 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. */
static void sha1_block(uint32_t h[5], const uint8_t p[64]) {
uint32_t w[80];
for (int i = 0; i < 16; i++)
w[i] = ((uint32_t)p[4 * i] << 24) | ((uint32_t)p[4 * i + 1] << 16) |
((uint32_t)p[4 * i + 2] << 8) | (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; }
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). */
static void sha1(const uint8_t *msg, size_t len, uint8_t digest[20]) {
uint32_t h[5] = {0x67452301u, 0xEFCDAB89u, 0x98BADCFEu, 0x10325476u, 0xC3D2E1F0u};
uint64_t bits = (uint64_t)len * 8;
size_t i = 0;
while (i + 64 <= len) {
sha1_block(h, msg + i);
i += 64;
}
uint8_t tail[128] = {0};
size_t rem = len - i;
memcpy(tail, msg + i, rem);
tail[rem] = 0x80;
size_t n = (rem + 9 > 64) ? 128 : 64;
for (int j = 0; j < 8; j++) tail[n - 1 - j] = (uint8_t)(bits >> (8 * j));
sha1_block(h, tail);
if (n == 128) sha1_block(h, tail + 64);
for (int j = 0; j < 5; j++) {
digest[4 * j] = (uint8_t)(h[j] >> 24);
digest[4 * j + 1] = (uint8_t)(h[j] >> 16);
digest[4 * j + 2] = (uint8_t)(h[j] >> 8);
digest[4 * j + 3] = (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. */
static void hmac_sha1_counter(const uint8_t *key, size_t keylen, const uint8_t counter[8], uint8_t out[20]) {
uint8_t k[64] = {0}, inner[72], outer[84], ih[20];
if (keylen > 64) sha1(key, keylen, k);
else memcpy(k, key, keylen);
for (int i = 0; i < 64; i++) inner[i] = k[i] ^ 0x36;
memcpy(inner + 64, counter, 8);
sha1(inner, sizeof inner, ih);
for (int i = 0; i < 64; i++) outer[i] = k[i] ^ 0x5c;
memcpy(outer + 64, ih, 20);
sha1(outer, sizeof outer, out);
}
/* Base32 (RFC 4648) decode: A-Z2-7, 5 bits per char, msb-first. Returns the
* decoded byte count, or -1 on a char outside the alphabet / a full buffer. */
static int base32_decode(const char *in, uint8_t *out, size_t cap) {
uint32_t acc = 0;
int bits = 0;
size_t n = 0;
for (const char *p = in; *p; p++) {
int v;
if (*p >= 'A' && *p <= 'Z') v = *p - 'A';
else if (*p >= '2' && *p <= '7') v = *p - '2' + 26;
else return -1;
acc = ((acc << 5) | (uint32_t)v) & 0x1fff;
bits += 5;
if (bits >= 8) {
bits -= 8;
if (n >= cap) return -1;
out[n++] = (uint8_t)(acc >> bits);
}
}
return (int)n;
}
typedef struct {
const char *secret; /* Base32 (RFC 4648); spaces/case tolerated */
int digits; /* default 6 */
int period; /* seconds, default 30 */
} totp_opts;
/* decodeSecret() in the Go twin: strip whitespace, uppercase, strip trailing
* '=' padding, then base32-decode. Returns the key length, or -1 if invalid. */
static int decode_secret(const char *secret, uint8_t *out, size_t cap) {
char norm[256];
size_t n = 0;
for (const char *p = secret; *p; p++) {
char c = *p;
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue;
if (c >= 'a' && c <= 'z') c = (char)(c - 'a' + 'A');
if (n >= sizeof norm - 1) return -1;
norm[n++] = c;
}
while (n > 0 && norm[n - 1] == '=') n--;
norm[n] = '\0';
return base32_decode(norm, out, cap);
}
/* 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). 1 on success, 0 on a bad secret. */
static int totp_generate(totp_opts opts, int64_t timestamp_ms, char *token, size_t cap) {
if (opts.digits <= 0) opts.digits = 6; /* withDefaults() in the Go twin */
if (opts.period <= 0) opts.period = 30;
if ((size_t)opts.digits >= cap) return 0;
uint8_t key[64];
int keylen = decode_secret(opts.secret, key, sizeof key);
if (keylen <= 0) return 0;
uint64_t counter = (uint64_t)timestamp_ms / 1000 / (uint64_t)opts.period;
uint8_t msg[8], mac[20];
for (int i = 0; i < 8; i++) msg[i] = (uint8_t)(counter >> (8 * (7 - i)));
hmac_sha1_counter(key, (size_t)keylen, msg, mac);
int off = mac[19] & 0x0f; /* dynamic truncation, RFC 4226 §5.4 */
uint32_t bin = ((uint32_t)(mac[off] & 0x7f) << 24) | ((uint32_t)mac[off + 1] << 16) |
((uint32_t)mac[off + 2] << 8) | (uint32_t)mac[off + 3];
uint64_t mod = 1;
for (int i = 0; i < opts.digits; i++) mod *= 10;
snprintf(token, cap, "%0*llu", opts.digits, bin % mod);
return 1;
}
/* Validate() in the Go twin: the current period and ±1 adjacent periods
* (otpauth's default window=1). Plain strcmp — the Go twin uses the
* constant-time hmac.Equal for this compare. */
static int totp_validate(const char *token, totp_opts opts, int64_t timestamp_ms) {
if (opts.period <= 0) opts.period = 30; /* same defaults as generate */
int64_t window = (int64_t)opts.period * 1000;
char candidate[16];
for (int k = 0; k < 3; k++) {
int64_t ts = k == 0 ? timestamp_ms : timestamp_ms + (k == 1 ? -window : window);
if (totp_generate(opts, ts, candidate, sizeof candidate) && strcmp(candidate, token) == 0)
return 1;
}
return 0;
}
int main(void) {
/* RFC 6238 appendix-B vectors (SHA1, 8 digits, 30s):
* secret = base32("12345678901234567890"). */
totp_opts o8 = {"GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 8, 30};
const int64_t ts[4] = {59000, 1111111109000LL, 1234567890000LL, 2000000000000LL};
const char *want[4] = {"94287082", "07081804", "89005924", "69279037"};
char token[16];
for (int i = 0; i < 4; i++) {
totp_generate(o8, ts[i], token, sizeof token);
printf("ts=%-13lld token=%s want=%s %s\n", (long long)ts[i], token, want[i],
strcmp(token, want[i]) == 0 ? "ok" : "MISMATCH");
}
/* Tool defaults (6 digits) + whitespace/case-tolerant secret entry. */
totp_opts o6 = {"gezd gnbv gy3t qojq gezd gnbv gy3t qojq", 6, 30};
totp_generate(o6, 59000, token, sizeof token);
printf("6-digit=%s validate(now)=%d validate(+1 period)=%d validate(wrong)=%d\n",
token, totp_validate(token, o6, 59000), totp_validate(token, o6, 89000),
totp_validate("123789", o6, 59000));
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 →