Skip to content

Argon2 Hash & Verify — C source

Hash passwords with Argon2id — the winner of the Password Hashing Competition. Configure memory, iterations, and parallelism. WASM-powered, client-side.

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

/*
 * argon2 — Argon2id password hashing (PHC string format).
 *
 * Language: C (C11, POSIX) + the reference Argon2 C library + OpenSSL 3.x
 *           libcrypto (RAND_bytes for the per-hash random salt).
 * Source:   CosmoDev polyglot showcase port of the argon2 tool, ported from
 *           src/lib/argon2.ts (the canonical TypeScript implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * The TS reference drives argon2-browser — the reference Argon2 C library
 * (v1.0.2) compiled to WASM — through its emscripten glue (_argon2_hash_ext,
 * _argon2_verify_ext, _argon2_error_message, _argon2_encodedlen). C is where
 * that code was born: this port calls the very same library natively through
 * its public <argon2.h> API, and re-implements in plain C the one piece the
 * TS file owns itself — parsing the PHC string:
 *
 *   $argon2id$v=19$m=65536,t=3,p=1$<unpadded-b64 salt>$<unpadded-b64 digest>
 *
 * Salt and digest use unpadded standard Base64. Hashing uses Argon2id (the
 * hybrid of Argon2i's side-channel resistance and Argon2d's GPU resistance —
 * the Password Hashing Competition winner and the recommended mode for
 * password storage), version 1.3 (v=19).
 *
 * Build: cc -std=c11 argon2.c -largon2 -lcrypto
 */

#define _POSIX_C_SOURCE 200809L

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

#include <openssl/rand.h>
#include <argon2.h>

/** Defaults follow the OWASP-recommended Argon2id profile (64 MiB, 3 passes). */
static const uint32_t ARGON2_DEFAULT_MEMORY = 65536;   /* KiB */
static const uint32_t ARGON2_DEFAULT_ITERATIONS = 3;
static const uint32_t ARGON2_DEFAULT_PARALLELISM = 1;
static const uint32_t ARGON2_DEFAULT_HASH_LENGTH = 32; /* bytes */

/** Random salt size in bytes (128 bits — the PHC recommendation). */
#define SALT_BYTES 16

/** C library return code for "password does not match" — a verdict, not an error. */
#define VERIFY_MISMATCH (-35)

/** Argon2 variant ids as the library encodes them (matches the TS TYPE_BY_NAME). */
typedef enum {
    ARGON2_TYPE_D = 0,
    ARGON2_TYPE_I = 1,
    ARGON2_TYPE_ID = 2,
} argon2_kind;

/* ------------------------------------------------------------------ hex --- */

/** Lowercase hex of a byte array into out (2*len+1 bytes, NUL-terminated). */
static void bytes_to_hex(const uint8_t *bytes, size_t len, char *out) {
    static const char HEX[] = "0123456789abcdef";
    for (size_t i = 0; i < len; i++) {
        out[i * 2] = HEX[bytes[i] >> 4];
        out[i * 2 + 1] = HEX[bytes[i] & 0x0f];
    }
    out[len * 2] = '\0';
}

/* ------------------------------------------------- unpadded Base64 (PHC) --- */

/** char -> 6-bit value for the standard Base64 alphabet, or -1. */
static int b64_sextet(char c) {
    if (c >= 'A' && c <= 'Z') return c - 'A';
    if (c >= 'a' && c <= 'z') return c - 'a' + 26;
    if (c >= '0' && c <= '9') return c - '0' + 52;
    if (c == '+') return 62;
    if (c == '/') return 63;
    return -1;
}

/**
 * Unpadded standard Base64 (the PHC encoding) -> bytes, mirroring the TS
 * phcBase64ToBytes: rejects empty input, any non-alphabet character, and an
 * impossible length (1 mod 4). Writes at most *cap bytes into out and returns
 * the decoded length, or -1 on error.
 */
