login consent app sql

This commit is contained in:
2022-05-03 08:54:45 +02:00
parent e7253acfd8
commit f9a6535906
1652 changed files with 187600 additions and 45 deletions

View File

@ -0,0 +1,5 @@
5.3
---
* Add the component
* Use `bcrypt` as default algorithm in `NativePasswordHasher`

View File

@ -0,0 +1,223 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
/**
* Hashes a user's password.
*
* @author Sarah Khalil <mkhalil.sarah@gmail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @final
*/
class UserPasswordHashCommand extends Command
{
protected static $defaultName = 'security:hash-password';
protected static $defaultDescription = 'Hash a user password';
private $hasherFactory;
private $userClasses;
public function __construct(PasswordHasherFactoryInterface $hasherFactory, array $userClasses = [])
{
$this->hasherFactory = $hasherFactory;
$this->userClasses = $userClasses;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setDescription(self::$defaultDescription)
->addArgument('password', InputArgument::OPTIONAL, 'The plain password to hash.')
->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the hasher used to hash the password.')
->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the hasher generate one.')
->setHelp(<<<EOF
The <info>%command.name%</info> command hashes passwords according to your
security configuration. This command is mainly used to generate passwords for
the <comment>in_memory</comment> user provider type and for changing passwords
in the database while developing the application.
Suppose that you have the following security configuration in your application:
<comment>
# app/config/security.yml
security:
password_hashers:
Symfony\Component\Security\Core\User\InMemoryUser: plaintext
App\Entity\User: auto
</comment>
If you execute the command non-interactively, the first available configured
user class under the <comment>security.password_hashers</comment> key is used and a random salt is
generated to hash the password:
<info>php %command.full_name% --no-interaction [password]</info>
Pass the full user class path as the second argument to hash passwords for
your own entities:
<info>php %command.full_name% --no-interaction [password] 'App\Entity\User'</info>
Executing the command interactively allows you to generate a random salt for
hashing the password:
<info>php %command.full_name% [password] 'App\Entity\User'</info>
In case your hasher doesn't require a salt, add the <comment>empty-salt</comment> option:
<info>php %command.full_name% --empty-salt [password] 'App\Entity\User'</info>
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$errorIo = $output instanceof ConsoleOutputInterface ? new SymfonyStyle($input, $output->getErrorOutput()) : $io;
$input->isInteractive() ? $errorIo->title('Symfony Password Hash Utility') : $errorIo->newLine();
$password = $input->getArgument('password');
$userClass = $this->getUserClass($input, $io);
$emptySalt = $input->getOption('empty-salt');
$hasher = $this->hasherFactory->getPasswordHasher($userClass);
$saltlessWithoutEmptySalt = !$emptySalt && !$hasher instanceof LegacyPasswordHasherInterface;
if ($saltlessWithoutEmptySalt) {
$emptySalt = true;
}
if (!$password) {
if (!$input->isInteractive()) {
$errorIo->error('The password must not be empty.');
return 1;
}
$passwordQuestion = $this->createPasswordQuestion();
$password = $errorIo->askQuestion($passwordQuestion);
}
$salt = null;
if ($input->isInteractive() && !$emptySalt) {
$emptySalt = true;
$errorIo->note('The command will take care of generating a salt for you. Be aware that some hashers advise to let them generate their own salt. If you\'re using one of those hashers, please answer \'no\' to the question below. '.\PHP_EOL.'Provide the \'empty-salt\' option in order to let the hasher handle the generation itself.');
if ($errorIo->confirm('Confirm salt generation ?')) {
$salt = $this->generateSalt();
$emptySalt = false;
}
} elseif (!$emptySalt) {
$salt = $this->generateSalt();
}
$hashedPassword = $hasher->hash($password, $salt);
$rows = [
['Hasher used', \get_class($hasher)],
['Password hash', $hashedPassword],
];
if (!$emptySalt) {
$rows[] = ['Generated salt', $salt];
}
$io->table(['Key', 'Value'], $rows);
if (!$emptySalt) {
$errorIo->note(sprintf('Make sure that your salt storage field fits the salt length: %s chars', \strlen($salt)));
} elseif ($saltlessWithoutEmptySalt) {
$errorIo->note('Self-salting hasher used: the hasher generated its own built-in salt.');
}
$errorIo->success('Password hashing succeeded');
return 0;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('user-class')) {
$suggestions->suggestValues($this->userClasses);
return;
}
}
/**
* Create the password question to ask the user for the password to be hashed.
*/
private function createPasswordQuestion(): Question
{
$passwordQuestion = new Question('Type in your password to be hashed');
return $passwordQuestion->setValidator(function ($value) {
if ('' === trim($value)) {
throw new InvalidArgumentException('The password must not be empty.');
}
return $value;
})->setHidden(true)->setMaxAttempts(20);
}
private function generateSalt(): string
{
return base64_encode(random_bytes(30));
}
private function getUserClass(InputInterface $input, SymfonyStyle $io): string
{
if (null !== $userClass = $input->getArgument('user-class')) {
return $userClass;
}
if (!$this->userClasses) {
throw new RuntimeException('There are no configured password hashers for the "security" extension.');
}
if (!$input->isInteractive() || 1 === \count($this->userClasses)) {
return reset($this->userClasses);
}
$userClasses = $this->userClasses;
natcasesort($userClasses);
$userClasses = array_values($userClasses);
return $io->choice('For which user class would you like to hash a password?', $userClasses, reset($userClasses));
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Exception;
/**
* Interface for exceptions thrown by the password-hasher component.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Exception;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class InvalidPasswordException extends \RuntimeException implements ExceptionInterface
{
public function __construct(string $message = 'Invalid password.', int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Exception;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
trait CheckPasswordLengthTrait
{
private function isPasswordTooLong(string $password): bool
{
return PasswordHasherInterface::MAX_PASSWORD_LENGTH < \strlen($password);
}
}

View File

@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Exception\LogicException;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
/**
* MessageDigestPasswordHasher uses a message digest algorithm.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class MessageDigestPasswordHasher implements LegacyPasswordHasherInterface
{
use CheckPasswordLengthTrait;
private $algorithm;
private $encodeHashAsBase64;
private $iterations = 1;
private $hashLength = -1;
/**
* @param string $algorithm The digest algorithm to use
* @param bool $encodeHashAsBase64 Whether to base64 encode the password hash
* @param int $iterations The number of iterations to use to stretch the password hash
*/
public function __construct(string $algorithm = 'sha512', bool $encodeHashAsBase64 = true, int $iterations = 5000)
{
$this->algorithm = $algorithm;
$this->encodeHashAsBase64 = $encodeHashAsBase64;
try {
$this->hashLength = \strlen($this->hash('', 'salt'));
} catch (\LogicException $e) {
// ignore algorithm not supported
}
$this->iterations = $iterations;
}
public function hash(string $plainPassword, string $salt = null): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
if (!\in_array($this->algorithm, hash_algos(), true)) {
throw new LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm));
}
$salted = $this->mergePasswordAndSalt($plainPassword, $salt);
$digest = hash($this->algorithm, $salted, true);
// "stretch" hash
for ($i = 1; $i < $this->iterations; ++$i) {
$digest = hash($this->algorithm, $digest.$salted, true);
}
return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest);
}
public function verify(string $hashedPassword, string $plainPassword, string $salt = null): bool
{
if (\strlen($hashedPassword) !== $this->hashLength || false !== strpos($hashedPassword, '$')) {
return false;
}
return !$this->isPasswordTooLong($plainPassword) && hash_equals($hashedPassword, $this->hash($plainPassword, $salt));
}
public function needsRehash(string $hashedPassword): bool
{
return false;
}
private function mergePasswordAndSalt(string $password, ?string $salt): string
{
if (!$salt) {
return $password;
}
if (false !== strrpos($salt, '{') || false !== strrpos($salt, '}')) {
throw new \InvalidArgumentException('Cannot use { or } in salt.');
}
return $password.'{'.$salt.'}';
}
}

