├── .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 |
12 |
13 |
14 |
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 |
--------------------------------------------------------------------------------