Skip to content

HMAC Generator — C source

Generate a keyed-hash HMAC (SHA-1/256/384/512) for a message and secret. Runs entirely in your browser via Web Crypto, with a shareable link to your exact input.

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

/*
 * hmac-generator — RFC 2104 keyed-hash HMAC of a UTF-8 message, hex output.
 *
 * Language: C (C11, standard library only)
 * Source:   CosmoDev polyglot showcase port of the `hmac-generator` tool,
 *           ported from src/lib/hmac.ts (the canonical TypeScript lib).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * The TypeScript reference delegates to `crypto.subtle.sign` with an HMAC
 * key, and the Rust port uses the RustCrypto `hmac`/`sha1`/`sha2` crates.
 * ISO C has no crypto library, so the ecosystem equivalent would be
 * OpenSSL's libcrypto (`HMAC()`); to keep this port standard-library-only,
 * the four SHA cores are implemented below from FIPS 180-4 — mirroring the
 * C port of the `hash` tool — with HMAC layered on top from RFC 2104:
 *
 *     K0   = secret, hashed when longer than the block size, zero-padded
 *     tag  = H((K0 ^ opad) || H((K0 ^ ipad) || message))
 *
 * A C string carries no encoding, so callers pass the text's UTF-8 bytes —
 * the same byte sequence a browser hands to crypto.subtle.sign. A NULL or
 * empty algorithm name selects SHA-256, standing in for the optional
 * parameter default the TypeScript reference declares. An empty secret is
 * rejected: libcrypto's HMAC() would accept a zero-length key, but the
 * TypeScript reference (SubtleCrypto) refuses one, and matching that
 * contract keeps the ports in parity.
 *
 * SHA-1 is offered because the tool lists it for legacy compatibility; it
 * is not collision-resistant and must not authenticate anything.
 *
 * Build: cc -std=c11 hmac.c
 */

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

/* ------------------------------------------------------------- algorithm set --- */

/** Canonical algorithm names. The spellings match the TypeScript union so the
 * same string works across every port. */
static const char HMAC_SHA1_NAME[] = "SHA-1";
static const char HMAC_SHA256_NAME[] = "SHA-256"; /* default algorithm */
static const char HMAC_SHA384_NAME[] = "SHA-384";
static const char HMAC_SHA512_NAME[] = "SHA-512";

/** The algorithms the tool knows. HMAC_UNKNOWN is the C standing of the
 * ValueError the Python port raises and HmacError::UnknownAlgorithm in the
 * Rust port. */
typedef enum {
    HMAC_SHA1 = 0,
    HMAC_SHA256,
    HMAC_SHA384,
    HMAC_SHA512,
    HMAC_ALGO_COUNT,
    HMAC_UNKNOWN = -1
} HmacAlgorithm;

/** Longest digest, in bytes — size raw tag buffers with this. */
#define HMAC_MAX_DIGEST_LEN 64
/** Longest HMAC key block, in bytes — SHA-384/SHA-512 use 128. */
#define HMAC_MAX_BLOCK_LEN 128
/** Longest hex output, including the NUL terminator. */
#define HMAC_MAX_HEX_LEN (HMAC_MAX_DIGEST_LEN * 2 + 1)

/* ------------------------------------------------------------------ helpers --- */

static uint32_t rotl32(uint32_t x, unsigned n) { return (x << n) | (x >> (32 - n)); }
static uint32_t rotr32(uint32_t x, unsigned n) { return (x >> n) | (x << (32 - n)); }
static uint64_t rotr64(uint64_t x, unsigned n) { return (x >> n) | (x << (64 - n)); }

/** Lowercase hexadecimal encoding of `n` bytes; `out` needs 2n + 1 bytes. */
static void to_hex(const uint8_t *bytes, size_t n, char *out)
{
    static const char HEX[] = "0123456789abcdef";
    for (size_t i = 0; i < n; i++) {
        out[i * 2] = HEX[bytes[i] >> 4];
        out[i * 2 + 1] = HEX[bytes[i] & 0x0f];
    }
    out[n * 2] = '\0';
}

