Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
50.00% covered (danger)
50.00%
2 / 4
CRAP
74.19% covered (warning)
74.19%
69 / 93
EncoderFactory
0.00% covered (danger)
0.00%
0 / 1
50.00% covered (danger)
50.00%
2 / 4
87.60
74.19% covered (warning)
74.19%
69 / 93
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getEncoder
100.00% covered (success)
100.00%
1 / 1
13
100.00% covered (success)
100.00%
14 / 14
 createEncoder
0.00% covered (danger)
0.00%
0 / 1
7.10
87.50% covered (warning)
87.50%
14 / 16
 getEncoderConfigFromAlgorithm
0.00% covered (danger)
0.00%
0 / 1
61.20
63.93% covered (warning)
63.93%
39 / 61
<?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\Security\Core\Encoder;
use Symfony\Component\Security\Core\Exception\LogicException;
/**
 * A generic encoder factory implementation.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class EncoderFactory implements EncoderFactoryInterface
{
    private $encoders;
    public function __construct(array $encoders)
    {
        $this->encoders = $encoders;
    }
    /**
     * {@inheritdoc}
     */
    public function getEncoder($user)
    {
        $encoderKey = null;
        if ($user instanceof EncoderAwareInterface && (null !== $encoderName = $user->getEncoderName())) {
            if (!\array_key_exists($encoderName, $this->encoders)) {
                throw new \RuntimeException(sprintf('The encoder "%s" was not configured.', $encoderName));
            }
            $encoderKey = $encoderName;
        } else {
            foreach ($this->encoders as $class => $encoder) {
                if ((\is_object($user) && $user instanceof $class) || (!\is_object($user) && (is_subclass_of($user, $class) || $user == $class))) {
                    $encoderKey = $class;
                    break;
                }
            }
        }
        if (null === $encoderKey) {
            throw new \RuntimeException(sprintf('No encoder has been configured for account "%s".', \is_object($user) ? get_debug_type($user) : $user));
        }
        if (!$this->encoders[$encoderKey] instanceof PasswordEncoderInterface) {
            $this->encoders[$encoderKey] = $this->createEncoder($this->encoders[$encoderKey]);
        }
        return $this->encoders[$encoderKey];
    }
    /**
     * Creates the actual encoder instance.
     *
     * @throws \InvalidArgumentException
     */
    private function createEncoder(array $config, bool $isExtra = false): PasswordEncoderInterface
    {
        if (isset($config['algorithm'])) {
            $rawConfig = $config;
            $config = $this->getEncoderConfigFromAlgorithm($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));
        }
        $encoder = new $config['class'](...$config['arguments']);
        if ($isExtra || !\in_array($config['class'], [NativePasswordEncoder::class, SodiumPasswordEncoder::class], true)) {
            return $encoder;
        }
        if ($rawConfig ?? null) {
            $extraEncoders = array_map(function (string $algo) use ($rawConfig): PasswordEncoderInterface {
                $rawConfig['algorithm'] = $algo;
                return $this->createEncoder($rawConfig);
            }, ['pbkdf2', $rawConfig['hash_algorithm'] ?? 'sha512']);
        } else {
            $extraEncoders = [new Pbkdf2PasswordEncoder(), new MessageDigestPasswordEncoder()];
        }
        return new MigratingPasswordEncoder($encoder, ...$extraEncoders);
    }
    private function getEncoderConfigFromAlgorithm(array $config): array
    {
        if ('auto' === $config['algorithm']) {
            $encoderChain = [];
            // "plaintext" is not listed as any leaked hashes could then be used to authenticate directly
            foreach ([SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native', 'pbkdf2', $config['hash_algorithm']] as $algo) {
                $config['algorithm'] = $algo;
                $encoderChain[] = $this->createEncoder($config, true);
            }
            return [
                'class' => MigratingPasswordEncoder::class,
                'arguments' => $encoderChain,
            ];
        }
        if ($fromEncoders = ($config['migrate_from'] ?? false)) {
            unset($config['migrate_from']);
            $encoderChain = [$this->createEncoder($config, true)];
            foreach ($fromEncoders as $name) {
                if ($encoder = $this->encoders[$name] ?? false) {
                    $encoder = $encoder instanceof PasswordEncoderInterface ? $encoder : $this->createEncoder($encoder, true);
                } else {
                    $encoder = $this->createEncoder(['algorithm' => $name], true);
                }
                $encoderChain[] = $encoder;
            }
            return [
                'class' => MigratingPasswordEncoder::class,
                'arguments' => $encoderChain,
            ];
        }
        switch ($config['algorithm']) {
            case 'plaintext':
                return [
                    'class' => PlaintextPasswordEncoder::class,
                    'arguments' => [$config['ignore_case']],
                ];
            case 'pbkdf2':
                return [
                    'class' => Pbkdf2PasswordEncoder::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->getEncoderConfigFromAlgorithm($config);
            case 'native':
                return [
                    'class' => NativePasswordEncoder::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' => SodiumPasswordEncoder::class,
                    'arguments' => [
                        $config['time_cost'] ?? null,
                        (($config['memory_cost'] ?? 0) << 10) ?: null,
                    ],
                ];
            case 'argon2i':
                if (SodiumPasswordEncoder::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. Either use %s"auto" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? '"argon2id", ' : ''));
                }
                return $this->getEncoderConfigFromAlgorithm($config);
            case 'argon2id':
                if (($hasSodium = SodiumPasswordEncoder::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"auto", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? '"argon2i", ' : ''));
                }
                return $this->getEncoderConfigFromAlgorithm($config);
        }
        return [
            'class' => MessageDigestPasswordEncoder::class,
            'arguments' => [
                $config['algorithm'],
                $config['encode_as_base64'] ?? true,
                $config['iterations'] ?? 5000,
            ],
        ];
    }
}