Search by

units / password

ecom

Password-hashing unit - Argon2id, Argon2i and bcrypt via PHP's password_* functions, on the patterns/unit contract.

v1.0.1 2026-10-11 03:00 UTC

This package is auto-updated.

Last update: 2026-10-11 03:00:47 UTC


README

Store passwords so they can be checked, and never read back.

use Units\Password\Password;

$passwords = Password::create(['algorithm' => 'argon2id']);

$stored = $passwords->hash($password)->hash;   // store this
$passwords->verify($password, $stored);        // true
$passwords->verify('wrong', $stored);          // false

PHP's own password_hash(), password_verify() and password_needs_rehash(). The cryptography is the one your PHP was already built and tested with.

The words

Password hash — one way, and slow on purpose, so guessing is expensive.

Salt — random input inside the hash, so the same password gives a different result every time. Automatic here, and the reason one cracked password does not open every account that shares it.

Pepper — a secret mixed into the password before hashing, kept out of the database. Not here: it is your secret and your decision.

Cost — how much work the hash does. Higher is slower, and stronger.

Storing

$stored = $passwords->hash($password)->hash;

// $argon2id$v=19$m=65536,t=4,p=1$…

One line of text, carrying its own algorithm, its own settings and its own salt. Store it in a VARCHAR(255).

The same password gives a different string every time. Never store a constant, never compare two stored values, never expect them to match.

Checking

if ($passwords->verify($password, $stored)) {
    // signed in
}

The only way to check. It reads the algorithm out of the stored value, not out of your configuration, so one unit checks today's Argon2id hashes and 2019's bcrypt hashes alike.

verify() never throws. An empty, truncated or nonsense value is simply false:

$passwords->verify($password, '');          // false
$passwords->verify($password, 'nonsense');  // false

Upgrading stored passwords

Settings get stronger over time. You can only re-hash while the password is in hand — right after a successful check:

if ($passwords->verify($password, $stored) && $passwords->needsRehash($stored)) {
    $stored = $passwords->hash($password)->hash;   // quietly stronger
}

Nobody resets anything. Each user is upgraded the next time they sign in.

What a stored hash says about itself

use Units\Password\PasswordResult;

$result = PasswordResult::fromHash($stored);

$result->algorithm;   // 'argon2id'
$result->options;     // ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1]
(string) $result;     // the stored value, unchanged

PasswordResult::fromHash('nonsense')->algorithm;   // 'unknown' — a fact, not a crash

The setting that does nothing

Settings go to PHP unchanged, so a setting from the wrong algorithm is silently ignored — by PHP, and therefore by you:

Password::create(['algorithm' => 'argon2id', 'options' => ['cost' => 12]])
    ->hash($password)->options;

// ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1]  — PHP's defaults

cost is bcrypt's. Argon2id has none, so that is not a stronger Argon2id — it is an ordinary one, and needsRehash() will not notice either. Ask the hash instead of trusting the request:

isset($result->options['cost']);    // false — never used
$result->options['memory_cost'];    // 65536 — used

Argon2id takes memory_cost, time_cost, threads. bcrypt takes cost.

Prefer Argon2id

$passwords->algorithms();          // ['argon2id', 'argon2i', 'bcrypt']
$passwords->supports('argon2id');  // true only if this PHP was built with it

Argon2id is optional in PHP, so the unit refuses an algorithm this build cannot run — when you configure it, not at a signup:

Password::create(['algorithm' => 'scrypt']);
// InvalidArgumentException: [password] unsupported algorithm: scrypt - supported: argon2id, argon2i, bcrypt

Use bcrypt when something else has to read the value.

The pepper is yours

A pepper never enters the database, so stealing the table is not enough on its own. Where it comes from, how it rotates, whether you want one — your call, not a library's. This unit hashes what it is handed.

// an application composing both units
$peppered = Hasher::create()->hmac($password, $pepper)->hash;
$stored   = Password::create(['algorithm' => 'argon2id'])->hash($peppered)->hash;

Not the hasher

units/hasher fingerprints data: fast, always the same answer. This protects credentials: slow, never the same answer. Each refuses to be the other — Password has no hmac() or sha256(), Hasher has no needsRehash().

It knows its version

$row = ['hash' => $stored, 'dna' => $passwords->dna()->toArray()];

Password::schema()->label();     // Password v1.0.0
Password::schema()->version();   // '1.0.0' - which scheme hashed this

Reference

Method Gives you
create(['algorithm' => …, 'options' => …, 'dna' => …]) a password unit
hash($password) PasswordResult — store ->hash
verify($password, $stored) bool, never throws
needsRehash($stored) bool — is this behind the settings
algorithms() supports($name) what this PHP can actually run
algorithm() options() dna() whoami() help() itself

PasswordResult carries hash, algorithm and options. It is immutable, and (string) $result is the stored value.

MIT · tests: composer test