View File

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
/**
* Hashes passwords using the best available hasher.
* Verifies them using a chain of hashers.
*
* /!\ Don't put a PlaintextPasswordHasher in the list as that'd mean a leaked hash
* could be used to authenticate successfully without knowing the cleartext password.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class MigratingPasswordHasher implements PasswordHasherInterface
{
private $bestHasher;
private $extraHashers;
public function __construct(PasswordHasherInterface $bestHasher, PasswordHasherInterface ...$extraHashers)
{
$this->bestHasher = $bestHasher;
$this->extraHashers = $extraHashers;
}
public function hash(string $plainPassword, string $salt = null): string
{
return $this->bestHasher->hash($plainPassword, $salt);
}
public function verify(string $hashedPassword, string $plainPassword, string $salt = null): bool
{
if ($this->bestHasher->verify($hashedPassword, $plainPassword, $salt)) {
return true;
}
if (!$this->bestHasher->needsRehash($hashedPassword)) {
return false;
}
foreach ($this->extraHashers as $hasher) {
if ($hasher->verify($hashedPassword, $plainPassword, $salt)) {
return true;
}
}
return false;
}
public function needsRehash(string $hashedPassword): bool
{
return $this->bestHasher->needsRehash($hashedPassword);
}
}

View File

@ -0,0 +1,120 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
/**
* Hashes passwords using password_hash().
*
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
* @author Terje Bråten <terje@braten.be>
* @author Nicolas Grekas <p@tchwork.com>
*/
final class NativePasswordHasher implements PasswordHasherInterface
{
use CheckPasswordLengthTrait;
private $algorithm = \PASSWORD_BCRYPT;
private $options;
/**
* @param string|null $algorithm An algorithm supported by password_hash() or null to use the best available algorithm
*/
public function __construct(int $opsLimit = null, int $memLimit = null, int $cost = null, string $algorithm = null)
{
$cost = $cost ?? 13;
$opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4);
$memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024);
if (3 > $opsLimit) {
throw new \InvalidArgumentException('$opsLimit must be 3 or greater.');
}
if (10 * 1024 > $memLimit) {
throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
}
if ($cost < 4 || 31 < $cost) {
throw new \InvalidArgumentException('$cost must be in the range of 4-31.');
}
if (null !== $algorithm) {
$algorithms = [1 => \PASSWORD_BCRYPT, '2y' => \PASSWORD_BCRYPT];
if (\defined('PASSWORD_ARGON2I')) {
$algorithms[2] = $algorithms['argon2i'] = \PASSWORD_ARGON2I;
}
if (\defined('PASSWORD_ARGON2ID')) {
$algorithms[3] = $algorithms['argon2id'] = \PASSWORD_ARGON2ID;
}
$this->algorithm = $algorithms[$algorithm] ?? $algorithm;
}
$this->options = [
'cost' => $cost,
'time_cost' => $opsLimit,
'memory_cost' => $memLimit >> 10,
'threads' => 1,
];
}
public function hash(string $plainPassword): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
if (\PASSWORD_BCRYPT === $this->algorithm && (72 < \strlen($plainPassword) || false !== strpos($plainPassword, "\0"))) {
$plainPassword = base64_encode(hash('sha512', $plainPassword, true));
}
return password_hash($plainPassword, $this->algorithm, $this->options);
}
public function verify(string $hashedPassword, string $plainPassword): bool
{
if ('' === $plainPassword || $this->isPasswordTooLong($plainPassword)) {
return false;
}
if (0 !== strpos($hashedPassword, '$argon')) {
// Bcrypt cuts on NUL chars and after 72 bytes
if (0 === strpos($hashedPassword, '$2') && (72 < \strlen($plainPassword) || false !== strpos($plainPassword, "\0"))) {
$plainPassword = base64_encode(hash('sha512', $plainPassword, true));
}
return password_verify($plainPassword, $hashedPassword);
}
if (\extension_loaded('sodium') && version_compare(\SODIUM_LIBRARY_VERSION, '1.0.14', '>=')) {
return sodium_crypto_pwhash_str_verify($hashedPassword, $plainPassword);
}
if (\extension_loaded('libsodium') && version_compare(phpversion('libsodium'), '1.0.14', '>=')) {
return \Sodium\crypto_pwhash_str_verify($hashedPassword, $plainPassword);
}
return password_verify($plainPassword, $hashedPassword);
}
/**
* {@inheritdoc}
*/
public function needsRehash(string $hashedPassword): bool
{
return password_needs_rehash($hashedPassword, $this->algorithm, $this->options);
}
}

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
/**
* @author Christophe Coevoet <stof@notk.org>
*/
interface PasswordHasherAwareInterface
{
/**
* Gets the name of the password hasher used to hash the password.
*
* If the method returns null, the standard way to retrieve the password hasher
* will be used instead.
*/
public function getPasswordHasherName(): ?string;
}

