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