shoppingfeed/password

此包已被废弃且不再维护。未建议替代包。

此包尚未发布任何版本,可用的信息很少。


README

此库提供了一种安全的方式来验证和存储密码。

特性

散列和验证器

接口 ShoppingFeed\Password\PasswordHashStrategyInterface 描述了散列方法的合约。有两种实现

  • ShoppingFeed\Password\Md5PasswordHash:生成给定密码的 md5 摘要,没有盐(用于与旧散列兼容)
  • ShoppingFeed\Password\StandardPasswordHash:使用内置函数 password_hash 生成密码的摘要。在 PHP <= 7.1 中,默认散列是 BCrypt。

接口 ShoppingFeed\Password\PasswordValidatorInterface 描述了验证散列的合约。有两种实现

  • ShoppingFeed\Password\EqualPasswordValidator:此验证器使用散列策略获取给定密码的摘要,并与有效散列进行严格比较。
  • ShoppingFeed\Password\StandardPasswordHash:此类还实现了验证器。它使用内置函数 password_verify 检查给定的密码是否与有效散列匹配。我们需要使用此函数,因为 password_hash 每次调用都会生成一个随机的 IV,因此输出永远不会相同。

密码散列迁移

当使用的散列方法变得脆弱或损坏时(例如:md5、sha1),始终更新散列是一个好习惯。为此,每个验证器都必须实现方法 needsRehash。例如,由于 Md5 散列脆弱,此方法始终返回 true。《StandardPasswordHash》使用内置函数 password_needs_rehash 来确定散列是否需要更新,这意味着将更新散列到新方法的责任委托给了 PHP 安全团队。

ShoppingFeed\Password\RehashPasswordValidator 可以装饰任何验证器,以便在验证过程中直接更新散列。如果使用旧散列方法匹配散列和密码,则调用适配器来在数据库中进行更新,适配器必须实现接口 ShoppingFeed\Password\RehashPasswordAdapterInterface

更新过程的示例

<?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]);

同时处理多种散列类型

更新过程可以帮助您将旧散列迁移到新方法,但在过程中,您可能需要同时处理多种散列类型。如果您可以识别散列类型,处理多种类型不是问题。

ShoppingFeed\Password\DelegatePasswordValidator 可以同时装饰多个验证器,使用比较函数来找到要使用的正确验证器。

示例

<?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");

结合迁移和委派

所有这些组件的目标是提供一种管理多种散列类型的方法,同时进行迁移。

您可以使用迁移功能将所有散列更新为 bcrypt,同时支持旧 md5 散列和 bcrypt,对于已更新的散列同时支持。

示例

<?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.
}