D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
usr
/
local
/
psa
/
admin
/
plib
/
vendor
/
laminas
/
laminas-validator
/
src
/
Filename :
UndisclosedPassword.php
back
Copy
<?php namespace Laminas\Validator; use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use SensitiveParameter; use function array_filter; use function explode; use function is_string; use function sha1; use function strcmp; use function strtoupper; use function substr; final class UndisclosedPassword extends AbstractValidator { // phpcs:disable SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedConstant private const HIBP_API_URI = 'https://api.pwnedpasswords.com'; private const HIBP_API_REQUEST_TIMEOUT = 300; private const HIBP_CLIENT_USER_AGENT_STRING = 'laminas-validator'; private const HIBP_CLIENT_ACCEPT_HEADER = 'application/vnd.haveibeenpwned.v2+json'; private const HIBP_K_ANONYMITY_HASH_RANGE_LENGTH = 5; private const HIBP_K_ANONYMITY_HASH_RANGE_BASE = 0; private const SHA1_STRING_LENGTH = 40; // phpcs:enable private const PASSWORD_BREACHED = 'passwordBreached'; private const NOT_A_STRING = 'wrongInput'; // phpcs:disable Generic.Files.LineLength.TooLong /** @var array<string, string> */ 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 !== []; } }