/** Big-endian store/load helpers — the FIPS word order, independent of host
 * endianness and of the htonl family's platform quirks. */
static void store_be32(uint8_t *p, uint32_t v)
{
    p[0] = (uint8_t)(v >> 24);
    p[1] = (uint8_t)(v >> 16);
    p[2] = (uint8_t)(v >> 8);
    p[3] = (uint8_t)v;
}

static void store_be64(uint8_t *p, uint64_t v)
{
    for (int i = 0; i < 8; i++)
        p[i] = (uint8_t)(v >> (56 - 8 * i));
}

static uint32_t load_be32(const uint8_t *p)
{
    return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
           ((uint32_t)p[2] << 8) | (uint32_t)p[3];
}

static uint64_t load_be64(const uint8_t *p)
{
    uint64_t v = 0;
    for (int i = 0; i < 8; i++)
        v = (v << 8) | p[i];
    return v;
}

/* ----------------------------------------------- SHA-1, FIPS 180-4 section 6.1 --- */

static void sha1_compress(uint32_t h[5], const uint8_t block[64])
{
    uint32_t w[80];
    for (int i = 0; i < 16; i++)
        w[i] = load_be32(block + (size_t)i * 4);
    for (int i = 16; i < 80; i++)
        w[i] = rotl32(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); /* Ch */
            k = 0x5a827999u;
        } else if (i < 40) {
            f = b ^ c ^ d; /* Parity */
            k = 0x6ed9eba1u;
        } else if (i < 60) {
            f = (b & c) | (b & d) | (c & d); /* Maj */
            k = 0x8f1bbcdcu;
        } else {
            f = b ^ c ^ d; /* Parity */
            k = 0xca62c1d6u;
        }
        uint32_t t = rotl32(a, 5) + f + e + k + w[i];
        e = d;
        d = c;
        c = rotl32(b, 30);
        b = a;
        a = t;
    }
    h[0] += a;
    h[1] += b;
    h[2] += c;
    h[3] += d;
    h[4] += e;
}

/**
 * One-shot SHA-1. Full blocks are compressed straight from the input; the
 * final one or two blocks carry the remainder plus the FIPS padding (0x80,
 * zeros, 64-bit big-endian bit length). No heap allocation.
 */
static void sha1_sum(const uint8_t *msg, size_t len, uint8_t out[20])
{
    uint32_t h[5] = { 0x67452301u, 0xefcdab89u, 0x98badcfeu, 0x10325476u, 0xc3d2e1f0u };

    size_t off = 0;
    while (len - off >= 64) {
        sha1_compress(h, msg + off);
        off += 64;
    }

    uint8_t last[128] = { 0 };
    size_t rem = len - off;
    memcpy(last, msg + off, rem);
    last[rem] = 0x80;
    size_t blocks = (rem + 9 <= 64) ? 1 : 2; /* msg + 0x80 + 8 length bytes */
    store_be64(last + blocks * 64 - 8, (uint64_t)len * 8);
    sha1_compress(h, last);
    if (blocks == 2)
        sha1_compress(h, last + 64);

    for (int i = 0; i < 5; i++)
        store_be32(out + (size_t)i * 4, h[i]);
}

/* --------------------------------------------- SHA-256, FIPS 180-4 section 6.2 --- */

/** Round constants: the first 32 bits of the fractional parts of the cube
 * roots of the first 64 primes (FIPS 180-4 section 4.2.2). */
static const uint32_t SHA256_K[64] = {
    0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u,
    0x923f82a4u, 0xab1c5ed5u, 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u,
    0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u, 0xe49b69c1u, 0xefbe4786u,
    0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
    0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u,
    0x06ca6351u, 0x14292967u, 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u,
    0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, 0xa2bfe8a1u, 0xa81a664bu,
    0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
    0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au,
    0x5b9cca4fu, 0x682e6ff3u, 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u,
    0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u,
};

