BCrypt Password Hashing

This page explains how to securely hash and verify passwords using bcrypt.

Code snippets assume using namespace boost::capy; is in effect.

What is BCrypt?

BCrypt is a password-hashing function designed by Niels Provos and David Mazières. It incorporates:

  • A salt to protect against rainbow table attacks

  • An adaptive cost factor that can be increased as hardware improves

  • Built-in work factor that makes brute-force attacks expensive

BCrypt is the recommended algorithm for password storage.

Quick Start

#include <boost/capy/bcrypt.hpp>

// Hash a password
bcrypt::result hash = bcrypt::hash("my_password", 12);

// Store hash.str() in database...

// Later, verify the password
system::error_code ec;
bool valid = bcrypt::compare("my_password", stored_hash, ec);

if (ec)
    // Hash was malformed
else if (valid)
    // Password matches
else
    // Password does not match

Hashing Passwords

The hash function generates a salted hash:

// Default cost factor (10)
bcrypt::result r1 = bcrypt::hash("password");

// Custom cost factor
bcrypt::result r2 = bcrypt::hash("password", 12);

// Custom cost factor and version
bcrypt::result r3 = bcrypt::hash("password", 12, bcrypt::version::v2b);

Cost Factor

The cost factor (rounds) determines how expensive hashing is. Each increment doubles the work:

Cost Approximate Time (modern CPU)

10

~100ms

12

~400ms

14

~1.6s

16

~6.4s

Guidelines:

  • Minimum: 10 for new applications

  • Recommended: 12 for most applications

  • Maximum: 31 (impractically slow)

  • Adjust based on your hardware and latency requirements

Password Length Limit

BCrypt only uses the first 72 bytes of a password. Longer passwords are silently truncated. If you need to support longer passwords, pre-hash with SHA-256:

// For passwords > 72 bytes
std::string pre_hash = sha256(long_password);
bcrypt::result r = bcrypt::hash(pre_hash, 12);

Verifying Passwords

The compare function extracts the salt from a stored hash, re-hashes the input password, and compares:

system::error_code ec;
bool valid = bcrypt::compare(user_input, stored_hash, ec);

if (ec == bcrypt::error::invalid_hash)
{
    // Hash string is malformed - data corruption or tampering
    log_security_event("invalid hash format");
    return false;
}

if (valid)
    grant_access();
else
    reject_login();
Always check the error code. A false return value alone does not distinguish between "wrong password" and "malformed hash".

Working with Salts

You can generate and use salts separately:

// Generate a salt
bcrypt::result salt = bcrypt::gen_salt(12);

// Hash with explicit salt
system::error_code ec;
bcrypt::result hash = bcrypt::hash("password", salt.str(), ec);

This is rarely needed since hash() generates a salt automatically.

The result Type

bcrypt::result is a fixed-size buffer (no heap allocation):

bcrypt::result r = bcrypt::hash("password", 12);

// Access the hash string
core::string_view sv = r.str();  // Or just use r (implicit conversion)
char const* cstr = r.c_str();    // Null-terminated

// Check for valid result
if (r)
    store(r.str());

Hash String Format

A bcrypt hash string has this format:

$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
│  │  │                                                    │
│  │  │                                                    └─ hash (31 chars)
│  │  └─ salt (22 chars)
│  └─ cost factor
└─ version

Total length: 60 characters.

Extracting the Cost Factor

To check the cost factor of an existing hash:

system::error_code ec;
unsigned rounds = bcrypt::get_rounds(stored_hash, ec);

if (ec)
    // Invalid hash format
else if (rounds < 12)
    // Consider re-hashing with higher cost

Upgrading Cost Factor

When a user logs in successfully, you can check if their hash needs upgrading:

system::error_code ec;
bool valid = bcrypt::compare(password, stored_hash, ec);

if (valid && !ec)
{
    unsigned current_cost = bcrypt::get_rounds(stored_hash, ec);

    if (!ec && current_cost < 12)
    {
        // Re-hash with higher cost
        bcrypt::result new_hash = bcrypt::hash(password, 12);
        update_stored_hash(user_id, new_hash.str());
    }
}

Error Handling

BCrypt defines two error codes:

Error Meaning

bcrypt::error::invalid_salt

Salt string is malformed

bcrypt::error::invalid_hash

Hash string is malformed

These errors indicate either data corruption or malicious input. Log them as security events.

Version Selection

BCrypt has multiple version prefixes:

Version Description

version::v2a

Original specification

version::v2b

Fixed handling of passwords > 255 chars (recommended)

Use v2b for new hashes. All versions produce compatible hashes that can be verified by any version.

Security Considerations

Do:

  • Use cost factor 12 or higher

  • Store the complete hash string (includes salt and cost)

  • Compare in constant time (handled by compare)

  • Log invalid hash errors as security events

Do Not:

  • Store salts separately (they are embedded in the hash)

  • Use bcrypt for general-purpose hashing (use SHA-256)

  • Compare hashes with == (timing attacks)

Summary

Function Purpose

bcrypt::hash(password, rounds)

Hash a password with auto-generated salt

bcrypt::hash(password, salt, ec)

Hash with explicit salt

bcrypt::compare(password, hash, ec)

Verify a password against a hash

bcrypt::gen_salt(rounds)

Generate a random salt

bcrypt::get_rounds(hash, ec)

Extract cost factor from hash

Next Steps