shoppingfeed/password

This package is abandoned and no longer maintained. No replacement package was suggested.

This package has no released version yet, and little information is available.


README

This library provides a safe way to validate and store passwords.

Features

Hashes and validators

The interface ShoppingFeed\Password\PasswordHashStrategyInterface describes the contract of a hash method. Two implementations are available:

  • ShoppingFeed\Password\Md5PasswordHash: Produces an md5 sum of the given password with no salt (used for compatibility with legacy hashes)
  • ShoppingFeed\Password\StandardPasswordHash: Produces a sum of the password using password_hash built-in function. With PHP <= 7.1 the default hash is BCrypt.

The interface ShoppingFeed\Password\PasswordValidatorInterface describes the contract to validate a hash. Two implementations are available:

  • ShoppingFeed\Password\EqualPasswordValidator: This validator uses a hash strategy to get the sum of the given password and does a strict comparison with the valid hash.
  • ShoppingFeed\Password\StandardPasswordHash: This class implements a validator too. It uses password_verify built-in function to check that the given password is matching the valid hash. We need to use this function because password_hash generates a random IV each time it is called, so the output is never the same.

Password hash migration

It's a good practice to always update hashes when the used method becomes weak or broken (example: md5, sha1). To do this, each validator must implement the method needsRehash. For example, as Md5 hash is weak, this method always return true. The StandardPasswordHash uses password_needs_rehash built-in function to know if a hash needs to be updated or not, this means that the responsibility to update the hash to a new method is delegated to PHP security team.

The class ShoppingFeed\Password\RehashPasswordValidator can decorate any validator to allow it to update the hash directly in the validation process. If the hash and password are matching using the old hash method, an adapter is called to do the update in your database, the adapter must implement the interface ShoppingFeed\Password\RehashPasswordAdapterInterface.

Example of update process:

<?php
use ShoppingFeed\Application\Password\LegacyRehashPasswordAdapter;
use ShoppingFeed\Password\Md5PasswordHash;
use ShoppingFeed\Password\EqualPasswordValidator;
use ShoppingFeed\Password\RehashPasswordValidator;
use ShoppingFeed\Password\StandardPasswordHash;

$oldValidator = new EqualPasswordValidator(new Md5PasswordHash);
$newMethod = new StandardPasswordHash;
$adapter   = new LegacyRehashPasswordAdapter; // this class belongs to the application, it can't be generic

// The decorated validator uses the oldValidator to verify that the password is matching
// Then is uses the new method to hash the password the given one is valid.
// Finally, the new hash is passed to the adapter to update the hash in the database.
$decoratedValidator = new RehashPasswordValidator($oldValidator, $newMethod, $adapter);

// return false, the adapter is not called because the password is not matching the hash
$decoratedValidator->validate('foo', md5('bar'));

// return true, the adapter is called with a new BCrypt hash of the password "foo"
// 
// it may be hard to update the password with nothing else than a new hash,
// that's why you can pass a context array in 3rd parameter: the context is passed to the adapter.
// Here, the adapter can update the password of the user "42".
$decoratedValidator->validate('foo', md5('foo'), ['user' => 42]);

Working with many hashes types at the same time

The update process can help you to migrate old hashes to a new method, but during the process, you may have different hashes types to handle at the same time. If you can identity the hash type, handling many types is not a problem.

The class ShoppingFeed\Password\DelegatePasswordValidator can decorate many validators at the same time, using comparator functions to find the write validator to use.

Example:

<?php
// Let's imagine you use md5, sha1, sh256 and bcrypt hashes to store your passwords.
use ShoppingFeed\Password\DelegatePasswordValidator;
use ShoppingFeed\Password\Md5PasswordHash;
use ShoppingFeed\Password\EqualPasswordValidator;
use ShoppingFeed\Password\StandardPasswordHash;

// We build all the validator that we need
$md5Validator = new EqualPasswordValidator(new Md5PasswordHash());
$sha1Validator = new EqualPasswordValidator(new Sha1PasswordHash());
$sha256Validator = new EqualPasswordValidator(new Sha256PasswordHash());
$bcryptValidator = new StandardPasswordHash();

// Then we can build the delegate, it take one argument:
// The default validator to use. It will always be used if no other one can be used.
$delegateValidator = new DelegatePasswordValidator($md5Validator);

$delegateValidator
    // The first argument is the comparison function.
    // Here, we want to use the bcrypt validator if the hash starts with "$",
    // because it is  the only kind of hash with this specificity we use.
    ->addValidator(function (string $hash): bool { return $hash[0] === '$'; }, $bcryptValidator)
    // If we store a sha256 hash directly in binary, its length is 32 bytes.
    // Moreover, sha256 is the only method to produce a 32 bytes long hash in our hashes method.
    ->addValidator(function (string $hash): bool { return strlen($hash) === 32; }, $sha256Validator)
    // Same for sha1, it produces a 20 bytes long hash.
    ->addValidator(function (string $hash): bool { return strlen($hash) === 20; }, $sha1Validator)
;

// In this case, the delegate will use $bcryptValidator because the given hash starts with '$'
$delegateValidator->validate('foo', '$abcd');

// Here, it will use $sha256Validator because the hash is 32 bytes long
$delegateValidator->validate('foo', "\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04");

// Here, it will use $sha1Validator because the hash is 20 bytes long
$delegateValidator->validate('foo', "\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04");

// Here, it will use $md5Validator because no other validator matching this kind of hash (even if the given hash is not a valid md5).
$delegateValidator->validate('foo', "\x01\x02\x03\x04\x01\x02\x03\x04");

Combining migration and delegation

The goal of all these components is to provide a way to manage many hashes types, while doing a migration.

You can use the migration feature to update all your hashes to bcrypt, while supporting your old md5 hashes and bcrypt at the same time for the hashes that has already been updated.

Example:

<?php
use ShoppingFeed\Application\Password\LegacyRehashPasswordAdapter;

use ShoppingFeed\Password\DelegatePasswordValidator;
use ShoppingFeed\Password\Md5PasswordHash;
use ShoppingFeed\Password\EqualPasswordValidator;
use ShoppingFeed\Password\StandardPasswordHash;
use ShoppingFeed\Password\RehashPasswordValidator;

// Create old and new validator
$oldValidator = new EqualPasswordValidator(new Md5PasswordHash());
$newHashAndValidator = new StandardPasswordHash();

// Create the delegate to validate both md5 and bcrypt hashes
$delegateValidator = new DelegatePasswordValidator($newHashAndValidator);
$delegateValidator->addValidator(function ($hash) { return $hash[0] !== '$'; }, $oldValidator);

// Setup the rehash validator decorator
$validator = new RehashPasswordValidator($delegateValidator, $newHashAndValidator, new LegacyRehashPasswordAdapter());

// And now we have a validator that:
// - verify the given password using an md5 or bcrypt hash
// - always update md5 hashes to bcrypt
// - may update bcrypt to another new method if the php security team set a new default method.
if ($validator->validate($_GET['password'], $user['password'], ['user' => $user])) {
    // the password is valid and may have been updated, but we don't care in this userland code :-)
} else {
    // the password is not valid and has not been updated.
}