Protecting user passwords is one of the most critical responsibilities of any application. Storing passwords in plain text is a severe security risk that can lead to data breaches, compromised accounts, and legal liabilities. In C, building a secure password storage system requires using cryptographic hashing with salt and key stretching, and relying on well-vetted libraries rather than writing your own cryptographic functions. This article provides a comprehensive guide to implementing a secure password storage system in C, covering the core principles, algorithm selection, practical implementation with libsodium, secure storage practices, and additional security measures.

Principles of Password Security

Password security rests on three fundamental concepts: hashing, salting, and key stretching.

Hashing

A cryptographic hash function maps an input (the password) to a fixed-size output (the hash). A secure hash is one-way—given the hash, it is computationally infeasible to reverse it to find the original password. However, simple hash functions like SHA-256 are fast, which makes them vulnerable to brute-force attacks where an attacker can try millions of passwords per second.

Salting

A salt is a random, unique value added to each password before hashing. This ensures that even if two users have the same password, their hashes will differ. Salting defeats precomputed rainbow table attacks because an attacker would need to compute a separate table for each salt, which is impractical.

Key Stretching

Key stretching makes the hashing process deliberately slow and resource-intensive, increasing the time required for each brute-force attempt. Algorithms like bcrypt, scrypt, and Argon2 use configurable work factors (time cost, memory cost, parallelism) to scale security as hardware improves.

Choosing the Right Hashing Algorithm

Not all hashing algorithms are suitable for password storage. Avoid fast general-purpose hashes such as MD5, SHA-1, or raw SHA-256. Even salted, these can be cracked rapidly with modern GPUs or ASICs. Use algorithms specifically designed for password hashing:

  • Argon2 – Winner of the Password Hashing Competition (2015). It offers robust resistance against GPU and ASIC attacks, with configurable memory, time, and parallelism. libsodium provides crypto_pwhash_* functions that use Argon2id (the recommended variant) by default.
  • bcrypt – A well-established algorithm that incorporates a cost parameter. It is resistant to GPU-based attacks but uses fixed memory, making it less optimal against memory-hard attacks compared to Argon2.
  • scrypt – Designed to be memory-hard, requiring a significant amount of memory to compute a hash. It is a good choice when Argon2 is not available, but Argon2 is generally preferred.

For any new project in C, libsodium is the recommended library. It provides modern, safe defaults for password hashing and is actively maintained. You can learn more from the libsodium password hashing documentation.

Setting Up libsodium

libsodium is a cross-platform, easy-to-use cryptographic library. To use it for password hashing, first include the header and initialize the library. On most systems, you can install it via your package manager (e.g., apt install libsodium-dev on Debian/Ubuntu) or compile from source.

#include <sodium.h>

int main() {
    if (sodium_init() < 0) {
        // Library initialization failed – abort or handle error
        return 1;
    }
    // ... rest of application
}

After sodium_init() succeeds, you can safely call password hashing functions.

Implementing Password Hashing with libsodium

libsodium offers the crypto_pwhash_str() function, which generates a formatted string containing the salt and parameters alongside the hash. This simplifies storage: you only need to save the output string, not the salt separately.

Function Signature

int crypto_pwhash_str(
    char *out,
    const char *passwd,
    unsigned long long passwdlen,
    unsigned long long opslimit,
    size_t memlimit
);
  • out – A buffer to receive the resulting hash string. It must be at least crypto_pwhash_STRBYTES bytes long.
  • passwd – The password to hash (a null-terminated string).
  • passwdlen – The length of the password (use strlen(passwd)).
  • opslimit – A time cost parameter. libsodium defines constants: crypto_pwhash_OPSLIMIT_INTERACTIVE (fast, suitable for interactive operations), crypto_pwhash_OPSLIMIT_MODERATE, and crypto_pwhash_OPSLIMIT_SENSITIVE (slow, for high-security scenarios).
  • memlimit – Memory cost parameter, in bytes. Corresponding constants: crypto_pwhash_MEMLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_MODERATE, crypto_pwhash_MEMLIMIT_SENSITIVE.

Returns 0 on success, -1 on failure (e.g., insufficient memory or invalid parameters).

Example: Hashing a Password

#include <stdio.h>
#include <string.h>
#include <sodium.h>

#define HASH_STR_LEN crypto_pwhash_STRBYTES

int hash_password(const char *password, char *hash_out) {
    if (sodium_init() < 0) {
        fprintf(stderr, "libsodium initialization failed.\n");
        return -1;
    }

    // Use interactive cost parameters for reasonable speed
    if (crypto_pwhash_str(
            hash_out,
            password,
            strlen(password),
            crypto_pwhash_OPSLIMIT_INTERACTIVE,
            crypto_pwhash_MEMLIMIT_INTERACTIVE) != 0) {
        // Hashing failed – likely OOM
        fprintf(stderr, "Password hashing failed.\n");
        return -1;
    }
    return 0;
}

