Check four things: the algorithm, its cost parameters, a unique salt per password, and how existing hashes are migrated. OWASP's current order of preference is Argon2id, then scrypt, then bcrypt for legacy systems, with PBKDF2 where FIPS-140 compliance is required.
The recommended order
The OWASP Password Storage Cheat Sheet ranks the options as follows, with the minimums it states:
| Algorithm | Minimum settings stated | When to use |
|---|---|---|
| Argon2id | 19 MiB memory, 2 iterations, parallelism 1 | First choice |
| scrypt | N=2^17 (128 MiB), r=8, p=1 | When Argon2id is not available |
| bcrypt | Work factor of at least 10 | Legacy systems only |
| PBKDF2 | 600,000 iterations with HMAC-SHA-256 | When FIPS-140 is required |
The sheet lists equivalent memory and iteration trade-offs for Argon2id, so a different balance may be fine. Check the current page when you set values, since recommendations get updated.
Reviewer questions
- Which algorithm and parameters does the diff use? Compare against the table.
- Is the salt unique and random per password? Salts need not be secret, but they must be unique. Most common libraries generate and store them automatically, so a hand rolled salt is a flag.
- Is anything fast used for passwords, such as a plain hash function? That is a reason to block.
- Where is a pepper stored, if used? The sheet says it should live separately from the database, ideally in a vault or HSM, and it adds defense in depth but no security on its own.
- How do old hashes move to the new scheme? A common approach is to rehash at next login, but the plan should be written down.
bcrypt traps
The sheet notes bcrypt accepts a maximum of 72 bytes in most implementations, so enforce a 72 byte maximum password length or a lower limit your implementation has. It also warns that pre-hashing before bcrypt is risky because of null byte truncation and password shucking. If pre-hashing is unavoidable, it suggests HMAC-SHA-384 keyed with a pepper and base64 encoded, with the pepper stored outside the database.
# red flag: a general purpose hash, no salt, no work factor
digest = hashlib.sha256(password.encode()).hexdigest()
# better: a password hashing library with sensible defaults
hasher = argon2.PasswordHasher()
digest = hasher.hash(password)
What to ask for in tests
Ask for a test that hashes the same password twice and expects different outputs, and one that verifies a known hash from the old scheme still works during migration. Also check that the password never appears in logs or error messages on failure.
Where Safeguard fits
Safeguard scans dependencies and SBOMs, including the crypto libraries you pick. Try the free scan at gold.safeguard.sh/scan. Parameter choices remain a review call.
Quick check
Is bcrypt acceptable?
OWASP lists it for legacy systems where Argon2id and scrypt are not available, with a work factor of at least 10.
Does a pepper replace a salt?
No. Each password still needs a unique salt. A pepper is an extra shared secret stored away from the database.
Sources
Last reviewed: 19 October 2026.