static void sha256_compress(uint32_t h[8], const uint8_t block[64])
{
    uint32_t w[64];
    for (int i = 0; i < 16; i++)
        w[i] = load_be32(block + (size_t)i * 4);
    for (int i = 16; i < 64; i++) {
        uint32_t s0 = rotr32(w[i - 15], 7) ^ rotr32(w[i - 15], 18) ^ (w[i - 15] >> 3);
        uint32_t s1 = rotr32(w[i - 2], 17) ^ rotr32(w[i - 2], 19) ^ (w[i - 2] >> 10);
        w[i] = w[i - 16] + s0 + w[i - 7] + s1;
    }

    uint32_t a = h[0], b = h[1], c = h[2], d = h[3];
    uint32_t e = h[4], f = h[5], g = h[6], hh = h[7];
    for (int i = 0; i < 64; i++) {
        uint32_t S1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25);
        uint32_t ch = (e & f) ^ (~e & g);
        uint32_t t1 = hh + S1 + ch + SHA256_K[i] + w[i];
        uint32_t S0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22);
        uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
        uint32_t t2 = S0 + maj;
        hh = g;
        g = f;
        f = e;
        e = d + t1;
        d = c;
        c = b;
        b = a;
        a = t1 + t2;
    }
    h[0] += a;
    h[1] += b;
    h[2] += c;
    h[3] += d;
    h[4] += e;
    h[5] += f;
    h[6] += g;
    h[7] += hh;
}

/** One-shot SHA-256 — same padding shape as SHA-1 (64-byte blocks, 64-bit
 * big-endian bit length), different round function. */
static void sha256_sum(const uint8_t *msg, size_t len, uint8_t out[32])
{
    uint32_t h[8] = { 0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au,
                      0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u };

    size_t off = 0;
    while (len - off >= 64) {
        sha256_compress(h, msg + off);
        off += 64;
    }

    uint8_t last[128] = { 0 };
    size_t rem = len - off;
    memcpy(last, msg + off, rem);
    last[rem] = 0x80;
    size_t blocks = (rem + 9 <= 64) ? 1 : 2; /* msg + 0x80 + 8 length bytes */
    store_be64(last + blocks * 64 - 8, (uint64_t)len * 8);
    sha256_compress(h, last);
    if (blocks == 2)
        sha256_compress(h, last + 64);

    for (int i = 0; i < 8; i++)
        store_be32(out + (size_t)i * 4, h[i]);
}

/* ------------------------------- SHA-384 / SHA-512, FIPS 180-4 sections 6.3-6.4 --- */

/** Round constants: the first 64 bits of the fractional parts of the cube
 * roots of the first 80 primes (FIPS 180-4 section 4.2.3). */
