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, ¶llelism,
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, ¶llelism,
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, ¶ms); /* 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, ¶ms) == 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 →