---
title: 'Password Storage'
source: 'https://academia.sh/en/courses/php-web/password-storage'
course: 'PHP on the Web: State, Data and Security'
language: en
updated: '2026-10-21T05:03:37+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Hashes and verifies a password with password_hash() and password_verify(), and measures the prefix of the hash without printing the whole of it'
    - 'Shows with the party measurement who determines the result of the cost parameter'
    - 'Rehashes an old-cost hash at login with password_needs_rehash()'
---

# Password Storage

Measuring that a password is stored as a hash, that a different salt gives a different hash for the same password, the cost parameter, and rehashing at login; the default cost is in the environment's hands.

The Request Forgery Protection lesson had the server determine who a request came from. If the identity itself rests on a password, the question of how the password is stored remains: who determines the stored value and the setting that produces it? This lesson measures the built-in way of storing a password and whether one of its settings is in the environment's hands.

**SE5.** In this lesson the password and the hash are used only for storage and verification. Guessing passwords, cracking hashes and reversing a stored hash are not modeled. The whole of a hash is printed nowhere; only the prefix, the length and the verification result are printed.

## Hash and Verification

A password is not stored as it is. A **hash** is a one-way digest of the password: the same password gives the same digest, but the password cannot be produced from the digest. For passwords this is done with `password_hash()`. The function generates a random **salt** on every call and writes it into the hash; so two hashes of the same password differ from each other. `password_verify()` takes the password and the hash, reads the salt and the setting inside the hash, and verifies. The `$2y$12$` prefix at the start of the hash carries the algorithm (`2y`, bcrypt) and the **cost** parameter.

The file below defines the functions used in the lesson: producing a hash, taking the prefix, and verifying at login and rehashing if needed.

```php
<?php
// password.php — hashing, prefix and rehashing at login
declare(strict_types=1);

function hashPassword(string $password, ?int $cost = null): string {
    if ($cost === null) {
        return password_hash($password, PASSWORD_BCRYPT);
    }
    return password_hash($password, PASSWORD_BCRYPT, ['cost' => $cost]);
}

function prefix(string $hash): string {
    return substr($hash, 0, 7);
}

function logIn(string $password, string $storedHash, int $targetCost): array {
    if (!password_verify($password, $storedHash)) {
        return ['succeeded' => false, 'hash' => $storedHash];
    }
    if (password_needs_rehash($storedHash, PASSWORD_BCRYPT, ['cost' => $targetCost])) {
        $storedHash = hashPassword($password, $targetCost);
    }
    return ['succeeded' => true, 'hash' => $storedHash];
}
```

The first measurement hashes a password; verifies a correct and a wrong password; hashes the same password twice and checks whether the hashes differ and whether both are verified.

```php
<?php
// hash.php — hash, prefix and verification
declare(strict_types=1);
require 'password.php';

$hash = hashPassword('example-password');
$second = hashPassword('example-password');
echo 'prefix ', prefix($hash), ' · length ', strlen($hash), "\n";
echo 'correct password ', var_export(password_verify('example-password', $hash), true), "\n";
echo 'wrong password ', var_export(password_verify('other-password', $hash), true), "\n";
echo 'two hashes ', $hash === $second ? 'same' : 'different', "\n";
echo 'second hash verifies ', var_export(password_verify('example-password', $second), true), "\n";
```

```
prefix $2y$12$ · length 60
correct password true
wrong password false
two hashes different
second hash verifies true
```

The hash is 60 characters and starts with `$2y$12$`: bcrypt, cost 12. The correct password was verified, the wrong one was not. The two hashes of the same password differ, because a new salt was written on every call; even so, the second one was also verified with the same password. Verification does not compare hashes; it rehashes the password with the salt inside the hash and compares the result. The whole of the hash was not printed; the prefix and the length are enough for the measurement.

## Party: Who Sets the Cost

The cost parameter determines how much work producing a hash requires. If it is not given explicitly, the runtime's default is used; this default depends on the PHP version (10 before 8.4, 12 after). The same code can produce a different prefix in another environment. For the measurement, the environment's deviation is the default cost being 10. The decision order is client, third party, environment; if none could change it, the server.

```php
<?php
// party.php — who can change this result?
declare(strict_types=1);

// The parties outside the code, in decision order: every request, every external call, every deployment.
const EXTERNAL_PARTIES = ['client', 'third party', 'environment'];

/** Computes the result once without deviation, then once with each external party's deviation. */
function probe(callable $result, array $deviations): array {
    $baseline = $result([]);
    $changed = [];
    foreach (EXTERNAL_PARTIES as $party) {
        if (array_key_exists($party, $deviations)) {
            $changed[$party] = $result([$party => $deviations[$party]]) !== $baseline;
        }
    }
    return $changed;
}

/** The first external party that can change the result; the server if none can. */
function party(array $changed): string {
    foreach (EXTERNAL_PARTIES as $p) {
        if ($changed[$p] ?? false) return $p;
    }
    return 'server';
}

function partyReport(string $name, callable $result, array $deviations): string {
    $changed = probe($result, $deviations);
    $lines = [];
    foreach ($changed as $p => $c) $lines[] = $p . ($c ? ' changed' : ' could not change');
    $owner = party($changed);
    echo "$name → $owner\n    ", implode(' · ', $lines), "\n";
    return $owner;
}
```