View File

@ -0,0 +1,242 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\LogicException;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface;
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
use Symfony\Component\Security\Core\Encoder\PasswordHasherAdapter;
/**
* A generic hasher factory implementation.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class PasswordHasherFactory implements PasswordHasherFactoryInterface
{
private $passwordHashers;
/**
* @param array<string, PasswordHasherInterface|array> $passwordHashers
*/
public function __construct(array $passwordHashers)
{
$this->passwordHashers = $passwordHashers;
}
/**
* {@inheritdoc}
*/
public function getPasswordHasher($user): PasswordHasherInterface
{
$hasherKey = null;
if (($user instanceof PasswordHasherAwareInterface && null !== $hasherName = $user->getPasswordHasherName()) || ($user instanceof EncoderAwareInterface && null !== $hasherName = $user->getEncoderName())) {
if (!\array_key_exists($hasherName, $this->passwordHashers)) {
throw new \RuntimeException(sprintf('The password hasher "%s" was not configured.', $hasherName));
}
$hasherKey = $hasherName;
} else {
foreach ($this->passwordHashers as $class => $hasher) {
if ((\is_object($user) && $user instanceof $class) || (!\is_object($user) && (is_subclass_of($user, $class) || $user == $class))) {
$hasherKey = $class;
break;
}
}
}
if (null === $hasherKey) {
throw new \RuntimeException(sprintf('No password hasher has been configured for account "%s".', \is_object($user) ? get_debug_type($user) : $user));
}
return $this->createHasherUsingAdapter($hasherKey);
}
/**
* Creates the actual hasher instance.
*
* @throws \InvalidArgumentException
*/
private function createHasher(array $config, bool $isExtra = false): PasswordHasherInterface
{
if (isset($config['algorithm'])) {
$rawConfig = $config;
$config = $this->getHasherConfigFromAlgorithm($config);
}
if (!isset($config['class'])) {
throw new \InvalidArgumentException('"class" must be set in '.json_encode($config));
}
if (!isset($config['arguments'])) {
throw new \InvalidArgumentException('"arguments" must be set in '.json_encode($config));
}
$hasher = new $config['class'](...$config['arguments']);
if (!$hasher instanceof PasswordHasherInterface && $hasher instanceof PasswordEncoderInterface) {
$hasher = new PasswordHasherAdapter($hasher);
}
if ($isExtra || !\in_array($config['class'], [NativePasswordHasher::class, SodiumPasswordHasher::class], true)) {
return $hasher;
}
if ($rawConfig ?? null) {
$extrapasswordHashers = array_map(function (string $algo) use ($rawConfig): PasswordHasherInterface {
$rawConfig['algorithm'] = $algo;
return $this->createHasher($rawConfig);
}, ['pbkdf2', $rawConfig['hash_algorithm'] ?? 'sha512']);
} else {
$extrapasswordHashers = [new Pbkdf2PasswordHasher(), new MessageDigestPasswordHasher()];
}
return new MigratingPasswordHasher($hasher, ...$extrapasswordHashers);
}
private function createHasherUsingAdapter(string $hasherKey): PasswordHasherInterface
{
if (!$this->passwordHashers[$hasherKey] instanceof PasswordHasherInterface) {
$this->passwordHashers[$hasherKey] = $this->passwordHashers[$hasherKey] instanceof PasswordEncoderInterface
? new PasswordHasherAdapter($this->passwordHashers[$hasherKey])
: $this->createHasher($this->passwordHashers[$hasherKey])
;
}
return $this->passwordHashers[$hasherKey];
}
private function getHasherConfigFromAlgorithm(array $config): array
{
if ('auto' === $config['algorithm']) {
// "plaintext" is not listed as any leaked hashes could then be used to authenticate directly
if (SodiumPasswordHasher::isSupported()) {
$algorithms = ['native', 'sodium', 'pbkdf2'];
} else {
$algorithms = ['native', 'pbkdf2'];
}
if ($config['hash_algorithm'] ?? '') {
$algorithms[] = $config['hash_algorithm'];
}
$hasherChain = [];
foreach ($algorithms as $algorithm) {
$config['algorithm'] = $algorithm;
$hasherChain[] = $this->createHasher($config, true);
}
return [
'class' => MigratingPasswordHasher::class,
'arguments' => $hasherChain,
];
}
if ($frompasswordHashers = ($config['migrate_from'] ?? false)) {
unset($config['migrate_from']);
$hasherChain = [$this->createHasher($config, true)];
foreach ($frompasswordHashers as $name) {
if (isset($this->passwordHashers[$name])) {
$hasher = $this->createHasherUsingAdapter($name);
} else {
$hasher = $this->createHasher(['algorithm' => $name], true);
}
$hasherChain[] = $hasher;
}
return [
'class' => MigratingPasswordHasher::class,
'arguments' => $hasherChain,
];
}
switch ($config['algorithm']) {
case 'plaintext':
return [
'class' => PlaintextPasswordHasher::class,
'arguments' => [$config['ignore_case'] ?? false],
];
case 'pbkdf2':
return [
'class' => Pbkdf2PasswordHasher::class,
'arguments' => [
$config['hash_algorithm'] ?? 'sha512',
$config['encode_as_base64'] ?? true,
$config['iterations'] ?? 1000,
$config['key_length'] ?? 40,
],
];
case 'bcrypt':
$config['algorithm'] = 'native';
$config['native_algorithm'] = \PASSWORD_BCRYPT;
return $this->getHasherConfigFromAlgorithm($config);
case 'native':
return [
'class' => NativePasswordHasher::class,
'arguments' => [
$config['time_cost'] ?? null,
(($config['memory_cost'] ?? 0) << 10) ?: null,
$config['cost'] ?? null,
] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
];
case 'sodium':
return [
'class' => SodiumPasswordHasher::class,
'arguments' => [
$config['time_cost'] ?? null,
(($config['memory_cost'] ?? 0) << 10) ?: null,
],
];
case 'argon2i':
if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$config['algorithm'] = 'sodium';
} elseif (\defined('PASSWORD_ARGON2I')) {
$config['algorithm'] = 'native';
$config['native_algorithm'] = \PASSWORD_ARGON2I;
} else {
throw new LogicException(sprintf('Algorithm "argon2i" is not available. Use "%s" instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id" or "auto' : 'auto'));
}
return $this->getHasherConfigFromAlgorithm($config);
case 'argon2id':
if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
$config['algorithm'] = 'sodium';
} elseif (\defined('PASSWORD_ARGON2ID')) {
$config['algorithm'] = 'native';
$config['native_algorithm'] = \PASSWORD_ARGON2ID;
} else {
throw new LogicException(sprintf('Algorithm "argon2id" is not available. Either use "%s", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? 'argon2i", "auto' : 'auto'));
}
return $this->getHasherConfigFromAlgorithm($config);
}
return [
'class' => MessageDigestPasswordHasher::class,
'arguments' => [
$config['algorithm'],
$config['encode_as_base64'] ?? true,
$config['iterations'] ?? 5000,
],
];
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
/**
* PasswordHasherFactoryInterface to support different password hashers for different user accounts.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface PasswordHasherFactoryInterface
{
/**
* Returns the password hasher to use for the given user.
*
* @param PasswordHasherAwareInterface|PasswordAuthenticatedUserInterface|string $user
*
* @throws \RuntimeException When no password hasher could be found for the user
*/
public function getPasswordHasher($user): PasswordHasherInterface;
}