static long phc_base64_decode(const char *b64, uint8_t *out, size_t cap) {
    size_t n = strlen(b64);
    if (n == 0) return -1;
    if (n % 4 == 1) return -1;
    for (size_t i = 0; i < n; i++) {
        if (b64_sextet(b64[i]) < 0) return -1;
    }
    size_t out_len = (n / 4) * 3;
    if (n % 4 == 2) out_len = (n / 4) * 3 + 1;
    if (n % 4 == 3) out_len = (n / 4) * 3 + 2;
    if (out_len > cap) return -1;

    size_t p = 0;
    for (size_t i = 0; i < n; i += 4) {
        int d0 = b64_sextet(b64[i]);
        /* Length was validated above (not 1 mod 4), so every group has >= 2 chars. */
        int d1 = b64_sextet(b64[i + 1]);
        if (d0 < 0 || d1 < 0) return -1;
        if (p < out_len) out[p++] = (uint8_t)((d0 << 2) | (d1 >> 4));
        if (i + 2 < n) {
            int d2 = b64_sextet(b64[i + 2]);
            if (d2 < 0) return -1;
            if (p < out_len) out[p++] = (uint8_t)(((d1 & 0x0f) << 4) | (d2 >> 2));
            if (i + 3 < n) {
                int d3 = b64_sextet(b64[i + 3]);
                if (d3 < 0) return -1;
                if (p < out_len) out[p++] = (uint8_t)(((d2 & 0x03) << 6) | d3);
            }
        }
    }
    return (long)p;
}

/* ------------------------------------------------------- PHC string parse --- */

/** Parameters extracted from a PHC string (parse_argon2's return type). */
typedef struct {
    argon2_kind type;      /* argon2d / argon2i / argon2id */
    unsigned version;      /* 19 for v1.3 */
    unsigned memory;       /* KiB */
    unsigned iterations;
    unsigned parallelism;
    char salt[2 * 128 + 1];  /* decoded salt, lowercase hex */
    char hash[2 * 256 + 1];  /* decoded digest, lowercase hex ('' if absent) */
} argon2_params;

/**
 * Parse a PHC-format Argon2 string
 * (`$argon2id$v=19$m=65536,t=3,p=1$salt$hash`) into its typed parameters.
 * Accepts argon2d / argon2i / argon2id; the digest segment is optional (some
 * encoders omit it); salt and hash are returned as lowercase hex. Returns
 * NULL on success or a human-readable error message (mirrors the TS throw).
 */
static const char *parse_argon2(const char *encoded, argon2_params *out) {
    /* Skip leading whitespace (the TS trims before matching). */
    while (*encoded == ' ' || *encoded == '\t' || *encoded == '\n' || *encoded == '\r') encoded++;
    if (strncmp(encoded, "$argon2", 7) != 0) goto bad;
    encoded += 7;
    argon2_kind kind;
    if (strncmp(encoded, "id", 2) == 0) { kind = ARGON2_TYPE_ID; encoded += 2; }
    else if (strncmp(encoded, "i", 1) == 0) { kind = ARGON2_TYPE_I; encoded += 1; }
    else if (strncmp(encoded, "d", 1) == 0) { kind = ARGON2_TYPE_D; encoded += 1; }
    else goto bad;

    char salt_b64[256];
    char hash_b64[512];
    unsigned version = 0, memory = 0, iterations = 0, parallelism = 0;
    int hash_fields = 0;
    /* %n records the consumed offset so trailing garbage can be rejected. */
    int consumed = 0;
    int n = sscanf(encoded,
                   "$v=%u$m=%u,t=%u,p=%u$%255[A-Za-z0-9+/]$%511[A-Za-z0-9+/]%n",
                   &version, &memory, &iterations, &parallelism,
                   salt_b64, hash_b64, &consumed);
    if (n != 6) {
        /* Retry without the optional digest segment. */
        consumed = 0;
        n = sscanf(encoded, "$v=%u$m=%u,t=%u,p=%u$%255[A-Za-z0-9+/]%n",
                   &version, &memory, &iterations, &parallelism,
                   salt_b64, &consumed);
        if (n != 5) goto bad;
    } else {
        hash_fields = 1;
    }
    if (encoded[consumed] != '\0') goto bad;

    uint8_t salt[SALT_BYTES * 8]; /* generous cap for long explicit salts */
    long salt_len = phc_base64_decode(salt_b64, salt, sizeof salt);
    if (salt_len < 0) goto bad;
    bytes_to_hex(salt, (size_t)salt_len, out->salt);

    out->hash[0] = '\0';
    if (hash_fields) {
        uint8_t digest[256];
        long digest_len = phc_base64_decode(hash_b64, digest, sizeof digest);
        if (digest_len < 0) goto bad;
        bytes_to_hex(digest, (size_t)digest_len, out->hash);
    }
    out->type = kind;
    out->version = version;
    out->memory = memory;
    out->iterations = iterations;
    out->parallelism = parallelism;
    return NULL;

bad:
    return "Invalid Argon2 string: expected $argon2id$v=19$m=…,t=…,p=…$salt$hash";
}