static const uint64_t SHA512_K[80] = {
    0x428a2f98d728ae22ull, 0x7137449123ef65cdull, 0xb5c0fbcfec4d3b2full, 0xe9b5dba58189dbbcull,
    0x3956c25bf348b538ull, 0x59f111f1b605d019ull, 0x923f82a4af194f9bull, 0xab1c5ed5da6d8118ull,
    0xd807aa98a3030242ull, 0x12835b0145706fbeull, 0x243185be4ee4b28cull, 0x550c7dc3d5ffb4e2ull,
    0x72be5d74f27b896full, 0x80deb1fe3b1696b1ull, 0x9bdc06a725c71235ull, 0xc19bf174cf692694ull,
    0xe49b69c19ef14ad2ull, 0xefbe4786384f25e3ull, 0x0fc19dc68b8cd5b5ull, 0x240ca1cc77ac9c65ull,
    0x2de92c6f592b0275ull, 0x4a7484aa6ea6e483ull, 0x5cb0a9dcbd41fbd4ull, 0x76f988da831153b5ull,
    0x983e5152ee66dfabull, 0xa831c66d2db43210ull, 0xb00327c898fb213full, 0xbf597fc7beef0ee4ull,
    0xc6e00bf33da88fc2ull, 0xd5a79147930aa725ull, 0x06ca6351e003826full, 0x142929670a0e6e70ull,
    0x27b70a8546d22ffcull, 0x2e1b21385c26c926ull, 0x4d2c6dfc5ac42aedull, 0x53380d139d95b3dfull,
    0x650a73548baf63deull, 0x766a0abb3c77b2a8ull, 0x81c2c92e47edaee6ull, 0x92722c851482353bull,
    0xa2bfe8a14cf10364ull, 0xa81a664bbc423001ull, 0xc24b8b70d0f89791ull, 0xc76c51a30654be30ull,
    0xd192e819d6ef5218ull, 0xd69906245565a910ull, 0xf40e35855771202aull, 0x106aa07032bbd1b8ull,
    0x19a4c116b8d2d0c8ull, 0x1e376c085141ab53ull, 0x2748774cdf8eeb99ull, 0x34b0bcb5e19b48a8ull,
    0x391c0cb3c5c95a63ull, 0x4ed8aa4ae3418acbull, 0x5b9cca4f7763e373ull, 0x682e6ff3d6b2b8a3ull,
    0x748f82ee5defb2fcull, 0x78a5636f43172f60ull, 0x84c87814a1f0ab72ull, 0x8cc702081a6439ecull,
    0x90befffa23631e28ull, 0xa4506cebde82bde9ull, 0xbef9a3f7b2c67915ull, 0xc67178f2e372532bull,
    0xca273eceea26619cull, 0xd186b8c721c0c207ull, 0xeada7dd6cde0eb1eull, 0xf57d4f7fee6ed178ull,
    0x06f067aa72176fbaull, 0x0a637dc5a2c898a6ull, 0x113f9804bef90daeull, 0x1b710b35131c471bull,
    0x28db77f523047d84ull, 0x32caab7b40c72493ull, 0x3c9ebe0a15c9bebcull, 0x431d67c49c100d4cull,
    0x4cc5d4becb3e42b6ull, 0x597f299cfc657e2aull, 0x5fcb6fab3ad6faecull, 0x6c44198c4a475817ull,
};

static void sha512_compress(uint64_t h[8], const uint8_t block[128])
{
    uint64_t w[80];
    for (int i = 0; i < 16; i++)
        w[i] = load_be64(block + (size_t)i * 8);
    for (int i = 16; i < 80; i++) {
        uint64_t s0 = rotr64(w[i - 15], 1) ^ rotr64(w[i - 15], 8) ^ (w[i - 15] >> 7);
        uint64_t s1 = rotr64(w[i - 2], 19) ^ rotr64(w[i - 2], 61) ^ (w[i - 2] >> 6);
        w[i] = w[i - 16] + s0 + w[i - 7] + s1;
    }

    uint64_t a = h[0], b = h[1], c = h[2], d = h[3];
    uint64_t e = h[4], f = h[5], g = h[6], hh = h[7];
    for (int i = 0; i < 80; i++) {
        uint64_t S1 = rotr64(e, 14) ^ rotr64(e, 18) ^ rotr64(e, 41);
        uint64_t ch = (e & f) ^ (~e & g);
        uint64_t t1 = hh + S1 + ch + SHA512_K[i] + w[i];
        uint64_t S0 = rotr64(a, 28) ^ rotr64(a, 34) ^ rotr64(a, 39);
        uint64_t maj = (a & b) ^ (a & c) ^ (b & c);
        uint64_t t2 = S0 + maj;
        hh = g;
        g = f;
        f = e;
        e = d + t1;
        d = c;
        c = b;
        b = a;
        a = t1 + t2;
    }
    h[0] += a;
    h[1] += b;
    h[2] += c;
    h[3] += d;
    h[4] += e;
    h[5] += f;
    h[6] += g;
    h[7] += hh;
}