View File

@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Exception\LogicException;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
/**
* Pbkdf2PasswordHasher uses the PBKDF2 (Password-Based Key Derivation Function 2).
*
* Providing a high level of Cryptographic security,
* PBKDF2 is recommended by the National Institute of Standards and Technology (NIST).
*
* But also warrants a warning, using PBKDF2 (with a high number of iterations) slows down the process.
* PBKDF2 should be used with caution and care.
*
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
* @author Andrew Johnson
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Pbkdf2PasswordHasher implements LegacyPasswordHasherInterface
{
use CheckPasswordLengthTrait;
private $algorithm;
private $encodeHashAsBase64;
private $iterations = 1;
private $length;
private $encodedLength = -1;
/**
* @param string $algorithm The digest algorithm to use
* @param bool $encodeHashAsBase64 Whether to base64 encode the password hash
* @param int $iterations The number of iterations to use to stretch the password hash
* @param int $length Length of derived key to create
*/
public function __construct(string $algorithm = 'sha512', bool $encodeHashAsBase64 = true, int $iterations = 1000, int $length = 40)
{
$this->algorithm = $algorithm;
$this->encodeHashAsBase64 = $encodeHashAsBase64;
$this->length = $length;
try {
$this->encodedLength = \strlen($this->hash('', 'salt'));
} catch (\LogicException $e) {
// ignore unsupported algorithm
}
$this->iterations = $iterations;
}
public function hash(string $plainPassword, string $salt = null): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
if (!\in_array($this->algorithm, hash_algos(), true)) {
throw new LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm));
}
$digest = hash_pbkdf2($this->algorithm, $plainPassword, $salt, $this->iterations, $this->length, true);
return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest);
}
public function verify(string $hashedPassword, string $plainPassword, string $salt = null): bool
{
if (\strlen($hashedPassword) !== $this->encodedLength || false !== strpos($hashedPassword, '$')) {
return false;
}
return !$this->isPasswordTooLong($plainPassword) && hash_equals($hashedPassword, $this->hash($plainPassword, $salt));
}
public function needsRehash(string $hashedPassword): bool
{
return false;
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\LegacyPasswordHasherInterface;
/**
* PlaintextPasswordHasher does not do any hashing but is useful in testing environments.
*
* As this hasher is not cryptographically secure, usage of it in production environments is discouraged.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class PlaintextPasswordHasher implements LegacyPasswordHasherInterface
{
use CheckPasswordLengthTrait;
private $ignorePasswordCase;
/**
* @param bool $ignorePasswordCase Compare password case-insensitive
*/
public function __construct(bool $ignorePasswordCase = false)
{
$this->ignorePasswordCase = $ignorePasswordCase;
}
/**
* {@inheritdoc}
*/
public function hash(string $plainPassword, string $salt = null): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
return $this->mergePasswordAndSalt($plainPassword, $salt);
}
public function verify(string $hashedPassword, string $plainPassword, string $salt = null): bool
{
if ($this->isPasswordTooLong($plainPassword)) {
return false;
}
$pass2 = $this->mergePasswordAndSalt($plainPassword, $salt);
if (!$this->ignorePasswordCase) {
return hash_equals($hashedPassword, $pass2);
}
return hash_equals(strtolower($hashedPassword), strtolower($pass2));
}
public function needsRehash(string $hashedPassword): bool
{
return false;
}
private function mergePasswordAndSalt(string $password, ?string $salt): string
{
if (empty($salt)) {
return $password;
}
if (false !== strrpos($salt, '{') || false !== strrpos($salt, '}')) {
throw new \InvalidArgumentException('Cannot use { or } in salt.');
}
return $password.'{'.$salt.'}';
}
}

