Use when storing user passwords or credentials in a database — covers algorithm selection, work factor configuration, and migration from legacy hashing schemes.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add jeffreytse/grimoire-core --skill apply-password-hashing --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Apply Password Hashing?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jeffreytse-apply-password-hashing)More formats (shields.io, HTML) on the badges page.
---
name: apply-password-hashing
description: Use when storing user passwords or credentials in a database — covers algorithm selection, work factor configuration, and migration from legacy hashing schemes.
source: 'OWASP Password Storage Cheat Sheet (owasp.org/www-project-cheat-sheets); NIST SP 800-63B Section 5.1.1.2; CWE-916; OWASP Top 10 2021 A07'
tags: [security, owasp, passwords, bcrypt, argon2, hashing, authentication, developer]
---
# Apply Password Hashing
Store passwords with a memory-hard, salted, slow hash function (Argon2id, bcrypt, or scrypt) — never MD5, SHA-1, SHA-256, or unsalted hashes.
## Why This Is Best Practice
**Adopted by:** NIST SP 800-63B (authoritative US federal standard for digital identity) mandates slow hashing for memorized secrets. OWASP Top 10 2021 A07 (Authentication Failures) cites weak password storage as a primary failure mode. Django defaults to Argon2, ASP.NET Core Identity defaults to PBKDF2, Spring Security defaults to bcrypt. PCI DSS v4.0 Requirement 8.3.2 mandates strong hashing for stored passwords.
**Impact:** Every major password breach demonstrates the cost of weak hashing — LinkedIn (2012, SHA-1, 117M cracked in days), RockYou (2009, plaintext, 32M passwords), Adobe (2013, 3DES ECB, trivially reversible). bcrypt with cost factor 12 requires ~300ms per attempt; modern GPU rigs crack MD5 at 100 billion hashes/second. Proper hashing makes offline cracking economically infeasible for strong passwords.
**Why best:** Fast hashes (MD5, SHA-256) are designed for speed and destroy security — a single GPU cracks billions per second. Argon2id wins over bcrypt by resisting GPU attacks via memory-hardness; it wins over PBKDF2 by resisting ASICs. bcrypt is the safe fallback when Argon2 is unavailable.
Sources: OWASP Password Storage Cheat Sheet; NIST SP 800-63B; HaveIBeenPwned breach analysis; CWE-916
## Steps
1. **Choose the right algorithm by priority**:
| Priority | Algorithm | Use when |
|----------|-----------|----------|
| 1st | Argon2id | Modern apps — best resistance to GPU/ASIC cracking |
| 2nd | bcrypt | Argon2 unavailable; widely supported; proven 25+ years |
| 3rd | scrypt | Alternative memory-hard option |
| 4th | PBKDF2-HMAC-SHA256 | FIPS compliance required |
| Never | MD5, SHA-*, plain | Not password hashing algorithms |
2. **Configure Argon2id with secure parameters** — OWASP minimum (2023):
```python
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=2, # iterations
memory_cost=19456, # 19 MB memory
parallelism=1,
hash_len=32,
salt_len=16
)
hash = ph.hash(password)
```
Tune `memory_cost` and `time_cost` so hashing takes ≥500ms on your hardware.
3. **Configure bcrypt with work factor ≥12**:
```python
import bcrypt
salt = bcrypt.gensalt(rounds=12) # never use rounds < 10
hashed = bcrypt.hashpw(password.encode(), salt)
```
```javascript
const bcrypt = require('bcrypt');
const ROUNDS = 12;
const hash = await bcrypt.hash(password, ROUNDS);
```
Benchmark: increase `rounds` until hashing takes ~300–500ms on your hardware.
4. **Never manage salts manually** — Argon2, bcrypt, and scrypt generate and embed a random salt automatically. The output string includes the salt. Do NOT add a separate global "pepper" without understanding the implications.
```python
# bcrypt output embeds the salt — store the full string:
# $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdBPj9bIyZzAQm.
```
5. **Verify correctly — never re-hash and compare**:
```python
# Argon2
try:
ph.verify(stored_hash, provided_password)
if ph.check_needs_rehash(stored_hash):
# rehash with updated parameters on next login
new_hash = ph.hash(provided_password)
update_hash_in_db(user_id, new_hash)
except argon2.exceptions.VerifyMismatchError:
raise AuthenticationError()
```
6. **Migrate legacy hashes without forcing password resets** — wrap old hashes:
```python
# On login: detect old bcrypt hash, rehash with Argon2, store new
if is_legacy_bcrypt(stored_hash):
if bcrypt.checkpw(password, stored_hash):
new_hash = ph.hash(password)
update_to_argon2(user_id, new_hash)
```
7. **Enforce a maximum password length of 64–128 characters** — bcrypt silently truncates at 72 bytes; Argon2 does not, but extremely long passwords can cause DoS. Validate before hashing:
```python
if len(password) > 128:
raise ValueError("Password too long")
```
## Rules
- Always use the library's built-in salt generation — manual salting is error-prone.
- Store only the full hash string (which includes algorithm, parameters, and salt) — never store passwords or salts separately.
- Increase work factors annually — hardware gets faster; a 2015 bcrypt cost 10 is a 2025 cost 9 equivalent.
- Do NOT pre-hash passwords before bcrypt (e.g., `bcrypt(sha256(password))`) — introduces new vulnerabilities without benefit.
## Common Mistakes
- **Using `sha256(password + salt)` and calling it "secure"** — fast hashing with a salt is still fast; bcrypt with no salt is still better than SHA-256 with a salt.
- **Rolling your own hashing** — every custom implementation has missed bcrypt's cost factor, truncation handling, or encoding edge cases.
- **Storing the hash encoding as `hex` instead of the full output string** — loses the embedded parameters and salt.
- **Using rounds < 10 for bcrypt** — cost factors below 10 are crackable with consumer hardware.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!