ASVS 5.0 V6.5.2 says lookup secrets such as recovery codes that carry less than 112 bits of entropy must be stored with an approved password hashing algorithm and a random salt of at least 32 bits. If the secret has 112 bits or more, a standard hash is acceptable. It is a Level 2 requirement.
Why it matters
Recovery codes are often short, like ten digits. If your database leaks and they are stored in plain text or a fast hash, an attacker can use them or crack them in seconds. A slow, salted hash makes each guess expensive.
The entropy figure is the dividing line. Nineteen random alphanumeric characters or 34 random digits reach 112 bits. Most codes users can type are shorter.
How to implement it
Pick one of two approaches and document it.
Option A, long codes. Generate at least 112 bits and store a SHA-256 hash.
import secrets, hashlib
code = secrets.token_hex(16) # 128 bits
stored = hashlib.sha256(code.encode()).hexdigest()
Option B, short codes. Use a password hash with a per code salt.
from argon2 import PasswordHasher
ph = PasswordHasher()
code = "".join(secrets.choice("23456789ABCDEFGHJKMNPQRSTUVWXYZ") for _ in range(10))
stored = ph.hash(code) # salt is generated and embedded
Show the plain codes to the user once at creation. Never log them.
How to test it
- Inspect the database for the recovery table. No plain codes, no unsalted fast hashes for short ones.
- Create two accounts with the same code in a test fixture and confirm the stored values differ.
- Review the generator to calculate entropy: characters in the alphabet raised to the length.
Common mistakes
- MD5 or unsalted SHA-1 of a 10 digit code.
- Reversible encryption so support can "show" the codes again.
- Logging codes at generation time.
Quick check
Is a SHA-256 hash fine for an 8 digit code?
No. Eight digits is far below 112 bits, so it needs a salted password hashing algorithm.