A login system must verify passwords, but it should not need to recover them. That distinction matters when an authentication database is copied through a vulnerability, backup exposure, or operational mistake.

If the database contains plaintext passwords, the compromise immediately reveals them. If it contains fast, unsalted hashes, an attacker can test large numbers of password guesses efficiently and reuse work across accounts.

Password hashing changes the problem. The application stores a verifier produced by a deliberately expensive password-hashing function. During login, it applies the same function to the submitted password and checks whether the result matches.

The goal is not to make guessing impossible. It is to make each offline guess costly enough that a stolen database is harder to turn into usable passwords.

Start with the right threat model

Password hashing primarily protects against offline password guessing after stored verifiers are exposed.

The important word is offline. Once an attacker has copied the password database, normal login controls such as request rate limits no longer govern the guessing process. The attacker can test candidate passwords on their own hardware.

A strong storage design therefore tries to increase the cost of every guess.

It does not solve every authentication problem. Password hashing does not protect a password that is captured by phishing, malware, a compromised application process, or unsafe logging. It also does not stop an attacker who already has a valid authenticated session.

Treat password hashing as one layer in authentication security, not as a complete account-protection strategy.

Do not encrypt passwords for normal verification

Encryption and password hashing solve different problems.

Encryption is reversible when the decryption key is available:

plaintext -> encrypt(key) -> ciphertext
ciphertext -> decrypt(key) -> plaintext

That is useful when an application genuinely needs to recover the original data. Normal password verification does not have that requirement.

A password-hashing function is designed for one-way verification:

submitted password + parameters -> password hash

The application compares the computed result with the stored verifier rather than decrypting a stored password.

This removes a valuable target: there is no decryption key that turns the entire password table back into plaintext. An attacker can still guess passwords, but each candidate must be tested through the password-hashing function.

Use a password-hashing function, not a general-purpose hash

General-purpose cryptographic hashes are designed to process data quickly. That property is useful for checksums, signatures, and many integrity constructions, but it works against password storage.

If legitimate verification is extremely cheap, offline guessing is cheap too.

Password-hashing functions deliberately consume computational resources. Modern designs can also require a configurable amount of memory. This is why algorithms intended for password storage should be preferred over directly applying a fast hash such as SHA-256 to a password.

A useful mental model is:

fast hash:
    make one computation inexpensive

password hash:
    make legitimate verification acceptable
    while making millions of guesses expensive

For new systems, Argon2id is a widely recommended memory-hard choice when a well-maintained implementation is available. Other established password-hashing schemes, including scrypt and bcrypt, remain present in real systems and can be appropriate depending on platform support and migration constraints.

The important design decision is to use a mature password-hashing implementation with tunable work parameters rather than inventing a construction from general-purpose primitives.

Give every password a unique random salt

A salt is a random value stored alongside the password verifier. Each password gets its own salt.

Conceptually:

salt = random value unique to this password
verifier = password_hash(password, salt, parameters)
store(salt, verifier, parameters)

The salt is not a secret. It can be stored in the same record as the verifier.

Its purpose is to make identical passwords produce different stored results and to prevent attackers from efficiently reusing precomputed work across many accounts.

Suppose Alice and Ben both choose the same password. Without salts, their stored hashes can be identical. That leaks the fact that the underlying passwords are probably the same and allows one computation to test the candidate against both records.

With independent salts:

same password + salt A -> verifier A
same password + salt B -> verifier B

An attacker must perform the password-hashing work separately for each salt.

Do not use a username, email address, account ID, or one application-wide constant as the salt. A salt should be generated by a cryptographically secure random source and should be unique for each stored password.

Most mature password-hashing libraries generate and encode salts for you. Prefer that behavior over designing your own storage format unless the platform requires otherwise.

Tune cost for your environment

Password-hashing algorithms expose work parameters because acceptable cost changes as hardware and applications change.

For a memory-hard function, tuning may include memory usage, iteration or time cost, and parallelism. Increasing these values can make each guess more expensive, but it also makes every legitimate login more expensive.

That creates a real trade-off.

If verification is too cheap, a stolen database is easier to attack. If verification consumes excessive CPU or memory, normal authentication can become slow and an attacker may be able to amplify resource pressure by sending many login attempts.

Choose parameters by measurement rather than copying arbitrary numbers from an old example.

A practical process is:

  1. start from current guidance for the password-hashing library or security standard you use;
  2. benchmark verification on production-like authentication hardware;
  3. choose the strongest settings that keep expected login and recovery workloads operationally acceptable;
  4. test concurrent verification, not only one isolated hash;
  5. revisit the settings as hardware and application capacity change.

The exact values are environment-dependent. A parameter set appropriate for a server fleet may be unsuitable for a small embedded device, and values chosen several years ago may no longer represent a useful cost.

Store the algorithm and parameters with the verifier

Password-hashing settings should be recoverable from the stored representation.

A password record needs enough information to determine how it was created:

algorithm
version, if applicable
work parameters
salt
verifier

Many password-hashing libraries provide a standard encoded string containing these fields. Using the library’s supported representation makes later verification and upgrades easier.