View File

@ -0,0 +1,114 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Exception\LogicException;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
/**
* Hashes passwords using libsodium.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Zan Baldwin <hello@zanbaldwin.com>
* @author Dominik Müller <dominik.mueller@jkweb.ch>
*/
final class SodiumPasswordHasher implements PasswordHasherInterface
{
use CheckPasswordLengthTrait;
private $opsLimit;
private $memLimit;
public function __construct(int $opsLimit = null, int $memLimit = null)
{
if (!self::isSupported()) {
throw new LogicException('Libsodium is not available. You should either install the sodium extension or use a different password hasher.');
}
$this->opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4);
$this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024);
if (3 > $this->opsLimit) {
throw new \InvalidArgumentException('$opsLimit must be 3 or greater.');
}
if (10 * 1024 > $this->memLimit) {
throw new \InvalidArgumentException('$memLimit must be 10k or greater.');
}
}
public static function isSupported(): bool
{
return version_compare(\extension_loaded('sodium') ? \SODIUM_LIBRARY_VERSION : phpversion('libsodium'), '1.0.14', '>=');
}
public function hash(string $plainPassword): string
{
if ($this->isPasswordTooLong($plainPassword)) {
throw new InvalidPasswordException();
}
if (\function_exists('sodium_crypto_pwhash_str')) {
return sodium_crypto_pwhash_str($plainPassword, $this->opsLimit, $this->memLimit);
}
if (\extension_loaded('libsodium')) {
return \Sodium\crypto_pwhash_str($plainPassword, $this->opsLimit, $this->memLimit);
}
throw new LogicException('Libsodium is not available. You should either install the sodium extension or use a different password hasher.');
}
public function verify(string $hashedPassword, string $plainPassword): bool
{
if ('' === $plainPassword) {
return false;
}
if ($this->isPasswordTooLong($plainPassword)) {
return false;
}
if (0 !== strpos($hashedPassword, '$argon')) {
if (0 === strpos($hashedPassword, '$2') && (72 < \strlen($plainPassword) || false !== strpos($plainPassword, "\0"))) {
$plainPassword = base64_encode(hash('sha512', $plainPassword, true));
}
// Accept validating non-argon passwords for seamless migrations
return password_verify($plainPassword, $hashedPassword);
}
if (\function_exists('sodium_crypto_pwhash_str_verify')) {
return sodium_crypto_pwhash_str_verify($hashedPassword, $plainPassword);
}
if (\extension_loaded('libsodium')) {
return \Sodium\crypto_pwhash_str_verify($hashedPassword, $plainPassword);
}
return false;
}
public function needsRehash(string $hashedPassword): bool
{
if (\function_exists('sodium_crypto_pwhash_str_needs_rehash')) {
return sodium_crypto_pwhash_str_needs_rehash($hashedPassword, $this->opsLimit, $this->memLimit);
}
if (\extension_loaded('libsodium')) {
return \Sodium\crypto_pwhash_str_needs_rehash($hashedPassword, $this->opsLimit, $this->memLimit);
}
throw new LogicException('Libsodium is not available. You should either install the sodium extension or use a different password hasher.');
}
}