/* ------------------------------------------------------- hash and verify --- */

/** Hashing + verification options (the TS Argon2Options). */
typedef struct {
    uint32_t memory;       /* KiB, default 65536 = 64 MiB. Must be >= 1024. */
    uint32_t iterations;   /* passes over memory, default 3. Must be >= 1. */
    uint32_t parallelism;  /* lanes, default 1. Must be >= 1. */
    uint32_t hash_length;  /* digest bytes, default 32. Must be 16..64. */
    const uint8_t *salt;   /* explicit salt; a random 16-byte salt is used when NULL. */
    size_t salt_len;
} argon2_options;

/** Result of a hash operation (the TS Argon2Result). */
typedef struct {
    char *hash;    /* raw digest, lowercase hex (hash_length bytes) — caller frees */
    char *encoded; /* self-contained PHC string — store this, verify against it */
    char *salt;    /* salt used, lowercase hex — caller frees */
} argon2_result;

static void argon2_result_free(argon2_result *r) {
    free(r->hash);
    free(r->encoded);
    free(r->salt);
    r->hash = r->encoded = r->salt = NULL;
}

/** Validate + normalise hashing parameters; returns an error message or NULL. */
static const char *normalize_options(const argon2_options *in, argon2_options *out) {
    *out = *in;
    if (out->memory == 0) out->memory = ARGON2_DEFAULT_MEMORY;
    if (out->iterations == 0) out->iterations = ARGON2_DEFAULT_ITERATIONS;
    if (out->parallelism == 0) out->parallelism = ARGON2_DEFAULT_PARALLELISM;
    if (out->hash_length == 0) out->hash_length = ARGON2_DEFAULT_HASH_LENGTH;
    if (out->memory < 1024) return "Memory must be at least 1024 KiB";
    if (out->iterations < 1) return "Iterations must be at least 1";
    if (out->parallelism < 1) return "Parallelism must be at least 1";
    if (out->hash_length < 16 || out->hash_length > 64) {
        return "Hash length must be between 16 and 64 bytes";
    }
    return NULL;
}

/**
 * Hash a password with Argon2id (hybrid of Argon2i's side-channel resistance
 * and Argon2d's GPU resistance — the Password Hashing Competition winner and
 * the recommended mode for password storage). Fills *out with the digest
 * (hex), the salt used (hex), and the self-contained PHC string. A fresh
 * random 16-byte salt is generated per call (OpenSSL RAND_bytes) unless
 * opts->salt is given. Returns NULL on success or an error message.
 */