Avoid a design where one global configuration value silently defines how every historical password was hashed. If that setting changes, the application still needs to verify records created with the previous settings.

Self-describing verifiers make gradual migration possible.

Upgrade hashes during successful logins

Password-hashing settings should not remain frozen forever, but changing parameters does not require forcing every user to reset a password immediately.

Because the application does not know the plaintext password while the user is offline, it cannot simply recompute all stored verifiers in a background migration. A successful login provides a safe opportunity because the user has just supplied the password.

The flow can be:

verify submitted password using stored parameters
        |
        v
verification succeeds?
        |
        +-- no  -> reject login
        |
        +-- yes -> stored parameters still current?
                       |
                       +-- yes -> continue
                       |
                       +-- no  -> hash submitted password with current settings
                                  replace old verifier
                                  continue

This approach is often called rehashing on login.

Keep the verification and upgrade operation carefully ordered. Do not replace a verifier until the old verifier has successfully authenticated the submitted password. Use the password-hashing library’s verification and rehash helpers when available rather than manually parsing or comparing encoded hashes.

Accounts that do not log in will retain older verifiers. Whether that is acceptable depends on the risk of the old scheme. A seriously obsolete or compromised storage method may justify a forced password reset instead of waiting for gradual migration.

Compare verifiers through the library API

Password verification should use the password-hashing library’s verification function.

A mature API typically handles encoded parameters, salt extraction, hashing, and the final comparison. This reduces the chance of mistakes in custom parsing or comparison logic.

Conceptually, application code should look like:

if password_hasher.verify(stored_verifier, submitted_password):
    authenticate user
else:
    reject login

The exact API varies by language and library. The important point is that application code delegates the cryptographic operation to a maintained implementation.

Do not build a custom password-hashing scheme by concatenating a password and salt, repeatedly hashing them, and assuming that a large iteration count reproduces the security properties of a dedicated password-hashing algorithm. Small construction mistakes can be difficult to see in review and expensive to correct after deployment.

Consider a pepper only as an additional control

Some systems add a pepper: a secret value that is not stored in the password database and is incorporated into password verification.

The intended benefit is separation. If an attacker steals only the database but not the pepper, offline verification becomes harder because the attacker is missing another required value.

A pepper is not a replacement for salts or a proper password-hashing function. It also introduces operational responsibilities:

  • the pepper must be protected separately from the password database;
  • authentication services need reliable access to it;
  • compromise of the application environment may expose it;
  • rotation can be difficult because existing password verifiers depend on the old value.

Use a pepper only when your threat model and secret-management capabilities justify the added complexity. A well-configured password-hashing function with unique salts remains the foundation.

Keep passwords out of logs and secondary storage

A strong verifier does not help if the plaintext password is copied elsewhere.

Authentication code should make sure passwords do not appear in application logs, analytics events, tracing attributes, error reports, support tooling, or request dumps. Review observability systems as part of the password-storage boundary because they often receive data automatically.

Be especially careful around failed authentication. Logging the submitted password to diagnose why verification failed would create a second, much weaker password database.

Backups containing password verifiers also deserve protection. Hashing reduces the impact of verifier exposure; it does not make the data harmless. An attacker who obtains an old backup can still perform offline guessing against it.

Understand what stronger hashing cannot fix

Password hashing improves the outcome of one specific failure: exposure of stored password verifiers.

It cannot make a weak user-chosen password strong. A commonly guessed password may still be recovered quickly because the attacker tests likely candidates first.

It cannot stop online credential stuffing with passwords stolen from another service. Login abuse controls, breached-password screening where appropriate, and multi-factor authentication address different parts of that problem.

It cannot protect passwords after an application server is fully compromised and able to observe them during login.

This is why authentication security works best as layers. Password hashing limits damage from database exposure, while other controls address password choice, online abuse, phishing, session theft, recovery, and application compromise.

Verify the design, not only the happy path

A password-storage review should answer concrete questions.

Can two accounts with the same password have different stored verifiers? If not, investigate the salt design.

Can the application identify the algorithm and work parameters used for an older record? If not, upgrades may be fragile.

Can current parameters be increased without invalidating existing accounts? A rehash-on-login path should make that possible.

Does authentication remain available under realistic concurrent verification load? Cost tuning should include capacity testing.

Can plaintext passwords reach logs, traces, error reports, queues, or analytics systems? Those paths need explicit review.

Finally, test migration behavior. A legacy verifier should authenticate correctly, upgrade only after successful verification, and continue to work with the new representation on the next login.

Conclusion

Secure password storage is less about hiding a hash and more about controlling the economics of offline guessing.

Use a dedicated password-hashing function, give each password a unique random salt, tune work parameters against realistic hardware and load, store enough metadata to verify older records, and upgrade verifiers as users authenticate. Keep plaintext passwords out of logs and treat optional controls such as peppers as additional layers with their own operational costs.

These measures cannot eliminate weak passwords or every form of credential theft. They do make one common security failure—exposure of an authentication database—meaningfully harder to turn into account credentials.