NotPwnedVerifier.php 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. <?php
  2. namespace Illuminate\Validation;
  3. use Exception;
  4. use Illuminate\Contracts\Validation\UncompromisedVerifier;
  5. use Illuminate\Support\Stringable;
  6. class NotPwnedVerifier implements UncompromisedVerifier
  7. {
  8. /**
  9. * The HTTP factory instance.
  10. *
  11. * @var \Illuminate\Http\Client\Factory
  12. */
  13. protected $factory;
  14. /**
  15. * The number of seconds the request can run before timing out.
  16. *
  17. * @var int
  18. */
  19. protected $timeout;
  20. /**
  21. * Create a new uncompromised verifier.
  22. *
  23. * @param \Illuminate\Http\Client\Factory $factory
  24. * @param int|null $timeout
  25. */
  26. public function __construct($factory, $timeout = null)
  27. {
  28. $this->factory = $factory;
  29. $this->timeout = $timeout ?? 30;
  30. }
  31. /**
  32. * Verify that the given data has not been compromised in public breaches.
  33. *
  34. * @param array $data
  35. * @return bool
  36. */
  37. public function verify($data)
  38. {
  39. $value = $data['value'];
  40. $threshold = $data['threshold'];
  41. if (empty($value = (string) $value)) {
  42. return false;
  43. }
  44. [$hash, $hashPrefix] = $this->getHash($value);
  45. return ! $this->search($hashPrefix)
  46. ->contains(function ($line) use ($hash, $hashPrefix, $threshold) {
  47. [$hashSuffix, $count] = explode(':', $line);
  48. return $hashPrefix.$hashSuffix == $hash && $count > $threshold;
  49. });
  50. }
  51. /**
  52. * Get the hash and its first 5 chars.
  53. *
  54. * @param string $value
  55. * @return array
  56. */
  57. protected function getHash($value)
  58. {
  59. $hash = strtoupper(sha1((string) $value));
  60. $hashPrefix = substr($hash, 0, 5);
  61. return [$hash, $hashPrefix];
  62. }
  63. /**
  64. * Search by the given hash prefix and returns all occurrences of leaked passwords.
  65. *
  66. * @param string $hashPrefix
  67. * @return \Illuminate\Support\Collection
  68. */
  69. protected function search($hashPrefix)
  70. {
  71. try {
  72. $response = $this->factory->withHeaders([
  73. 'Add-Padding' => true,
  74. ])->timeout($this->timeout)->get(
  75. 'https://api.pwnedpasswords.com/range/'.$hashPrefix
  76. );
  77. } catch (Exception $e) {
  78. report($e);
  79. }
  80. $body = (isset($response) && $response->successful())
  81. ? $response->body()
  82. : '';
  83. return (new Stringable($body))->trim()->explode("\n")->filter(function ($line) {
  84. return str_contains($line, ':');
  85. });
  86. }
  87. }