1.2 Hashes, MACs, KDFs, and Password Storage
Hashes, MACs, KDFs, and password hashes can all produce apparently random bytes. Their purpose cannot be inferred from that appearance; some APIs also allow configurable output lengths. Using SHA-256(password) as a password storage mechanism, or applying a standard hash function to sign a message, is a misapplication, confusing similar interfaces with equivalent security semantics.
Ordinary Hashes Lack Secrets
digest = Hash(message)Cryptographic hashes typically aim for preimage resistance, second-preimage resistance, and collision resistance. They are well-suited for content addressing, integrity fingerprints, and components of signing protocols.
However, anyone can recompute a standard hash. If an attacker can simultaneously replace both the file and its digest, SHA-256(file) cannot prove the file originated from a specific source. MD5 and SHA-1 are no longer suitable for collision-resistant scenarios; modern designs generally use SHA-256/384, SHA-3, or explicitly specified contemporary algorithms.
MACs Authenticate Messages Under a Shared Key
tag = HMAC(key, message)HMAC incorporates a secret key. Assuming the algorithm and key remain secure, a valid tag indicates that a key holder authenticated the message. Since the verifier also has the key, it cannot prove to a third party which holder generated it. Since the verifier must possess the same key, it is well-suited for service-to-service webhooks, internal protocols, and ensuring data integrity in database fields.
During verification, use the library's constant-time comparison to avoid leaking the position of the first differing byte through standard string comparisons. Additionally, encode the protocol context into the message:
version || method || path || timestamp || body_digestFields need unambiguous lengths or a canonical encoding. Direct concatenation maps both (ab,c) and (a,bc) to abc; delimiters also need escaping if field values can contain them. To prevent replay attacks, also validate the time window and a one-time ID; a valid HMAC does not prove first arrival. Checking that an ID is unused and marking it used must be atomic, or concurrent replays can both pass.
KDF for Deriving Isolated Subkeys
Do not reuse one master key directly for encryption, MACs, token signing, and database-field protection. HKDF-based KDFs can derive subkeys with isolated purposes from input key material:
K_encrypt = HKDF(master, info="atlas/v1/encryption")
K_mac = HKDF(master, info="atlas/v1/webhook-mac")info enables domain separation. Even when the underlying master key is the same, different protocols and uses won't directly reuse the same output. HKDF is suitable for high-entropy key material, not for "making user-generated short passwords safe."
Passwords Need Intentionally Expensive, Dedicated Functions
User passwords often have low entropy, making them vulnerable to offline guessing once an attacker gains access to the database. Generic hash functions are too fast and actually assist attackers. Password verification should use a memory-hard password hashing function with an independent random salt, such as Argon2id:
encoded = Argon2id(password, random_salt, memory, iterations, parallelism)The salt does not need to be kept secret. Its purpose is to ensure that identical passwords produce different verifier values, preventing attackers from precomputing hash tables that could compromise all accounts. Each record must store the algorithm, version, parameters, salt, and output, enabling future upgrades without breaking compatibility.
Parameters should be versioned and reviewed as hardware and workload change. They must be benchmarked under real production hardware and peak concurrency loads, tuned so that individual verification is slow enough to deter attackers but still within system capacity, with extra headroom to guard against login flood attacks. RFC 9106 provides reference configurations for Argon2id, but actual services must adjust based on available memory and denial-of-service risk.
A pepper is an additional server-side secret that can be stored in a KMS/HSM and applied before or after hashing using a reviewed, auditable method. It ensures that an attacker who only compromises the database lacks a complete set of inputs, but adds complexity to rotation and disaster recovery procedures. A pepper cannot replace the salt or a properly configured KDF.
Login Verification Should Support Progressive Upgrades
Read record → Verify the submitted password using the recorded parameters
→ If parameters are outdated, recalculate using updated parameters upon successful loginThis avoids storing plaintext passwords long term, but recalculation requires the password submitted in the successful login. An old verifier alone cannot generally be upgraded. The write must also avoid overwriting a concurrent password reset. Accounts that remain inactive for extended periods can be forcibly reset in the event of a security incident.
Password policies also directly impact real-world security. NIST SP 800-63B-4 prohibits mandatory character-composition rules for verifiers following that standard. Instead, it supports longer passwords and password manager paste functionality, discourages common or previously leaked passwords, limits online guessing attempts, and requires password changes when evidence of compromise is detected, rather than mandating periodic, arbitrary rotations.
A single-factor password must be at least 15 characters long; shorter passwords may be permitted as part of a multi-factor authentication flow, but no shorter than 8 characters. These are requirements within NIST’s stated scope, not a universal requirement for all products to mechanically enforce the same minimum length. Businesses must still model password requirements based on their identity assurance levels and regulatory obligations.
Test Random Salts and Rejected Passwords
This example requires argon2-cffi. Its high-level PasswordHasher API generates random salts and records parameters. Library defaults demonstrate the behavior; production still needs concurrency and memory benchmarks. Do not log submitted passwords or use the example constant as a real credential.
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
hasher = PasswordHasher()
password = "example passphrase for this local check"
encoded = hasher.hash(password)
assert encoded !=