← All articles

Generating and hashing passwords in C++

The standard library has neither a reliable CSPRNG nor password hashing, so the honest answer is: use libsodium. It gives you unbiased random bytes and Argon2id in two function calls, and it removes the buffer bookkeeping that makes hand-rolled C++ crypto dangerous.

1. Generate a password

randombytes_uniform() returns a value in the given range without modulo bias, using libsodium's CSPRNG. Call sodium_init() once at start-up:

cpp
#include <sodium.h>

#include <cstdint>
#include <stdexcept>
#include <string>

namespace {

const std::string kUpper   = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const std::string kLower   = "abcdefghijklmnopqrstuvwxyz";
const std::string kDigits  = "0123456789";
const std::string kSymbols = "!@#$%^&*()-_=+[]{};:,.?/~";

}  // namespace

// Call once before anything else: sodium_init() sets up the CSPRNG.
void init_crypto() {
    if (sodium_init() < 0) {
        throw std::runtime_error("libsodium failed to initialise");
    }
}

std::string generate_password(std::size_t length, const std::string& alphabet) {
    if (length == 0 || alphabet.empty()) {
        throw std::invalid_argument("length and alphabet must be non-empty");
    }

    std::string password;
    password.reserve(length);
    for (std::size_t i = 0; i < length; ++i) {
        // Unbiased: uniform over [0, alphabet.size()).
        const std::uint32_t index =
            randombytes_uniform(static_cast<std::uint32_t>(alphabet.size()));
        password.push_back(alphabet[index]);
    }
    return password;
}

Continuing the same file, if you want the guarantee that every character class appears you can pick one character per set and shuffle — or simply retry with generate_password() from above until the password passes your check, which is easier to review and just as unbiased:

cpp
#include <string>

bool has_every_class(const std::string& password) {
    bool upper = false, lower = false, digit = false, symbol = false;
    for (unsigned char c : password) {
        if (c >= 'A' && c <= 'Z') upper = true;
        else if (c >= 'a' && c <= 'z') lower = true;
        else if (c >= '0' && c <= '9') digit = true;
        else symbol = true;
    }
    return upper && lower && digit && symbol;
}

std::string generate_strong_password(std::size_t length = 24) {
    const std::string alphabet =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz"
        "0123456789"
        "!@#$%^&*()-_=+[]{};:,.?/~";

    for (;;) {
        std::string candidate = generate_password(length, alphabet);
        if (has_every_class(candidate)) {
            return candidate;
        }
    }
}

2. Hash with Argon2id

crypto_pwhash_str() generates a random salt, applies Argon2id and returns a self-describing string containing the parameters — the same shape as the PHC strings used everywhere else:

cpp
#include <sodium.h>

#include <stdexcept>
#include <string>

std::string hash_password(const std::string& password) {
    // crypto_pwhash_STRBYTES is the buffer size libsodium requires (128).
    char encoded[crypto_pwhash_STRBYTES];

    // INTERACTIVE is tuned for a login on a normal server. For values that must
    // resist offline GPU cracking for years, use the MODERATE or SENSITIVE
    // constants, or crypto_pwhash_argon2id_*limits for a custom target.
    const int rc = crypto_pwhash_str(
        encoded,
        password.c_str(), password.size(),
        crypto_pwhash_OPSLIMIT_INTERACTIVE,
        crypto_pwhash_MEMLIMIT_INTERACTIVE);

    if (rc != 0) {
        // rc != 0 means the memory could not be allocated — not a password problem.
        throw std::runtime_error("crypto_pwhash_str failed (out of memory?)");
    }
    return std::string(encoded);   // "$argon2id$v=19$m=65536,t=2,p=1$..."
}

The stored string already contains the algorithm, parameters and salt, so it is the only thing you need in the database column. The equivalent explicit-parameter call is crypto_pwhash_str_alg() with crypto_pwhash_ALG_ARGON2ID13 when you want to be certain of the variant.

3. Verify

cpp
#include <sodium.h>

#include <string>

bool verify_password(const std::string& password, const std::string& stored) {
    // Constant-time comparison happens inside libsodium, and the parameters are
    // read from the stored string, so an old hash keeps verifying after you
    // raise the limits for new hashes.
    return crypto_pwhash_str_verify(stored.c_str(), password.c_str(), password.size()) == 0;
}

// Upgrading parameters on a successful login:
bool needs_rehash(const std::string& stored) {
    return crypto_pwhash_str_needs_rehash(
               stored.c_str(),
               crypto_pwhash_OPSLIMIT_MODERATE,
               crypto_pwhash_MEMLIMIT_MODERATE) != 0;
}

4. About bcrypt in C++

There is no standard bcrypt in C or C++, and the implementations that exist (OpenBSD's bcrypt(), assorted single-file ports) are easy to get subtly wrong — the Blowfish key schedule, the base64 variant and the 72-byte truncation are all places where "it verifies my test vector" is not the same as "it is correct". If you need bcrypt for compatibility, link against a maintained implementation rather than copying one into your project. If you are free to choose, Argon2id via libsodium is the better default, and it is already in the library you just added for randomness.

Pitfalls in C++

  • std::random_device is not guaranteed to be a CSPRNG by the language. Some older MinGW and MinGW-w64 builds return a deterministic sequence. Use libsodium or getrandom(2) / BCryptGenRandom directly.
  • rand() and std::mt19937 are predictable; seeding with time(nullptr) makes them trivially guessable.
  • Modulo bias. r % alphabet.size() is the classic mistake; that is precisely what randombytes_uniform() exists to avoid.
  • Comparing with ==, strcmp() or std::string::compare() on secrets leaks timing. Use sodium_memcmp(), or better, let the hashing function verify for you.
  • Buffer sizes. crypto_pwhash_STRBYTES is 128; a fixed char[64] is an overflow waiting to happen. Use the macros.
  • Leaving secrets in memory. std::string gives no wipe guarantees. sodium_memzero() the buffers you control, and prefer sodium_malloc() for anything long-lived.

Building

shell
# Debian/Ubuntu
sudo apt install libsodium-dev
g++ -std=c++17 -O2 passwords.cpp -lsodium -o passwords

# macOS
brew install libsodium
g++ -std=c++17 -O2 passwords.cpp -lsodium -o passwords

# Windows (vcpkg)
vcpkg install libsodium
cl /std:c++17 passwords.cpp /I %VCPKG_ROOT%\installed\x64-windows\include ^
   /link /LIBPATH:%VCPKG_ROOT%\installed\x64-windows\lib libsodium.lib

To confirm that your build produces interoperable hashes, generate one here and try it in the Verify tab: libsodium's Argon2id output and this site's Go implementation both follow the PHC format, so a hash from one must verify in the other.

Check a libsodium hash — paste the $argon2id$… string and the password to see whether another implementation agrees.