EncoderFactory.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Security\Core\Encoder;
  11. use Symfony\Component\Security\Core\Exception\LogicException;
  12. /**
  13. * A generic encoder factory implementation.
  14. *
  15. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  16. */
  17. class EncoderFactory implements EncoderFactoryInterface
  18. {
  19. private $encoders;
  20. public function __construct(array $encoders)
  21. {
  22. $this->encoders = $encoders;
  23. }
  24. /**
  25. * {@inheritdoc}
  26. */
  27. public function getEncoder($user)
  28. {
  29. $encoderKey = null;
  30. if ($user instanceof EncoderAwareInterface && (null !== $encoderName = $user->getEncoderName())) {
  31. if (!\array_key_exists($encoderName, $this->encoders)) {
  32. throw new \RuntimeException(sprintf('The encoder "%s" was not configured.', $encoderName));
  33. }
  34. $encoderKey = $encoderName;
  35. } else {
  36. foreach ($this->encoders as $class => $encoder) {
  37. if ((\is_object($user) && $user instanceof $class) || (!\is_object($user) && (is_subclass_of($user, $class) || $user == $class))) {
  38. $encoderKey = $class;
  39. break;
  40. }
  41. }
  42. }
  43. if (null === $encoderKey) {
  44. throw new \RuntimeException(sprintf('No encoder has been configured for account "%s".', \is_object($user) ? get_debug_type($user) : $user));
  45. }
  46. if (!$this->encoders[$encoderKey] instanceof PasswordEncoderInterface) {
  47. $this->encoders[$encoderKey] = $this->createEncoder($this->encoders[$encoderKey]);
  48. }
  49. return $this->encoders[$encoderKey];
  50. }
  51. /**
  52. * Creates the actual encoder instance.
  53. *
  54. * @throws \InvalidArgumentException
  55. */
  56. private function createEncoder(array $config, bool $isExtra = false): PasswordEncoderInterface
  57. {
  58. if (isset($config['algorithm'])) {
  59. $rawConfig = $config;
  60. $config = $this->getEncoderConfigFromAlgorithm($config);
  61. }
  62. if (!isset($config['class'])) {
  63. throw new \InvalidArgumentException('"class" must be set in '.json_encode($config));
  64. }
  65. if (!isset($config['arguments'])) {
  66. throw new \InvalidArgumentException('"arguments" must be set in '.json_encode($config));
  67. }
  68. $encoder = new $config['class'](...$config['arguments']);
  69. if ($isExtra || !\in_array($config['class'], [NativePasswordEncoder::class, SodiumPasswordEncoder::class], true)) {
  70. return $encoder;
  71. }
  72. if ($rawConfig ?? null) {
  73. $extraEncoders = array_map(function (string $algo) use ($rawConfig): PasswordEncoderInterface {
  74. $rawConfig['algorithm'] = $algo;
  75. return $this->createEncoder($rawConfig);
  76. }, ['pbkdf2', $rawConfig['hash_algorithm'] ?? 'sha512']);
  77. } else {
  78. $extraEncoders = [new Pbkdf2PasswordEncoder(), new MessageDigestPasswordEncoder()];
  79. }
  80. return new MigratingPasswordEncoder($encoder, ...$extraEncoders);
  81. }
  82. private function getEncoderConfigFromAlgorithm(array $config): array
  83. {
  84. if ('auto' === $config['algorithm']) {
  85. $encoderChain = [];
  86. // "plaintext" is not listed as any leaked hashes could then be used to authenticate directly
  87. foreach ([SodiumPasswordEncoder::isSupported() ? 'sodium' : 'native', 'pbkdf2', $config['hash_algorithm']] as $algo) {
  88. $config['algorithm'] = $algo;
  89. $encoderChain[] = $this->createEncoder($config, true);
  90. }
  91. return [
  92. 'class' => MigratingPasswordEncoder::class,
  93. 'arguments' => $encoderChain,
  94. ];
  95. }
  96. if ($fromEncoders = ($config['migrate_from'] ?? false)) {
  97. unset($config['migrate_from']);
  98. $encoderChain = [$this->createEncoder($config, true)];
  99. foreach ($fromEncoders as $name) {
  100. if ($encoder = $this->encoders[$name] ?? false) {
  101. $encoder = $encoder instanceof PasswordEncoderInterface ? $encoder : $this->createEncoder($encoder, true);
  102. } else {
  103. $encoder = $this->createEncoder(['algorithm' => $name], true);
  104. }
  105. $encoderChain[] = $encoder;
  106. }
  107. return [
  108. 'class' => MigratingPasswordEncoder::class,
  109. 'arguments' => $encoderChain,
  110. ];
  111. }
  112. switch ($config['algorithm']) {
  113. case 'plaintext':
  114. return [
  115. 'class' => PlaintextPasswordEncoder::class,
  116. 'arguments' => [$config['ignore_case']],
  117. ];
  118. case 'pbkdf2':
  119. return [
  120. 'class' => Pbkdf2PasswordEncoder::class,
  121. 'arguments' => [
  122. $config['hash_algorithm'] ?? 'sha512',
  123. $config['encode_as_base64'] ?? true,
  124. $config['iterations'] ?? 1000,
  125. $config['key_length'] ?? 40,
  126. ],
  127. ];
  128. case 'bcrypt':
  129. $config['algorithm'] = 'native';
  130. $config['native_algorithm'] = \PASSWORD_BCRYPT;
  131. return $this->getEncoderConfigFromAlgorithm($config);
  132. case 'native':
  133. return [
  134. 'class' => NativePasswordEncoder::class,
  135. 'arguments' => [
  136. $config['time_cost'] ?? null,
  137. (($config['memory_cost'] ?? 0) << 10) ?: null,
  138. $config['cost'] ?? null,
  139. ] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []),
  140. ];
  141. case 'sodium':
  142. return [
  143. 'class' => SodiumPasswordEncoder::class,
  144. 'arguments' => [
  145. $config['time_cost'] ?? null,
  146. (($config['memory_cost'] ?? 0) << 10) ?: null,
  147. ],
  148. ];
  149. case 'argon2i':
  150. if (SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
  151. $config['algorithm'] = 'sodium';
  152. } elseif (\defined('PASSWORD_ARGON2I')) {
  153. $config['algorithm'] = 'native';
  154. $config['native_algorithm'] = \PASSWORD_ARGON2I;
  155. } else {
  156. 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", ' : ''));
  157. }
  158. return $this->getEncoderConfigFromAlgorithm($config);
  159. case 'argon2id':
  160. if (($hasSodium = SodiumPasswordEncoder::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) {
  161. $config['algorithm'] = 'sodium';
  162. } elseif (\defined('PASSWORD_ARGON2ID')) {
  163. $config['algorithm'] = 'native';
  164. $config['native_algorithm'] = \PASSWORD_ARGON2ID;
  165. } else {
  166. 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", ' : ''));
  167. }
  168. return $this->getEncoderConfigFromAlgorithm($config);
  169. }
  170. return [
  171. 'class' => MessageDigestPasswordEncoder::class,
  172. 'arguments' => [
  173. $config['algorithm'],
  174. $config['encode_as_base64'] ?? true,
  175. $config['iterations'] ?? 5000,
  176. ],
  177. ];
  178. }
  179. }