*
* 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\User\PasswordAuthenticatedUserInterface;
/**
* A generic hasher factory implementation.
*
* @author Nicolas Grekas
* @author Robin Chalas
*/
class PasswordHasherFactory implements PasswordHasherFactoryInterface
{
/**
* @param array $passwordHashers
*/
public function __construct(
private array $passwordHashers,
) {
}
public function getPasswordHasher(string|PasswordAuthenticatedUserInterface|PasswordHasherAwareInterface $user): PasswordHasherInterface
{
$hasherKey = null;
if ($user instanceof PasswordHasherAwareInterface && null !== $hasherName = $user->getPasswordHasherName()) {
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));
}
if (!$this->passwordHashers[$hasherKey] instanceof PasswordHasherInterface) {
$this->passwordHashers[$hasherKey] = $this->createHasher($this->passwordHashers[$hasherKey]);
}
return $this->passwordHashers[$hasherKey];
}
/**
* Creates the actual hasher instance.
*
* @throws \InvalidArgumentException
*/
private function createHasher(array $config, bool $isExtra = false): PasswordHasherInterface
{
if (isset($config['instance'])) {
if (!isset($config['migrate_from'])) {
return $config['instance'];
}
$config = $this->getMigratingPasswordConfig($config);
}
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 ($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 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 ($config['migrate_from'] ?? false) {
return $this->getMigratingPasswordConfig($config);
}
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; use "%s" or 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,
],
];
}
private function getMigratingPasswordConfig(array $config): array
{
$frompasswordHashers = $config['migrate_from'];
unset($config['migrate_from']);
$hasherChain = [$this->createHasher($config, true)];
foreach ($frompasswordHashers as $name) {
if ($hasher = $this->passwordHashers[$name] ?? false) {
$hasher = $hasher instanceof PasswordHasherInterface ? $hasher : $this->createHasher($hasher, true);
} else {
$hasher = $this->createHasher(['algorithm' => $name], true);
}
$hasherChain[] = $hasher;
}
return [
'class' => MigratingPasswordHasher::class,
'arguments' => $hasherChain,
];
}
}