View File

@ -0,0 +1,116 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Hashes passwords based on the user and the PasswordHasherFactory.
*
* @author Ariel Ferrandini <arielferrandini@gmail.com>
*
* @final
*/
class UserPasswordHasher implements UserPasswordHasherInterface
{
private $hasherFactory;
public function __construct(PasswordHasherFactoryInterface $hasherFactory)
{
$this->hasherFactory = $hasherFactory;
}
/**
* @param PasswordAuthenticatedUserInterface $user
*/
public function hashPassword($user, string $plainPassword): string
{
if (!$user instanceof PasswordAuthenticatedUserInterface) {
if (!$user instanceof UserInterface) {
throw new \TypeError(sprintf('Expected an instance of "%s" as first argument, but got "%s".', UserInterface::class, get_debug_type($user)));
}
trigger_deprecation('symfony/password-hasher', '5.3', 'The "%s()" method expects a "%s" instance as first argument. Not implementing it in class "%s" is deprecated.', __METHOD__, PasswordAuthenticatedUserInterface::class, get_debug_type($user));
}
$salt = null;
if ($user instanceof LegacyPasswordAuthenticatedUserInterface) {
$salt = $user->getSalt();
} elseif ($user instanceof UserInterface) {
$salt = method_exists($user, 'getSalt') ? $user->getSalt() : null;
if ($salt) {
trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user));
}
}
$hasher = $this->hasherFactory->getPasswordHasher($user);
return $hasher->hash($plainPassword, $salt);
}
/**
* @param PasswordAuthenticatedUserInterface $user
*/
public function isPasswordValid($user, string $plainPassword): bool
{
if (!$user instanceof PasswordAuthenticatedUserInterface) {
if (!$user instanceof UserInterface) {
throw new \TypeError(sprintf('Expected an instance of "%s" as first argument, but got "%s".', UserInterface::class, get_debug_type($user)));
}
trigger_deprecation('symfony/password-hasher', '5.3', 'The "%s()" method expects a "%s" instance as first argument. Not implementing it in class "%s" is deprecated.', __METHOD__, PasswordAuthenticatedUserInterface::class, get_debug_type($user));
}
$salt = null;
if ($user instanceof LegacyPasswordAuthenticatedUserInterface) {
$salt = $user->getSalt();
} elseif ($user instanceof UserInterface) {
$salt = $user->getSalt();
if (null !== $salt) {
trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user));
}
}
if (null === $user->getPassword()) {
return false;
}
$hasher = $this->hasherFactory->getPasswordHasher($user);
return $hasher->verify($user->getPassword(), $plainPassword, $salt);
}
/**
* @param PasswordAuthenticatedUserInterface $user
*/
public function needsRehash($user): bool
{
if (null === $user->getPassword()) {
return false;
}
if (!$user instanceof PasswordAuthenticatedUserInterface) {
if (!$user instanceof UserInterface) {
throw new \TypeError(sprintf('Expected an instance of "%s" as first argument, but got "%s".', UserInterface::class, get_debug_type($user)));
}
trigger_deprecation('symfony/password-hasher', '5.3', 'The "%s()" method expects a "%s" instance as first argument. Not implementing it in class "%s" is deprecated.', __METHOD__, PasswordAuthenticatedUserInterface::class, get_debug_type($user));
}
$hasher = $this->hasherFactory->getPasswordHasher($user);
return $hasher->needsRehash($user->getPassword());
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher\Hasher;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
/**
* Interface for the user password hasher service.
*
* @author Ariel Ferrandini <arielferrandini@gmail.com>
*
* @method string hashPassword(PasswordAuthenticatedUserInterface $user, string $plainPassword) Hashes the plain password for the given user.
* @method bool isPasswordValid(PasswordAuthenticatedUserInterface $user, string $plainPassword) Checks if the plaintext password matches the user's password.
* @method bool needsRehash(PasswordAuthenticatedUserInterface $user) Checks if an encoded password would benefit from rehashing.
*/
interface UserPasswordHasherInterface
{
}

19
vendor/symfony/password-hasher/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2022 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
/**
* Provides password hashing and verification capabilities for "legacy" hashers that require external salts.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Nicolas Grekas <p@tchwork.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface LegacyPasswordHasherInterface extends PasswordHasherInterface
{
/**
* Hashes a plain password.
*
* @throws InvalidPasswordException If the plain password is invalid, e.g. excessively long
*/
public function hash(string $plainPassword, string $salt = null): string;
/**
* Checks that a plain password and a salt match a password hash.
*/
public function verify(string $hashedPassword, string $plainPassword, string $salt = null): bool;
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PasswordHasher;
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
/**
* Provides password hashing capabilities.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Fabien Potencier <fabien@symfony.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
interface PasswordHasherInterface
{
public const MAX_PASSWORD_LENGTH = 4096;
/**
* Hashes a plain password.
*
* @throws InvalidPasswordException When the plain password is invalid, e.g. excessively long
*/
public function hash(string $plainPassword): string;
/**
* Verifies a plain password against a hash.
*/
public function verify(string $hashedPassword, string $plainPassword): bool;
/**
* Checks if a password hash would benefit from rehashing.
*/
public function needsRehash(string $hashedPassword): bool;
}