int main() {
    char hash[HASH_STR_LEN];
    const char *pwd = "correct horse battery staple";

    if (hash_password(pwd, hash) == 0) {
        printf("Hash: %s\n", hash);
    }
    return 0;
}

The output hash is a printable ASCII string that includes the algorithm identifier, salt, and hash. For example: $argon2id$v=19$m=65536,t=2,p=1$.... Store this string directly in your database.

Verifying Passwords

To verify a password, use crypto_pwhash_str_verify(). It compares the input against the stored hash string in a time-constant manner to prevent timing attacks.

Function Signature

int crypto_pwhash_str_verify(
    const char *str,
    const char *passwd,
    unsigned long long passwdlen
);
  • str – The stored hash string (produced by crypto_pwhash_str).
  • passwd – The password candidate.
  • passwdlen – Length of the password.

Returns 0 on match, -1 on mismatch or error.

Example: Verifying a Password

int verify_password(const char *password, const char *stored_hash) {
    if (sodium_init() < 0) {
        return -1;
    }
    if (crypto_pwhash_str_verify(stored_hash, password, strlen(password)) == 0) {
        return 1; // match
    } else {
        return 0; // no match
    }
}

int main() {
    const char *stored = "$argon2id$v=19$m=65536,t=2,p=1$..."; // retrieved from DB
    const char *attempt = "wrong password";

    if (verify_password(attempt, stored)) {
        printf("Password correct.\n");
    } else {
        printf("Password incorrect.\n");
    }
    return 0;
}

Important: Never compare hashes using strcmp or any non-constant-time function. libsodium’s built-in verification handles this correctly.

Secure Storage of Hashes

Once you have generated the hash string, you must store it securely. Follow these guidelines:

  • Use a database with access controls – Store hashes in a read-only table or column for most application users. The database user that the application uses should have only necessary permissions.
  • Encrypt the storage medium – If possible, enable disk encryption (e.g., LUKS, BitLocker) or use encrypted file systems for the database files.
  • Avoid logging passwords – Never write password hashes to logs or console output in production. In the code examples above, printing the hash is only for debugging and must be removed before deployment.
  • Use parameterized queries – When storing or retrieving hashes from a SQL database, always use prepared statements to prevent SQL injection.
  • Backup securely – Treat backup copies of hash data with the same security standards as the live database.

Additional Security Considerations

Pepper (Optional)

A pepper is a secret key added to the password before hashing, separate from the salt. It is stored outside the database (e.g., in a configuration file or hardware security module). If an attacker gains access to the database but not the pepper, they still cannot crack the passwords. However, adding a pepper to libsodium’s crypto_pwhash requires care because the library expects only a password string. You can prepend the pepper to the password before passing it to the hashing function (e.g., strcat(pepper, password)) but ensure the pepper is properly handled in memory (see below).

Rate Limiting and Account Lockout

To defend against brute-force attacks, implement rate limiting on login endpoints. For example, limit attempts per IP address and per account. After a certain number of failures, enforce a time delay or lock the account temporarily. Use a sliding window or exponential backoff.

Password Policies

Encourage or enforce strong passwords through minimum length, complexity requirements, and checking against common password lists. However, avoid overly restrictive policies that push users to reuse passwords. Instead, recommend using a password manager.

Memory Scrubbing

C is memory-unsafe, and passwords can linger in memory. After hashing or verifying a password, securely erase the plaintext from memory. Use sodium_memzero() or explicit_bzero() rather than memset (which may be optimized away by the compiler).

// After verifying, clear the password buffer
sodium_memzero(password_buffer, password_len);

Additionally, use sodium_mlock() to prevent sensitive data from being swapped to disk.

Best Practices for Production Systems

  • Use established libraries – Never implement your own password hashing algorithm. libsodium, OpenSSL, and NSS are well-tested. OWASP’s Password Storage Cheat Sheet provides authoritative guidance.
  • Leverage built-in upgrade paths – When verifying a password, check if the stored hash uses outdated parameters. If so, rehash with stronger parameters and update the database.
  • Test error handling – Ensure that all functions return proper error codes and that the application fails closed (e.g., does not reveal whether a username exists in an error message).
  • Keep dependencies updated – Cryptographic libraries receive security patches. Subscribe to their release channels and update promptly.
  • Audit your code – Perform code reviews and use static analysis tools to detect common security flaws (e.g., buffer overflows, use of insecure functions like strcpy).

Conclusion

Building a secure password storage system in C requires careful attention to cryptographic principles and secure coding practices. By using libsodium’s crypto_pwhash_str() and crypto_pwhash_str_verify() with Argon2id, you can implement strong, adaptive password hashing with minimal code. Combine this with salting (handled automatically), proper memory management, rate limiting, and secure storage infrastructure to create a robust defense against credential theft. Always adhere to established guidelines from organizations like OWASP and NIST, and never trust your own custom cryptography. For further reading, the Argon2 RFC and libsodium docs offer deep technical details.