The result is the prefix of the produced hash. The client's deviation is another password.

```php
<?php
// cost.php — the party of the hash prefix
declare(strict_types=1);
require 'party.php';
require 'password.php';

function defaultPrefix(string $password, int $cost): string {
    return prefix(hashPassword($password, $cost));
}

partyReport(
    'default cost',
    fn(array $d) => defaultPrefix($d['client'] ?? 'example-password', $d['environment'] ?? PASSWORD_BCRYPT_DEFAULT_COST),
    ['client' => 'other-password', 'environment' => 10]
);
```

```
default cost → environment
    client could not change · environment changed
```

The owner of the prefix is the **environment**. The client's password did not change any character of the prefix: the password determines the content of the hash, not its setting. When the default cost became 10 instead of 12, the prefix became `$2y$10$` instead of `$2y$12$`. Because the cost is not written in the code, the setting is determined by the version of the runtime; the same code silently produces a hash with another cost in another deployment.

## Rehashing at Login

The cost is raised over time, but the stored hashes stay at the old cost. `password_needs_rehash()` says whether a hash fits the target setting. The plain-text password is available only at login, when the user types it, so rehashing is done there too. The measurement below compares a hash with cost 10 against a target cost of 12, then tries a login with a correct and a wrong password.

```php
<?php
// login.php — rehashing at login
declare(strict_types=1);
require 'password.php';

$old = hashPassword('example-password', 10);
echo 'old prefix ', prefix($old), "\n";
echo 'rehash needed ', var_export(password_needs_rehash($old, PASSWORD_BCRYPT, ['cost' => 12]), true), "\n";

$correct = logIn('example-password', $old, 12);
echo 'correct password ', var_export($correct['succeeded'], true), ' · prefix ', prefix($correct['hash']), "\n";
$wrong = logIn('other-password', $old, 12);
echo 'wrong password ', var_export($wrong['succeeded'], true), ' · prefix ', prefix($wrong['hash']), "\n";
```

```
old prefix $2y$10$
rehash needed true
correct password true · prefix $2y$12$
wrong password false · prefix $2y$10$
```

The hash with cost 10 gave `true` from `password_needs_rehash()` for the target 12. At a login with the correct password, the hash was produced again and the prefix became `$2y$12$`. A login with the wrong password failed and the stored hash was not touched: the prefix stayed `$2y$10$`. Doing the rehashing at login is not a preference but a necessity, because the plain-text password is available at no other moment.

## The Bounding Measurement

If the cost is given explicitly, the default is not read. The code writing `hashPassword($password, 12)` removes the environment's default from the result. The same party measurement is repeated here with the cost fixed; the environment's deviation (10) is the same.

```php
<?php
// explicit.php — the party when the cost is given explicitly
declare(strict_types=1);
require 'party.php';
require 'password.php';

function explicitPrefix(string $password): string {
    return prefix(hashPassword($password, 12));
}

partyReport(
    'explicit cost',
    fn(array $d) => explicitPrefix($d['client'] ?? 'example-password'),
    ['client' => 'other-password', 'environment' => 10]
);
```

```
explicit cost → server
    client could not change · environment could not change
```

When the cost is written in the code as `12`, the environment's default does not enter the result: the environment's deviation (10) changed nothing in the prefix. The owner is the **server**. This is not the server increasing the number of settings, but the environment no longer being able to determine anything; to raise the cost, the code is changed, while `password_needs_rehash()` carries old hashes to the new setting at login.

## Summary

- The measured class of this lesson is the **environment** under the default cost and the **server** under the explicit cost: the cost part of the prefix was determined by the runtime's default in the first and by the number written in the code in the second.
- `password_hash()` generates a new salt on every call; two hashes of the same password differ and both are verified with `password_verify()`. The hash is 60 characters, and its prefix carries the algorithm and the cost.
- A different password did not change the result of the prefix: the password determines the content of the hash, not its setting.
- `password_needs_rehash()` catches an old-cost hash; rehashing is done at the only place the plain-text password is available, at a correct login. A wrong password does not change the hash.

## Next Step

Storing the password is a part of an authentication flow. When a password, a session, a token and an authorization work together, who determines the identity of the request? The Authentication Flows lesson takes this up together with the findings of the whole security topic.