View File

@ -0,0 +1,40 @@
PasswordHasher Component
========================
The PasswordHasher component provides secure password hashing utilities.
Getting Started
---------------
```
$ composer require symfony/password-hasher
```
```php
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
// Configure different password hashers via the factory
$factory = new PasswordHasherFactory([
'common' => ['algorithm' => 'bcrypt'],
'memory-hard' => ['algorithm' => 'sodium'],
]);
// Retrieve the right password hasher by its name
$passwordHasher = $factory->getPasswordHasher('common');
// Hash a plain password
$hash = $passwordHasher->hash('plain'); // returns a bcrypt hash
// Verify that a given plain password matches the hash
$passwordHasher->verify($hash, 'wrong'); // returns false
$passwordHasher->verify($hash, 'plain'); // returns true (valid)
```
Resources
---------
* [Documentation](https://symfony.com/doc/current/security.html#c-hashing-passwords)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@ -0,0 +1,36 @@
{
"name": "symfony/password-hasher",
"type": "library",
"description": "Provides password hashing utilities",
"keywords": ["password", "hashing"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Robin Chalas",
"email": "robin.chalas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"symfony/security-core": "^5.3|^6.0",
"symfony/console": "^5.3|^6.0"
},
"conflict": {
"symfony/security-core": "<5.3"
},
"autoload": {
"psr-4": { "Symfony\\Component\\PasswordHasher\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}