/**
 * One-shot core shared by SHA-384 and SHA-512: identical compression, 128-byte
 * blocks, and a 128-bit big-endian bit-length field (the high half is the
 * top three bits of `len`, exactly zero below the 2^61-byte FIPS ceiling).
 * The algorithms differ only in the IV and in how many words are output —
 * SHA-384 truncates to the first six words (FIPS 180-4 section 6.3.3).
 */
static void sha512_family_sum(const uint64_t iv[8], unsigned out_words,
                              const uint8_t *msg, size_t len, uint8_t *out)
{
    uint64_t h[8];
    memcpy(h, iv, 8 * sizeof(uint64_t));

    size_t off = 0;
    while (len - off >= 128) {
        sha512_compress(h, msg + off);
        off += 128;
    }

    uint8_t last[256] = { 0 };
    size_t rem = len - off;
    memcpy(last, msg + off, rem);
    last[rem] = 0x80;
    size_t blocks = (rem + 17 <= 128) ? 1 : 2; /* msg + 0x80 + 16 length bytes */
    store_be64(last + blocks * 128 - 16, (uint64_t)(len >> 61)); /* bit length, high */
    store_be64(last + blocks * 128 - 8, (uint64_t)len << 3);     /* bit length, low  */
    sha512_compress(h, last);
    if (blocks == 2)
        sha512_compress(h, last + 128);

    for (unsigned i = 0; i < out_words; i++)
        store_be64(out + (size_t)i * 8, h[i]);
}

static void sha384_sum(const uint8_t *msg, size_t len, uint8_t out[48])
{
    static const uint64_t IV[8] = {
        0xcbbb9d5dc1059ed8ull, 0x629a292a367cd507ull, 0x9159015a3070dd17ull, 0x152fecd8f70e5939ull,
        0x67332667ffc00b31ull, 0x8eb44a8768581511ull, 0xdb0c2e0d64f98fa7ull, 0x47b5481dbefa4fa4ull,
    };
    sha512_family_sum(IV, 6, msg, len, out);
}

static void sha512_sum(const uint8_t *msg, size_t len, uint8_t out[64])
{
    static const uint64_t IV[8] = {
        0x6a09e667f3bcc908ull, 0xbb67ae8584caa73bull, 0x3c6ef372fe94f82bull, 0xa54ff53a5f1d36f1ull,
        0x510e527fade682d1ull, 0x9b05688c2b3e6c1full, 0x1f83d9abfb41bd6bull, 0x5be0cd19137e2179ull,
    };
    sha512_family_sum(IV, 8, msg, len, out);
}

/* ---------------------------------------------------- HMAC core, RFC 2104 --- */

/** One-shot hash function shape shared by all four cores above. */
typedef void (*HmacSumFn)(const uint8_t *, size_t, uint8_t *);

/** Per-algorithm HMAC parameters: 64-byte key blocks for the 32-bit SHA
 * family, 128-byte for SHA-384/SHA-512 (RFC 2104 section 2 + FIPS 180-4). */
static const struct {
    const char *name;
    HmacSumFn sum;
    size_t block_len;
    size_t digest_len;
} HMAC_PARAMS[HMAC_ALGO_COUNT] = {
    { HMAC_SHA1_NAME, sha1_sum, 64, 20 },
    { HMAC_SHA256_NAME, sha256_sum, 64, 32 },
    { HMAC_SHA384_NAME, sha384_sum, 128, 48 },
    { HMAC_SHA512_NAME, sha512_sum, 128, 64 },
};

/**
 * RFC 2104 HMAC over the one-shot hash function `sum`:
 *
 *     K0    secret, hashed when longer than the block size, zero-padded
 *     tag   H((K0 ^ opad) || H((K0 ^ ipad) || message))
 *
 * Each pass concatenates pad + payload into one heap buffer so the one-shot
 * SHA cores above stay untouched — display-source clarity over the streaming
 * shape a production HMAC would use. Returns false only on allocation
 * failure.
 */
