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:
#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:
#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:
#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_deviceis not guaranteed to be a CSPRNG by the language. Some older MinGW and MinGW-w64 builds return a deterministic sequence. Use libsodium orgetrandom(2)/BCryptGenRandomdirectly.rand()andstd::mt19937are predictable; seeding withtime(nullptr)makes them trivially guessable.- Modulo bias.
r % alphabet.size()is the classic mistake; that is precisely whatrandombytes_uniform()exists to avoid. - Comparing with
==,strcmp()orstd::string::compare()on secrets leaks timing. Usesodium_memcmp(), or better, let the hashing function verify for you. - Buffer sizes.
crypto_pwhash_STRBYTESis 128; a fixedchar[64]is an overflow waiting to happen. Use the macros. - Leaving secrets in memory.
std::stringgives no wipe guarantees.sodium_memzero()the buffers you control, and prefersodium_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.