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 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 →