*/ protected $messageTemplates = [ self::PASSWORD_BREACHED => 'The provided password was found in previous breaches, please create another password', self::NOT_A_STRING => 'The provided password is not a string, please provide a correct password', ]; // phpcs:enable public function __construct(private ClientInterface $httpClient, private RequestFactoryInterface $makeHttpRequest) { parent::__construct(); } // The following rule is buggy for parameters attributes // phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing.NoSpaceBetweenTypeHintAndParameter /** {@inheritDoc} */ public function isValid( #[SensitiveParameter] $value ): bool { if (! is_string($value)) { $this->error(self::NOT_A_STRING); return false; } if ($this->isPwnedPassword($value)) { $this->error(self::PASSWORD_BREACHED); return false; } return true; } // phpcs:enable SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing.NoSpaceBetweenTypeHintAndParameter private function isPwnedPassword( #[SensitiveParameter] string $password ): bool { $sha1Hash = $this->hashPassword($password); $rangeHash = $this->getRangeHash($sha1Hash); $hashList = $this->retrieveHashList($rangeHash); return $this->hashInResponse($sha1Hash, $hashList); } /** * We use a SHA1 hashed password for checking it against * the breached data set of HIBP. */ private function hashPassword( #[SensitiveParameter] string $password ): string { $hashedPassword = sha1($password); return strtoupper($hashedPassword); } /** * Creates a hash range that will be send to HIBP API * applying K-Anonymity * * @see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-by-exclusively-supporting-anonymity/ */ private function getRangeHash( #[SensitiveParameter] string $passwordHash ): string { return substr($passwordHash, self::HIBP_K_ANONYMITY_HASH_RANGE_BASE, self::HIBP_K_ANONYMITY_HASH_RANGE_LENGTH); } /** * Making a connection to the HIBP API to retrieve a * list of hashes that all have the same range as we * provided. * * @throws ClientExceptionInterface */ private function retrieveHashList( #[SensitiveParameter] string $passwordRange ): string { $request = $this->makeHttpRequest->createRequest( 'GET', self::HIBP_API_URI . '/range/' . $passwordRange ); $response = $this->httpClient->sendRequest($request); return (string) $response->getBody(); } /** * Checks if the password is in the response from HIBP */ private function hashInResponse( #[SensitiveParameter] string $sha1Hash, #[SensitiveParameter] string $resultStream ): bool { $data = explode("\r\n", $resultStream); $hashes = array_filter($data, static function ($value) use ($sha1Hash): bool { [$hash] = explode(':', $value); return strcmp($hash, substr($sha1Hash, self::HIBP_K_ANONYMITY_HASH_RANGE_LENGTH)) === 0; }); return $hashes !== []; } }