static bool hmac_sum(HmacSumFn sum, size_t block_len, size_t digest_len,
                     const uint8_t *msg, size_t msg_len,
                     const uint8_t *key, size_t key_len, uint8_t *out)
{
    /* K0: a digest never exceeds its own block size, so hashing an
     * over-long key always leaves room for the zero padding. */
    uint8_t k0[HMAC_MAX_BLOCK_LEN] = { 0 };
    if (key_len > block_len)
        sum(key, key_len, k0);
    else
        memcpy(k0, key, key_len);

    uint8_t *inner = malloc(block_len + msg_len);
    uint8_t *outer = malloc(block_len + digest_len);
    if (inner == NULL || outer == NULL) {
        free(inner);
        free(outer);
        memset(k0, 0, sizeof k0);
        return false;
    }

    for (size_t i = 0; i < block_len; i++) {
        inner[i] = k0[i] ^ 0x36; /* ipad */
        outer[i] = k0[i] ^ 0x5c; /* opad */
    }

    uint8_t inner_digest[HMAC_MAX_DIGEST_LEN];
    memcpy(inner + block_len, msg, msg_len);
    sum(inner, block_len + msg_len, inner_digest);
    memcpy(outer + block_len, inner_digest, digest_len);
    sum(outer, block_len + digest_len, out);

    free(inner);
    free(outer);
    /* Best-effort scrub of key material; ISO C offers no guarantee that a
     * plain memset survives dead-store elimination. */
    memset(k0, 0, sizeof k0);
    memset(inner_digest, 0, sizeof inner_digest);
    return true;
}

/* ------------------------------------------------------------- public API --- */

/** Failure kinds. The first two are the C standing of the ValueError the
 * Python port raises and the arms of the Rust port's HmacError enum;
 * HMAC_OUT_OF_MEMORY exists only because C has no exceptions or Result to
 * surface malloc failure through. */
typedef enum {
    HMAC_OK = 0,
    HMAC_UNKNOWN_ALGORITHM = -1,
    HMAC_EMPTY_SECRET = -2,
    HMAC_OUT_OF_MEMORY = -3
} HmacStatus;

/**
 * Compute HMAC(`message`, `secret`) under the named algorithm and write it
 * to `out_hex` as lowercase hex (NUL-terminated; `out_hex` must hold
 * HMAC_MAX_HEX_LEN bytes).
 *
 * `message` and `secret` are the strings' UTF-8 byte sequences. `algorithm`
 * may be NULL or "" for the SHA-256 default. An empty `secret` yields
 * HMAC_EMPTY_SECRET without touching `out_hex`; an unknown algorithm name
 * yields HMAC_UNKNOWN_ALGORITHM.
 */
HmacStatus hmac_hex(const char *message, const char *secret, const char *algorithm, char *out_hex)
{
    if (message == NULL)
        message = "";

    const char *name = (algorithm == NULL || algorithm[0] == '\0') ? HMAC_SHA256_NAME : algorithm;

    if (secret == NULL || secret[0] == '\0')
        return HMAC_EMPTY_SECRET;

    HmacAlgorithm algo = HMAC_UNKNOWN;
    for (int i = 0; i < HMAC_ALGO_COUNT; i++) {
        if (strcmp(name, HMAC_PARAMS[i].name) == 0) {
            algo = (HmacAlgorithm)i;
            break;
        }
    }
    if (algo == HMAC_UNKNOWN)
        return HMAC_UNKNOWN_ALGORITHM;

    uint8_t tag[HMAC_MAX_DIGEST_LEN];
    if (!hmac_sum(HMAC_PARAMS[algo].sum, HMAC_PARAMS[algo].block_len, HMAC_PARAMS[algo].digest_len,
                  (const uint8_t *)message, strlen(message),
                  (const uint8_t *)secret, strlen(secret), tag))
        return HMAC_OUT_OF_MEMORY;

    to_hex(tag, HMAC_PARAMS[algo].digest_len, out_hex);
    return HMAC_OK;
}

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 →