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.
//
// Language: C++17 (standard library + libargon2, the PHC winner reference
// implementation at https://github.com/P-H-C/phc-winner-argon2)
// Ported from src/lib/argon2.ts (the canonical TypeScript implementation).
// display source — part of CosmoDev's polyglot tool pages.
//
// The TS reference drives the very same reference C library compiled to WASM
// (argon2-browser); this port links it natively — argon2_hash / argon2_verify
// are the exact entry points the TS marshalling layer calls, so hashes verify
// byte-for-byte across both builds. The PHC parser, validator and Base64
// codec are pure C++.
//
// PHC string format (what `encoded` holds — the string you store in a DB):
// $argon2id$v=19$m=65536,t=3,p=1$<b64 salt>$<b64 digest>
// Salt and digest are unpadded standard Base64.
//
// Build: c++ -std=c++17 argon2.cpp -largon2
#include <argon2.h>
#include <cctype>
#include <cstdint>
#include <fstream>
#include <map>
#include <regex>
#include <stdexcept>
#include <string>
#include <vector>
namespace argon2tool {
using Bytes = std::vector<uint8_t>;
/** Defaults follow the OWASP-recommended Argon2id profile (64 MiB, 3 passes). */
struct Argon2Defaults {
static constexpr uint32_t memory = 65536; // KiB
static constexpr uint32_t iterations = 3;
static constexpr uint32_t parallelism = 1;
static constexpr uint32_t hashLength = 32;
};
/** Random salt size in bytes (128 bits — the PHC recommendation). */
constexpr size_t SALT_BYTES = 16;
/** Argon2 variant ids as the C library encodes them. */
enum class Argon2TypeCode : int { Argon2d = 0, Argon2i = 1, Argon2id = 2 };
/** C return code for "password does not match" — a verdict, not an error. */
constexpr int VERIFY_MISMATCH = -35;
/** Argon2 variant names accepted in PHC strings. */
enum class Argon2Variant { argon2d, argon2i, argon2id };
struct Argon2Options {
/** Memory cost in KiB (default 65536 = 64 MiB). Must be >= 1024. */
uint32_t memory = Argon2Defaults::memory;
/** Time cost — passes over memory (default 3). Must be >= 1. */
uint32_t iterations = Argon2Defaults::iterations;
/** Parallelism — lanes (default 1). Must be >= 1. */
uint32_t parallelism = Argon2Defaults::parallelism;
/** Digest length in bytes (default 32). Must be 16..64. */
uint32_t hashLength = Argon2Defaults::hashLength;
/** Explicit salt bytes; a random 16-byte salt is generated when empty. */
Bytes salt;
};
struct Argon2Result {
std::string hash; // raw digest, lowercase hex (hashLength bytes)
std::string encoded; // self-contained PHC string — store this, verify against it
std::string salt; // salt used, lowercase hex (16 bytes unless explicit)
};
/** Parameters extracted from a PHC string (parseArgon2's return type). */
struct Argon2Params {
Argon2Variant type = Argon2Variant::argon2id;
uint32_t version = 0;
uint32_t memory = 0;
uint32_t iterations = 0;
uint32_t parallelism = 0;
std::string salt; // lowercase hex
std::string hash; // lowercase hex, empty when the digest segment is absent
};
// ── small codecs ───────────────────────────────────────────────────────────
/** Lowercase hex of a byte array. */
static std::string bytesToHex(const Bytes& bytes) {
static const char* HEX = "0123456789abcdef";
std::string out;
out.reserve(bytes.size() * 2);
for (uint8_t b : bytes) {
out += HEX[b >> 4];
out += HEX[b & 0x0f];
}
return out;
}
/** charCode → 6-bit value for the standard Base64 alphabet. */
static int b64Index(char c) {
static const std::map<char, int> MAP = [] {
std::map<char, int> m;
const char* alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (int i = 0; alphabet[i] != '\0'; i++) m[alphabet[i]] = i;
return m;
}();
const auto it = MAP.find(c);
return it == MAP.end() ? -1 : it->second;
}
/**
* Unpadded standard Base64 (the PHC encoding) → bytes. Throws on any
* non-alphabet character or an impossible length (1 mod 4).
*/
static Bytes phcBase64ToBytes(const std::string& b64) {
if (b64.empty()) throw std::runtime_error("Invalid Argon2 string: empty Base64 field");
for (char c : b64) {
if (b64Index(c) < 0) {
throw std::runtime_error("Invalid Argon2 string: non-Base64 characters");
}
}
if (b64.size() % 4 == 1) {
throw std::runtime_error("Invalid Argon2 string: impossible Base64 length");
}
size_t outLength = (b64.size() * 3) / 4;
if (b64.size() % 4 == 2) outLength = ((b64.size() - 2) / 4) * 3 + 1;
if (b64.size() % 4 == 3) outLength = ((b64.size() - 3) / 4) * 3 + 2;
Bytes bytes(outLength);
size_t p = 0;
for (size_t i = 0; i < b64.size(); i += 4) {
const int d0 = b64Index(b64[i]);
// Length was validated above (not 1 mod 4), so every group has >= 2 chars.
const int d1 = b64Index(b64[i + 1]);
if (p < outLength) bytes[p++] = static_cast<uint8_t>((d0 << 2) | (d1 >> 4));
if (i + 2 < b64.size()) {
const int d2 = b64Index(b64[i + 2]);
if (p < outLength) bytes[p++] = static_cast<uint8_t>(((d1 & 0x0f) << 4) | (d2 >> 2));
if (i + 3 < b64.size()) {
const int d3 = b64Index(b64[i + 3]);
if (p < outLength) bytes[p++] = static_cast<uint8_t>(((d2 & 0x03) << 6) | d3);
}
}
}
return bytes;
}
// ── PHC parsing / validation ───────────────────────────────────────────────
/**
* 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. Throws on any malformed input.
*/
Argon2Params parseArgon2(const std::string& encoded) {
// Trim surrounding whitespace first, like the TS reference.
const size_t first = encoded.find_first_not_of(" \t\r\n");
if (first == std::string::npos) {
throw std::runtime_error("Invalid Argon2 string: expected $argon2id$v=19$m=…,t=…,p=…$salt$hash");
}
const size_t last = encoded.find_last_not_of(" \t\r\n");
const std::string trimmed = encoded.substr(first, last - first + 1);
static const std::regex RE(
R"(^\$(argon2(?:d|i|id))\$v=(\d+)\$m=(\d+),t=(\d+),p=(\d+)\$([A-Za-z0-9+/]+)(?:\$([A-Za-z0-9+/]+))?$)");
std::smatch m;
if (!std::regex_match(trimmed, m, RE)) {
throw std::runtime_error("Invalid Argon2 string: expected $argon2id$v=19$m=…,t=…,p=…$salt$hash");
}
Argon2Params params;
params.type = m[1].str() == "argon2d" ? Argon2Variant::argon2d
: m[1].str() == "argon2i" ? Argon2Variant::argon2i
: Argon2Variant::argon2id;
params.version = static_cast<uint32_t>(std::stoul(m[2].str()));
params.memory = static_cast<uint32_t>(std::stoul(m[3].str()));
params.iterations = static_cast<uint32_t>(std::stoul(m[4].str()));
params.parallelism = static_cast<uint32_t>(std::stoul(m[5].str()));
params.salt = bytesToHex(phcBase64ToBytes(m[6].str()));
params.hash = m[7].matched ? bytesToHex(phcBase64ToBytes(m[7].str())) : "";
return params;
}
/** Validate + normalise hashing parameters, throwing with a clear message. */
static Argon2Options normalizeOptions(const Argon2Options& options) {
if (options.memory < 1024) throw std::runtime_error("Memory must be at least 1024 KiB");
if (options.iterations < 1) throw std::runtime_error("Iterations must be at least 1");
if (options.parallelism < 1) throw std::runtime_error("Parallelism must be at least 1");
if (options.hashLength < 16 || options.hashLength > 64) {
throw std::runtime_error("Hash length must be between 16 and 64 bytes");
}
return options;
}
/** Random `length` bytes from the OS CSPRNG (argon2's own salt source). */
static Bytes randomSalt(size_t length) {
Bytes salt(length);
std::ifstream urandom("/dev/urandom", std::ios::binary);
if (!urandom || !urandom.read(reinterpret_cast<char*>(salt.data()), static_cast<std::streamsize>(length))) {
throw std::runtime_error("Failed to read the OS CSPRNG for the salt.");
}
return salt;
}
/** The library error string for a non-zero return code. */
static std::string argon2Message(int code) {
const char* msg = argon2_error_message(code);
return msg != nullptr ? std::string(msg) : "unknown Argon2 error " + std::to_string(code);
}
// ── hash / verify ──────────────────────────────────────────────────────────
/**
* 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). Returns the digest (hex), the
* salt used (hex), and the self-contained PHC string. A fresh random 16-byte
* salt is generated per call unless `options.salt` is given.
*/
Argon2Result argon2Hash(const std::string& password, const Argon2Options& options = {}) {
const Argon2Options opts = normalizeOptions(options);
const Bytes salt = !opts.salt.empty() ? opts.salt : randomSalt(SALT_BYTES);
Bytes digest(opts.hashLength);
// argon2_encodedlen gives the exact PHC string size for these parameters.
const size_t encodedLen =
argon2_encodedlen(opts.iterations, opts.memory, opts.parallelism, salt.size(),
opts.hashLength, static_cast<int>(Argon2TypeCode::Argon2id));
std::string encoded(encodedLen, '\0');
const int rc = argon2_hash(
opts.iterations, opts.memory, opts.parallelism, password.data(), password.size(),
salt.data(), salt.size(), digest.data(), opts.hashLength, &encoded[0], encodedLen,
static_cast<int>(Argon2TypeCode::Argon2id), ARGON2_VERSION_13);
if (rc != ARGON2_OK) throw std::runtime_error(argon2Message(rc));
return {bytesToHex(digest), std::string(encoded.c_str()), bytesToHex(salt)};
}
/**
* Verify a password against a PHC-format encoded hash (as produced by
* argon2Hash). Returns true on match, false on mismatch; throws only on a
* malformed encoded string or a runtime error. Any Argon2 type (d/i/id) is
* accepted — the type is read from the string itself.
*/
bool argon2Verify(const std::string& encoded, const std::string& password) {
const Argon2Params params = parseArgon2(encoded); // validate format up front
const int type = params.type == Argon2Variant::argon2d ? static_cast<int>(Argon2TypeCode::Argon2d)
: params.type == Argon2Variant::argon2i ? static_cast<int>(Argon2TypeCode::Argon2i)
: static_cast<int>(Argon2TypeCode::Argon2id);
const int rc = argon2_verify(encoded.c_str(), password.data(), password.size(), type);
if (rc == ARGON2_OK) return true;
if (rc == VERIFY_MISMATCH) return false;
throw std::runtime_error(argon2Message(rc));
}
} // namespace argon2tool
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 →