├── .coveralls.yml ├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets └── images │ └── password-exposed.png ├── bundles └── .gitkeep ├── composer.json ├── phpunit.xml ├── psalm.xml ├── src ├── AbstractPasswordExposedChecker.php ├── Enums │ └── PasswordStatus.php ├── Interfaces │ └── PasswordExposedCheckerInterface.php ├── PasswordExposedChecker.php └── PasswordExposedFunction.php └── tests └── Unit ├── CustomInjectionsTest.php ├── PasswordExposedByHashTest.php └── PasswordExposedTest.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: tests/Logs/clover.xml 2 | json_path: tests/Logs/coveralls-upload.json 3 | service_name: travis-ci -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [*.yml] 13 | indent_size = 2 14 | 15 | # Ignore paths 16 | [/{vendor, cache, bundles, assets}/**] 17 | charset = none 18 | end_of_line = none 19 | insert_final_newline = none 20 | trim_trailing_whitespace = none 21 | indent_style = none 22 | indent_size = none 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor/ 3 | /cache/ 4 | /tests/Logs 5 | bundles/* 6 | !bundles/. 7 | 8 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.1.33' 4 | - '7.2' 5 | - '7.3' 6 | - '7.4' 7 | - '8.0' 8 | - nightly 9 | matrix: 10 | allow_failures: 11 | - php: nightly 12 | install: 13 | - composer install 14 | script: 15 | - ./vendor/bin/phpunit --coverage-clover ./tests/Logs/clover.xml 16 | - ./vendor/bin/psalm 17 | after_script: 18 | - php vendor/bin/php-coveralls -v 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔒 Password Exposed Helper Function 2 | 3 | This PHP package provides a `password_exposed` helper function, that uses the haveibeenpwned.com API to check if a password has been exposed in a data breach. 4 | 5 |

6 | 7 |

8 | 9 |

10 | 11 | Build Status 12 | 13 | 14 | StyleCI 15 | 16 | 17 | 18 | 19 |

