A logbook for curious waters
← Software Engineering Field Guide

Secrets, Encryption, and Password Hashing

Before you begin: Bytes, credentials, cryptographic libraries, and authentication.

Secrets management controls access to credentials and keys. Encryption protects data that authorized code must later recover. Password hashing stores a verifier so a login can check a supplied password without retaining a decryptable copy of it. These mechanisms solve different problems. 1 2

InformationWhat the application needsAppropriate direction
A database credentialUse the credential to connectObtain it through a protected secret provider or workload identity
A private documentRecover its contents for an authorized readerUse established authenticated encryption and protected keys
A user’s login passwordCheck whether a later submission matchesUse a purpose-built password-hashing scheme

Base64 is only an encoding. It does not protect a credential from anyone who can read it.

Verify a password without decrypting one

Prefer the password facilities of an established authentication framework. For new systems, OWASP recommends Argon2id where available. The following standard-library example uses PBKDF2-HMAC-SHA-256 to make salting and verification visible; it is not a complete account system. Its 600,000-iteration setting follows OWASP’s published PBKDF2 guidance at the time of writing, and should not be treated as a permanent universal tuning value. 2

public sealed record PasswordRecord(byte[] Salt, byte[] Derived);

public static class Passwords
{
    private const int Iterations = 600_000;

    private static byte[] Derive(string password, byte[] salt)
    {
        byte[] input = System.Text.Encoding.UTF8.GetBytes(password);
        if (input.Length > 1024) throw new ArgumentException("Password input too long.");
        return System.Security.Cryptography.Rfc2898DeriveBytes.Pbkdf2(
            input, salt, Iterations,
            System.Security.Cryptography.HashAlgorithmName.SHA256, 32);
    }

    public static PasswordRecord Create(string password)
    {
        byte[] salt = System.Security.Cryptography.RandomNumberGenerator.GetBytes(16);
        return new PasswordRecord(salt, Derive(password, salt));
    }

    public static bool Verify(string password, PasswordRecord saved)
    {
        if (saved.Salt.Length != 16 || saved.Derived.Length != 32) return false;
        byte[] candidate = Derive(password, saved.Salt);
        return System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
            candidate, saved.Derived);
    }
}
PasswordRecord saved = Passwords.Create("demonstration password");
Console.WriteLine(Passwords.Verify("demonstration password", saved)); // True
Console.WriteLine(Passwords.Verify("wrong", saved));                 // False

The random salt is stored with the derived value. Verification repeats the derivation using that salt and compares the bytes. There is no decryption step. The record assumes trusted, validated storage data; its array properties are not an immutable storage format.

import hashlib
import hmac
import secrets

ITERATIONS = 600_000


def derive(password, salt):
    encoded = password.encode("utf-8")
    if len(encoded) > 1024:
        raise ValueError("Password input too long.")
    return hashlib.pbkdf2_hmac("sha256", encoded, salt, ITERATIONS, dklen=32)


def create_password_record(password):
    salt = secrets.token_bytes(16)
    return salt, derive(password, salt)


def verify_password(password, saved):
    salt, expected = saved
    if len(salt) != 16 or len(expected) != 32:
        return False
    return hmac.compare_digest(derive(password, salt), expected)
saved = create_password_record("demonstration password")
print(verify_password("demonstration password", saved))  # True
print(verify_password("wrong", saved))                  # False

The input limit bounds one resource risk and rejects rather than truncates oversized passwords. Both versions count UTF-8 bytes for that limit. They assume string input and a correctly shaped record from trusted storage.

Manage keys and credentials separately

Encryption requires a key that can recover plaintext. Use a maintained authenticated-encryption API and follow its nonce, key-size, and associated-data requirements; do not invent a cipher or compose encryption and authentication primitives casually. Protect and rotate keys separately from the encrypted records. Losing the only key can make legitimate recovery impossible, while storing it beside ciphertext with identical access controls may offer little protection against that compromise. 3

Application secrets belong outside source code and public logs. Prefer short-lived workload credentials where supported. Restrict secret-provider access, audit retrieval, and plan revocation after exposure. Moving a password into an environment variable is not enough if crash dumps or diagnostic endpoints reveal the environment. 1

Know what this code does not provide

A real password format records the algorithm and work parameters so settings can evolve and successful logins can trigger rehashing. It also needs vetted parsing, secure transport, rate limits, account-recovery rules, and protection against account enumeration. Using PBKDF2 alone does not establish FIPS compliance; that depends on the validated implementation and deployment requirements.

A salt is not a secret and does not prevent guessing a weak password. Slow hashing raises the cost of guesses; it does not make a compromised password harmless. Fast ordinary SHA-256 is appropriate for some integrity checks, but not as a replacement for password hashing. Keep these jobs separate instead of treating every unreadable-looking string as equivalent security.

Sources

  1. OWASP — Secrets Management Cheat Sheet.
  2. OWASP — Password Storage Cheat Sheet.
  3. OWASP — Cryptographic Storage Cheat Sheet.