static const char *argon2_hash_password(const char *password, const argon2_options *opts,
                                        argon2_result *out) {
    argon2_options norm;
    const char *err = normalize_options(opts, &norm);
    if (err) return err;

    uint8_t generated[SALT_BYTES];
    const uint8_t *salt = norm.salt;
    size_t salt_len = norm.salt_len;
    if (!salt) {
        if (RAND_bytes(generated, (int)sizeof generated) != 1) {
            return "Failed to generate a random salt";
        }
        salt = generated;
        salt_len = sizeof generated;
    }

    size_t pwd_len = strlen(password);
    uint8_t *digest = malloc(norm.hash_length);
    size_t encoded_len = argon2_encodedlen(norm.iterations, norm.memory, norm.parallelism,
                                           (uint32_t)salt_len, norm.hash_length, Argon2_id);
    char *encoded = malloc(encoded_len + 1);
    if (!digest || !encoded) {
        free(digest);
        free(encoded);
        return "Out of memory";
    }

    /* Same call the TS makes through the WASM glue (_argon2_hash_ext with
     * type=argon2id, version=0x13), minus the secret/AD pointers it zeroes. */
    int res = argon2_hash(norm.iterations, norm.memory, norm.parallelism,
                          password, pwd_len,
                          salt, salt_len,
                          digest, norm.hash_length,
                          encoded, encoded_len + 1,
                          Argon2_id, ARGON2_VERSION_NUMBER);
    if (res != ARGON2_OK) {
        free(digest);
        free(encoded);
        return argon2_error_message(res);
    }

    out->hash = malloc(norm.hash_length * 2 + 1);
    out->salt = malloc(salt_len * 2 + 1);
    out->encoded = encoded;
    if (!out->hash || !out->salt) {
        argon2_result_free(out);
        free(digest);
        return "Out of memory";
    }
    bytes_to_hex(digest, norm.hash_length, out->hash);
    bytes_to_hex(salt, salt_len, out->salt);
    free(digest);
    return NULL;
}

/**
 * Verify a password against a PHC-format encoded hash (as produced by
 * argon2_hash_password). Returns true on match, false on mismatch; a
 * malformed encoded string or a library error is reported on stderr and
 * also returns false (the TS throws for those two cases).
 */
static bool argon2_verify_password(const char *encoded, const char *password) {
    argon2_params params;
    const char *err = parse_argon2(encoded, &params); /* validate format up front */
    if (err) {
        fprintf(stderr, "argon2: %s\n", err);
        return false;
    }
    argon2_type type = (params.type == ARGON2_TYPE_ID) ? Argon2_id
                     : (params.type == ARGON2_TYPE_I) ? Argon2_i
                     : Argon2_d;
    int res = argon2_verify(encoded, password, strlen(password), type);
    if (res == ARGON2_OK) return true;
    if (res == VERIFY_MISMATCH) return false;
    fprintf(stderr, "argon2: %s\n", argon2_error_message(res));
    return false;
}

/* ------------------------------------------------------------- demo main --- */

int main(void) {
    /* Deterministic demo (explicit salt, reduced cost); real callers pass
     * {0} options to get the OWASP defaults + a fresh random salt. */
    const uint8_t salt[SALT_BYTES] = "cosmodev-salt!!";
    argon2_options opts = { .memory = 8192, .iterations = 2, .parallelism = 1,
                            .hash_length = 32, .salt = salt, .salt_len = sizeof salt };
    argon2_result result;
    const char *err = argon2_hash_password("correct horse battery staple", &opts, &result);
    if (err) {
        fprintf(stderr, "error: %s\n", err);
        return 1;
    }
    printf("hash:    %s\n", result.hash);
    printf("salt:    %s\n", result.salt);
    printf("encoded: %s\n", result.encoded);

    argon2_params params;
    if (parse_argon2(result.encoded, &params) == NULL) {
        printf("parsed:  type=%u v=%u m=%u t=%u p=%u\n",
               (unsigned)params.type, params.version, params.memory,
               params.iterations, params.parallelism);
    }

    printf("verify:  %s\n",
           argon2_verify_password(result.encoded, "correct horse battery staple") ? "true" : "false");
    printf("wrong:   %s\n",
           argon2_verify_password(result.encoded, "wrong horse") ? "true" : "false");

    argon2_result_free(&result);
    return 0;
}

Also available in 9 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 →