20 | 21 | ## Installation 22 | 23 | The `password_exposed` package can be easily installed using Composer. Just run the following command from the root of your project. 24 | 25 | ``` 26 | composer require "divineomega/password_exposed" 27 | ``` 28 | 29 | If you have never used the Composer dependency manager before, head to the [Composer website](https://getcomposer.org/) for more information on how to get started. 30 | 31 | ## Usage 32 | 33 | To check if a password has been exposed in a data breach, just pass it to the `password_exposed` method. 34 | 35 | Here is a basic usage example: 36 | 37 | ```php 38 | switch(password_exposed('hunter2')) { 39 | 40 | case PasswordStatus::EXPOSED: 41 | // Password has been exposed in a data breach. 42 | break; 43 | 44 | case PasswordStatus::NOT_EXPOSED: 45 | // Password has not been exposed in a known data breach. 46 | break; 47 | 48 | case PasswordStatus::UNKNOWN: 49 | // Unable to check password due to an API error. 50 | break; 51 | } 52 | ``` 53 | 54 | If you prefer to avoid using helper functions, the following syntax is also available. 55 | 56 | ```php 57 | $passwordStatus = (new PasswordExposedChecker())->passwordExposed($password); 58 | ``` 59 | 60 | ### SHA1 Hash 61 | You can also supply the SHA1 hash instead of the plain text password, by using the following method. 62 | 63 | ```php 64 | $passwordStatus = (new PasswordExposedChecker())->passwordExposedByHash($hash); 65 | ``` 66 | 67 | or... 68 | 69 | ```php 70 | $passwordStatus = password_exposed_by_hash($hash); 71 | ``` 72 | -------------------------------------------------------------------------------- /assets/images/password-exposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivineOmega/password_exposed/327f93ee5cab54622077bcae721412b55be16720/assets/images/password-exposed.png -------------------------------------------------------------------------------- /bundles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DivineOmega/password_exposed/327f93ee5cab54622077bcae721412b55be16720/bundles/.gitkeep -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "divineomega/password_exposed", 3 | "description": "This PHP package provides a `password_exposed` helper function, that uses the haveibeenpwned.com API to check if a password has been exposed in a data breach.", 4 | "type": "library", 5 | "license": "LGPL-3.0-only", 6 | "homepage": "https://github.com/DivineOmega/password_exposed", 7 | "support": { 8 | "issues": "https://github.com/DivineOmega/password_exposed/issues", 9 | "source": "https://github.com/DivineOmega/password_exposed/releases", 10 | "wiki": "https://github.com/DivineOmega/password_exposed/wiki" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Jordan Hall", 15 | "email": "jordan@hall05.co.uk" 16 | }, 17 | { 18 | "name": "Contributors", 19 | "homepage": "https://github.com/DivineOmega/password_exposed/graphs/contributors" 20 | } 21 | ], 22 | "require": { 23 | "php": "^7.1||^8.0", 24 | "psr/http-client": "^1.0", 25 | "psr/cache": "^1.0", 26 | "psr/http-message": "^1.0", 27 | "psr/http-message-implementation": "^1.0", 28 | "psr/http-factory-implementation": "^1.0", 29 | "php-http/discovery": "^1.6", 30 | "nyholm/psr7": "^1.0", 31 | "paragonie/certainty": "^2.4", 32 | "divineomega/do-file-cache-psr-6": "^2.0", 33 | "divineomega/psr-18-guzzle-adapter": "^1.0" 34 | }, 35 | "require-dev": { 36 | "phpunit/phpunit": "^7.0||^8.0", 37 | "fzaninotto/faker": "^1.7", 38 | "vimeo/psalm": "^4", 39 | "kriswallsmith/buzz": "^1.0", 40 | "symfony/cache": "^4.2.12", 41 | "php-coveralls/php-coveralls": "^2.1" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "DivineOmega\\PasswordExposed\\": "src/" 46 | }, 47 | "files": [ 48 | "src/PasswordExposedFunction.php" 49 | ] 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "DivineOmega\\PasswordExposed\\Tests\\": "tests/" 54 | } 55 | }, 56 | "extra": { 57 | "branch-alias": { 58 | "dev-master": "3.0-dev" 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true 63 | } 64 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Unit 14 | ./tests/Integration 15 | 16 | 17 | 18 | 19 | src 20 | 21 | src/Examples 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/AbstractPasswordExposedChecker.php: -------------------------------------------------------------------------------- 1 | passwordExposedByHash($this->getHash($password)); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function passwordExposedByHash(string $hash): string 34 | { 35 | $cache = $this->getCache(); 36 | $cacheKey = substr($hash, 0, 2).'_'.substr($hash, 2, 3); 37 | $body = null; 38 | 39 | try { 40 | $cacheItem = $cache->getItem($cacheKey); 41 | 42 | // try to get status from cache 43 | if ($cacheItem->isHit()) { 44 | $body = $cacheItem->get(); 45 | } 46 | } catch (\Exception $e) { 47 | $cacheItem = null; 48 | } 49 | 50 | // get status from api 51 | if ($body === null) { 52 | try { 53 | /** @var ResponseInterface $response */ 54 | $response = $this->makeRequest($hash); 55 | 56 | /** @var string $responseBody */ 57 | $body = $response->getBody()->getContents(); 58 | 59 | // cache status 60 | if ($cacheItem !== null) { 61 | $cacheLifeTime = $this->getCacheLifeTime(); 62 | 63 | if ($cacheLifeTime <= 0) { 64 | $cacheLifeTime = self::CACHE_EXPIRY_SECONDS; 65 | } 66 | 67 | $cacheItem->set($body); 68 | $cacheItem->expiresAfter($cacheLifeTime); 69 | $cache->save($cacheItem); 70 | } 71 | } catch (ClientExceptionInterface $e) { 72 | } 73 | } 74 | 75 | if ($body === null) { 76 | return PasswordExposedCheckerInterface::UNKNOWN; 77 | } 78 | 79 | return $this->getPasswordStatus($hash, $body); 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function isExposed(string $password): ?bool 86 | { 87 | return $this->isExposedByHash($this->getHash($password)); 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function isExposedByHash(string $hash): ?bool 94 | { 95 | $status = $this->passwordExposedByHash($hash); 96 | 97 | if ($status === PasswordExposedCheckerInterface::EXPOSED) { 98 | return true; 99 | } 100 | 101 | if ($status === PasswordExposedCheckerInterface::NOT_EXPOSED) { 102 | return false; 103 | } 104 | 105 | return null; 106 | } 107 | 108 | /** 109 | * @param $hash 110 | * 111 | * @throws \Psr\Http\Client\ClientExceptionInterface 112 | * 113 | * @return ResponseInterface 114 | */ 115 | protected function makeRequest(string $hash): ResponseInterface 116 | { 117 | $uri = $this->getUriFactory()->createUri('https://api.pwnedpasswords.com/range/'.substr($hash, 0, 5)); 118 | $request = $this->getRequestFactory()->createRequest('GET', $uri); 119 | 120 | return $this->getClient()->sendRequest($request); 121 | } 122 | 123 | /** 124 | * @param $string 125 | * 126 | * @return string 127 | */ 128 | protected function getHash(string $string): string 129 | { 130 | return sha1($string); 131 | } 132 | 133 | /** 134 | * @param string $hash 135 | * @param string $responseBody 136 | * 137 | * @return string 138 | */ 139 | protected function getPasswordStatus($hash, $responseBody): string 140 | { 141 | $hash = strtoupper($hash); 142 | $hashSuffix = substr($hash, 5); 143 | 144 | $lines = explode("\r\n", $responseBody); 145 | 146 | foreach ($lines as $line) { 147 | list($exposedHashSuffix, $occurrences) = explode(':', $line); 148 | if (hash_equals($hashSuffix, $exposedHashSuffix)) { 149 | return PasswordStatus::EXPOSED; 150 | } 151 | } 152 | 153 | return PasswordStatus::NOT_EXPOSED; 154 | } 155 | 156 | /** 157 | * @return ClientInterface 158 | */ 159 | abstract protected function getClient(): ClientInterface; 160 | 161 | /** 162 | * @return CacheItemPoolInterface 163 | */ 164 | abstract protected function getCache(): CacheItemPoolInterface; 165 | 166 | /** 167 | * @return int 168 | */ 169 | abstract protected function getCacheLifeTime(): int; 170 | 171 | /** 172 | * @return RequestFactoryInterface 173 | */ 174 | abstract protected function getRequestFactory(): RequestFactoryInterface; 175 | 176 | /** 177 | * @return UriFactoryInterface 178 | */ 179 | abstract protected function getUriFactory(): UriFactoryInterface; 180 | } 181 | -------------------------------------------------------------------------------- /src/Enums/PasswordStatus.php: -------------------------------------------------------------------------------- 1 | client = $client; 55 | $this->cache = $cache; 56 | $this->cacheLifeTime = $cacheLifeTime; 57 | $this->requestFactory = $requestFactory; 58 | $this->uriFactory = $uriFactory; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | protected function getClient(): ClientInterface 65 | { 66 | if ($this->client === null) { 67 | $this->client = $this->createClient(); 68 | } 69 | 70 | return $this->client; 71 | } 72 | 73 | /** 74 | * @return ClientInterface 75 | */ 76 | protected function createClient(): ClientInterface 77 | { 78 | $options = [ 79 | 'timeout' => 3, 80 | 'headers' => [ 81 | 'User_Agent' => 'password_exposed - https://github.com/DivineOmega/password_exposed', 82 | ], 83 | ]; 84 | 85 | $bundle = $this->getBundle(); 86 | if ($bundle !== null) { 87 | $options['verify'] = $bundle->getFilePath(); 88 | } 89 | 90 | return new Client($options); 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | protected function getCache(): CacheItemPoolInterface 97 | { 98 | if ($this->cache === null) { 99 | $this->cache = $this->createCache(); 100 | } 101 | 102 | return $this->cache; 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | protected function getCacheLifeTime(): int 109 | { 110 | if ($this->cacheLifeTime === null) { 111 | return self::CACHE_EXPIRY_SECONDS; 112 | } 113 | 114 | return $this->cacheLifeTime; 115 | } 116 | 117 | /** 118 | * @return CacheItemPool 119 | */ 120 | protected function createCache(): CacheItemPoolInterface 121 | { 122 | $cache = new CacheItemPool(); 123 | $cache->changeConfig( 124 | [ 125 | 'cacheDirectory' => sys_get_temp_dir().'/password-exposed-cache/', 126 | ] 127 | ); 128 | 129 | return $cache; 130 | } 131 | 132 | /** 133 | * @return RequestFactoryInterface 134 | */ 135 | protected function getRequestFactory(): RequestFactoryInterface 136 | { 137 | if ($this->requestFactory === null) { 138 | $this->requestFactory = $this->createRequestFactory(); 139 | } 140 | 141 | return $this->requestFactory; 142 | } 143 | 144 | /** 145 | * @return RequestFactoryInterface 146 | */ 147 | protected function createRequestFactory(): RequestFactoryInterface 148 | { 149 | return Psr17FactoryDiscovery::findRequestFactory(); 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | protected function getUriFactory(): UriFactoryInterface 156 | { 157 | if ($this->uriFactory === null) { 158 | $this->uriFactory = $this->createUriFactory(); 159 | } 160 | 161 | return $this->uriFactory; 162 | } 163 | 164 | /** 165 | * @return UriFactoryInterface 166 | */ 167 | protected function createUriFactory(): UriFactoryInterface 168 | { 169 | return Psr17FactoryDiscovery::findUriFactory(); 170 | } 171 | 172 | /** 173 | * @return Bundle 174 | */ 175 | protected function getBundle(): ?Bundle 176 | { 177 | if ($this->bundle === null) { 178 | $this->bundle = $this->createBundle(); 179 | } 180 | 181 | return $this->bundle; 182 | } 183 | 184 | /** 185 | * @param Bundle $bundle 186 | */ 187 | public function setBundle(Bundle $bundle): void 188 | { 189 | $this->bundle = $bundle; 190 | } 191 | 192 | /** 193 | * @return Bundle 194 | */ 195 | protected function createBundle(): ?Bundle 196 | { 197 | try { 198 | return $this->getBundleFromCertainty(); 199 | } catch (\Exception $exception) { 200 | return null; 201 | } 202 | } 203 | 204 | /** 205 | * @throws \ParagonIE\Certainty\Exception\CertaintyException 206 | * @throws \SodiumException 207 | * 208 | * @return Bundle 209 | */ 210 | protected function getBundleFromCertainty(): Bundle 211 | { 212 | $ourCertaintyDataDir = __DIR__.'/../bundles'; 213 | 214 | if (!is_writable($ourCertaintyDataDir)) { 215 | 216 | // If we can't write to the our Certainty data directory, just 217 | // use the latest bundle from the Certainty package. 218 | return (new Fetch($ourCertaintyDataDir))->getLatestBundle(); 219 | } 220 | 221 | if (PHP_INT_SIZE === 4 && !extension_loaded('sodium')) { 222 | 223 | // If the platform would run verification checks slowly, use the 224 | // latest bundle from the Certainty package and disable verification. 225 | return (new Fetch($ourCertaintyDataDir))->getLatestBundle(false, false); 226 | } 227 | 228 | // If the platform can run verification checks well enough, get 229 | // latest remote bundle and verify it. 230 | try { 231 | // Try the replication server first, since the upstream server 232 | // is under tremendous load. 233 | return (new RemoteFetch($ourCertaintyDataDir)) 234 | ->setChronicle( 235 | 'https://php-chronicle-replica.pie-hosted.com/chronicle/replica/_vi6Mgw6KXBSuOFUwYA2H2GEPLawUmjqFJbCCuqtHzGZ', 236 | 'MoavD16iqe9-QVhIy-ewD4DMp0QRH-drKfwhfeDAUG0=' 237 | ) 238 | ->getLatestBundle(); 239 | } catch (ConnectException $ex) { 240 | // Fallback to the main server. 241 | return (new RemoteFetch($ourCertaintyDataDir))->getLatestBundle(); 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/PasswordExposedFunction.php: -------------------------------------------------------------------------------- 1 | passwordExposed($password); 13 | } 14 | 15 | /** 16 | * @param string $hash 17 | * 18 | * @return string 19 | */ 20 | function password_exposed_by_hash($hash): string 21 | { 22 | return (new PasswordExposedChecker())->passwordExposedByHash($hash); 23 | } 24 | 25 | /** 26 | * @param string $password 27 | * 28 | * @return bool|null 29 | */ 30 | function password_is_exposed($password): ?bool 31 | { 32 | return (new PasswordExposedChecker())->isExposed($password); 33 | } 34 | 35 | /** 36 | * @param string $hash 37 | * 38 | * @return bool|null 39 | */ 40 | function password_is_exposed_by_hash($hash): ?bool 41 | { 42 | return (new PasswordExposedChecker())->isExposedByHash($hash); 43 | } 44 | -------------------------------------------------------------------------------- /tests/Unit/CustomInjectionsTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($passwordExposedChecker->isExposed('hunter2')); 25 | $this->assertFalse($passwordExposedChecker->isExposed($this->getPasswordHashUnlikelyToBeExposed())); 26 | } 27 | 28 | public function testCustomLibrary() 29 | { 30 | $client = new FileGetContents(Psr17FactoryDiscovery::findResponseFactory()); 31 | $cache = new FilesystemAdapter('test', 3600, __DIR__.'/../../cache/symfony'); 32 | 33 | $passwordExposedChecker = new PasswordExposedChecker($client, $cache); 34 | 35 | $this->assertEquals(PasswordStatus::EXPOSED, $passwordExposedChecker->passwordExposed('hunter2')); 36 | $this->assertEquals(PasswordStatus::NOT_EXPOSED, $passwordExposedChecker->passwordExposed($this->getPasswordHashUnlikelyToBeExposed())); 37 | } 38 | 39 | public function testLocalBundleInjection() 40 | { 41 | $pemFiles = glob(__DIR__.'/../../vendor/paragonie/certainty/data/*.pem'); 42 | $bundle = new Bundle(end($pemFiles)); 43 | 44 | $cache = new CacheItemPool(); 45 | $cache->changeConfig( 46 | [ 47 | 'cacheDirectory' => __DIR__.'/../../cache/dofilecache/', 48 | 'gzipCompression' => false, 49 | ] 50 | ); 51 | 52 | $passwordExposedChecker = new PasswordExposedChecker(null, $cache); 53 | $passwordExposedChecker->setBundle($bundle); 54 | 55 | $this->assertEquals(PasswordStatus::EXPOSED, $passwordExposedChecker->passwordExposed('hunter2')); 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | private function getPasswordHashUnlikelyToBeExposed() 62 | { 63 | $faker = Factory::create(); 64 | 65 | return sha1($faker->words(6, true)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Unit/PasswordExposedByHashTest.php: -------------------------------------------------------------------------------- 1 | changeConfig( 20 | [ 21 | 'cacheDirectory' => __DIR__.'/../../cache/dofilecache/', 22 | 'gzipCompression' => false, 23 | ] 24 | ); 25 | $this->checker = new PasswordExposedChecker(null, $cache); 26 | } 27 | 28 | public function testFunctionExists() 29 | { 30 | $this->assertTrue(function_exists('password_exposed_by_hash')); 31 | } 32 | 33 | /** 34 | * @return array 35 | */ 36 | public function exposedPasswordHashProvider() 37 | { 38 | return [ 39 | [sha1('test')], 40 | [sha1('password')], 41 | [sha1('hunter2')], 42 | ]; 43 | } 44 | 45 | /** 46 | * @dataProvider exposedPasswordHashProvider 47 | * 48 | * @param string $hash 49 | */ 50 | public function testExposedPasswords($hash) 51 | { 52 | $this->assertEquals($this->checker->passwordExposedByHash($hash), PasswordStatus::EXPOSED); 53 | $this->assertEquals(password_exposed_by_hash($hash), PasswordStatus::EXPOSED); 54 | $this->assertTrue($this->checker->isExposedByHash($hash)); 55 | $this->assertTrue(password_is_exposed_by_hash($hash)); 56 | } 57 | 58 | public function testNotExposedPasswords() 59 | { 60 | $this->assertEquals( 61 | $this->checker->passwordExposedByHash($this->getPasswordHashUnlikelyToBeExposed()), 62 | PasswordStatus::NOT_EXPOSED 63 | ); 64 | $this->assertEquals( 65 | password_exposed_by_hash($this->getPasswordHashUnlikelyToBeExposed()), 66 | PasswordStatus::NOT_EXPOSED 67 | ); 68 | $this->assertEquals( 69 | $this->checker->isExposedByHash($this->getPasswordHashUnlikelyToBeExposed()), 70 | false 71 | ); 72 | $this->assertEquals( 73 | password_is_exposed_by_hash($this->getPasswordHashUnlikelyToBeExposed()), 74 | false 75 | ); 76 | } 77 | 78 | /** 79 | * @return string 80 | */ 81 | private function getPasswordHashUnlikelyToBeExposed() 82 | { 83 | $faker = Factory::create(); 84 | 85 | return sha1($faker->words(6, true)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Unit/PasswordExposedTest.php: -------------------------------------------------------------------------------- 1 | changeConfig( 20 | [ 21 | 'cacheDirectory' => __DIR__.'/../../cache/dofilecache/', 22 | 'gzipCompression' => false, 23 | ] 24 | ); 25 | $this->checker = new PasswordExposedChecker(null, $cache); 26 | } 27 | 28 | public function testFunctionExists() 29 | { 30 | $this->assertTrue(function_exists('password_exposed')); 31 | } 32 | 33 | public function exposedPasswordsProvider() 34 | { 35 | return [ 36 | ['test'], 37 | ['password'], 38 | ['hunter2'], 39 | ]; 40 | } 41 | 42 | /** 43 | * @dataProvider exposedPasswordsProvider 44 | */ 45 | public function testExposedPasswords($password) 46 | { 47 | $this->assertEquals($this->checker->passwordExposed($password), PasswordStatus::EXPOSED); 48 | $this->assertEquals(password_exposed($password), PasswordStatus::EXPOSED); 49 | $this->assertTrue($this->checker->isExposed($password)); 50 | $this->assertTrue(password_is_exposed($password)); 51 | } 52 | 53 | public function testNotExposedPasswords() 54 | { 55 | $this->assertEquals( 56 | $this->checker->passwordExposed($this->getPasswordUnlikelyToBeExposed()), 57 | PasswordStatus::NOT_EXPOSED 58 | ); 59 | $this->assertEquals(password_exposed($this->getPasswordUnlikelyToBeExposed()), PasswordStatus::NOT_EXPOSED); 60 | $this->assertEquals( 61 | $this->checker->isExposed($this->getPasswordUnlikelyToBeExposed()), 62 | false 63 | ); 64 | $this->assertEquals( 65 | password_is_exposed($this->getPasswordUnlikelyToBeExposed()), 66 | false 67 | ); 68 | } 69 | 70 | private function getPasswordUnlikelyToBeExposed() 71 | { 72 | $faker = Factory::create(); 73 | 74 | $password = ''; 75 | 76 | for ($i = 0; $i < 6; $i++) { 77 | $password .= $faker->word(); 78 | $password .= ' '; 79 | } 80 | 81 | $password = trim($password); 82 | 83 | return $password; 84 | } 85 | } 86 | --------------------------------------------------------------------------------