-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature #15 Initial P0wnedPassword api checker (gnat42, sstok)
This PR was merged into the 1.0-dev branch. Discussion ---------- | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT PwnedPassword api checker. Fails validation if the user's password was found in the 500 million compromised password database. Commits ------- 326b004 initial P0wnedPassword api checker 3236291 Fix style
- Loading branch information
Showing
8 changed files
with
394 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the RollerworksPasswordStrengthValidator package. | ||
* | ||
* (c) Sebastiaan Stok <[email protected]> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace Rollerworks\Component\PasswordStrength\P0wnedPassword\Request; | ||
|
||
use GuzzleHttp\Psr7\Request; | ||
use Http\Client\Exception\HttpException; | ||
use Http\Client\Exception as HttpException2; | ||
use Http\Client\HttpClient; | ||
use Psr\Log\LoggerInterface; | ||
|
||
class Client | ||
{ | ||
/** @var HttpClient */ | ||
private $client; | ||
|
||
/** @var LoggerInterface */ | ||
private $logger; | ||
|
||
public function __construct(HttpClient $client, LoggerInterface $logger) | ||
{ | ||
$this->client = $client; | ||
$this->logger = $logger; | ||
} | ||
|
||
/** | ||
* @param $password | ||
* | ||
* @return Result | ||
* | ||
* @throws \Http\Client\Exception | ||
*/ | ||
public function check($password) | ||
{ | ||
$hashedPassword = strtoupper(sha1($password)); | ||
$checkHash = substr($hashedPassword, 0, 5); | ||
|
||
try { | ||
$response = $this->client->sendRequest(new Request('GET', 'https://api.pwnedpasswords.com/range/'.$checkHash)); | ||
if ($response->getStatusCode() === 200) { | ||
$rowResults = explode("\n", (string) $response->getBody()); | ||
$searchHash = substr($hashedPassword, 5); | ||
foreach ($rowResults as $result) { | ||
if (strpos($result, $searchHash) !== false) { | ||
$res = explode(':', $result); | ||
|
||
return new Result(trim($res[1])); | ||
} | ||
} | ||
} | ||
} catch (HttpException $exception) { | ||
$this->logger->error('HTTP Exception: '.$exception->getMessage()); | ||
} catch (HttpException2 $exception) { | ||
$this->logger->error('HTTP Exception: '.$exception->getMessage()); | ||
} catch (\Exception $exception) { | ||
$this->logger->error('Exception: '.$exception->getMessage()); | ||
} | ||
|
||
return new Result(0); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the RollerworksPasswordStrengthValidator package. | ||
* | ||
* (c) Sebastiaan Stok <[email protected]> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace Rollerworks\Component\PasswordStrength\P0wnedPassword\Request; | ||
|
||
class Result | ||
{ | ||
/** | ||
* @var int | ||
*/ | ||
private $useCount = 0; | ||
|
||
/** | ||
* Result constructor. | ||
* | ||
* @param int $useCount | ||
*/ | ||
public function __construct($useCount) | ||
{ | ||
$this->useCount = (int) $useCount; | ||
} | ||
|
||
/** | ||
* @return int | ||
*/ | ||
public function getUseCount() | ||
{ | ||
return $this->useCount; | ||
} | ||
|
||
/** | ||
* @return bool | ||
*/ | ||
public function wasFound() | ||
{ | ||
return $this->useCount > 0; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the RollerworksPasswordStrengthValidator package. | ||
* | ||
* (c) Sebastiaan Stok <[email protected]> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace Rollerworks\Component\PasswordStrength\Validator\Constraints; | ||
|
||
use Symfony\Component\Validator\Constraint; | ||
|
||
class P0wnedPassword extends Constraint | ||
{ | ||
public $message = 'This password was found in a database of compromised passwords. It has been used {{ used }} times. For security purposes you must use something else.'; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getTargets() | ||
{ | ||
return self::PROPERTY_CONSTRAINT; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the RollerworksPasswordStrengthValidator package. | ||
* | ||
* (c) Sebastiaan Stok <[email protected]> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace Rollerworks\Component\PasswordStrength\Validator\Constraints; | ||
|
||
use Rollerworks\Component\PasswordStrength\P0wnedPassword\Request\Client; | ||
use Symfony\Component\Validator\Constraint; | ||
use Symfony\Component\Validator\ConstraintValidator; | ||
use Symfony\Component\Validator\Exception\UnexpectedTypeException; | ||
|
||
class P0wnedPasswordValidator extends ConstraintValidator | ||
{ | ||
/** @var Client */ | ||
private $client; | ||
|
||
/** | ||
* P0wnedPasswordValidator constructor. | ||
* | ||
* @param Client $client | ||
*/ | ||
public function __construct(Client $client) | ||
{ | ||
$this->client = $client; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function validate($password, Constraint $constraint) | ||
{ | ||
if (null === $password) { | ||
return; | ||
} | ||
|
||
if (!is_scalar($password) && !(is_object($password) && method_exists($password, '__toString'))) { | ||
throw new UnexpectedTypeException($password, 'string'); | ||
} | ||
|
||
$password = (string) $password; | ||
|
||
$result = $this->client->check($password); | ||
if ($result->wasFound()) { | ||
$this->context | ||
->buildViolation($constraint->message) | ||
->setParameter('{{ used }}', number_format($result->getUseCount())) | ||
->addViolation(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the RollerworksPasswordStrengthValidator package. | ||
* | ||
* (c) Sebastiaan Stok <[email protected]> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace Rollerworks\Component\PasswordStrength\Tests\P0wnedPassword\Request; | ||
|
||
use GuzzleHttp\Psr7\Request; | ||
use GuzzleHttp\Psr7\Response; | ||
use Http\Client\HttpClient; | ||
use Rollerworks\Component\PasswordStrength\P0wnedPassword\Request\Client; | ||
use PHPUnit\Framework\MockObject\MockObject; | ||
use PHPUnit\Framework\TestCase; | ||
use Psr\Log\NullLogger; | ||
use Rollerworks\Component\PasswordStrength\P0wnedPassword\Request\Result; | ||
|
||
class ClientTest extends TestCase | ||
{ | ||
/** @var HttpClient|MockObject */ | ||
private $client; | ||
|
||
/** @var Client */ | ||
private $checker; | ||
|
||
private $foundResult = '00659D6178E2DAF3D6BE70358A1EB54883A:6 | ||
032471D9B185979579C773FDA622293BFFC:24 | ||
0371D53107275E34091E29220622B62ED72:2 | ||
0593916D0FE39C1CBF870BDA44AA1A6F9A2:15 | ||
06ACBE8716FFA0D70E8453AC0CF560B9B22:3 | ||
06C866D7AEE7CBB84255BBF5529520D878B:3 | ||
08403CDA0395608552144349EB96D30DD5F:28 | ||
08E1C1E99DE892ED16CDED45306D8DEDDBF:3 | ||
0955820A68920AF7559F89FE2D12EE83357:2 | ||
09F74D1DC15112328C8A8ACC90D394AFBE4:7 | ||
5F74FD0522862127A00BDEF879C4D9A1A02:4031 | ||
0A517529767483361432D4F3663F89BAA7E:2 | ||
0C341F894BD4EE961AE874ACD3BC8157825:4 | ||
'; | ||
private $notFoundResult = '00659D6178E2DAF3D6BE70358A1EB54883A:6 | ||
032471D9B185979579C773FDA622293BFFC:24 | ||
0371D53107275E34091E29220622B62ED72:2 | ||
0593916D0FE39C1CBF870BDA44AA1A6F9A2:15 | ||
06ACBE8716FFA0D70E8453AC0CF560B9B22:3 | ||
06C866D7AEE7CBB84255BBF5529520D878B:3 | ||
08403CDA0395608552144349EB96D30DD5F:28 | ||
08E1C1E99DE892ED16CDED45306D8DEDDBF:3 | ||
0955820A68920AF7559F89FE2D12EE83357:2 | ||
09F74D1DC15112328C8A8ACC90D394AFBE4:7 | ||
0A517529767483361432D4F3663F89BAA7E:2 | ||
0C341F894BD4EE961AE874ACD3BC8157825:4 | ||
'; | ||
|
||
public function setUp() | ||
{ | ||
$this->client = $this->createMock(HttpClient::class); | ||
$this->checker = new Client($this->client, new NullLogger()); | ||
} | ||
|
||
public function testResponseWithFoundResult() | ||
{ | ||
$password = 'correctbatteryhorse'; | ||
$responseMock = $this->createMock(Response::class); | ||
$responseMock->expects($this->once())->method('getStatusCode')->willReturn(200); | ||
$responseMock->expects($this->once())->method('getBody')->willReturn($this->foundResult); | ||
$request = new Request('GET', 'https://api.pwnedpasswords.com/range/C4FA0'); | ||
$this->client->expects($this->once()) | ||
->method('sendRequest') | ||
->with($request) | ||
->willReturn($responseMock); | ||
|
||
$result = $this->checker->check($password); | ||
$this->assertInstanceOf(Result::class, $result); | ||
$this->assertTrue($result->wasFound()); | ||
$this->assertEquals(4031, $result->getUseCount()); | ||
} | ||
|
||
public function testResponseWithoutFoundResult() | ||
{ | ||
$password = 'correctbatteryhorse'; | ||
$responseMock = $this->createMock(Response::class); | ||
$responseMock->expects($this->once())->method('getStatusCode')->willReturn(200); | ||
$responseMock->expects($this->once())->method('getBody')->willReturn($this->notFoundResult); | ||
$request = new Request('GET', 'https://api.pwnedpasswords.com/range/C4FA0'); | ||
$this->client->expects($this->once()) | ||
->method('sendRequest') | ||
->with($request) | ||
->willReturn($responseMock); | ||
|
||
$result = $this->checker->check($password); | ||
$this->assertInstanceOf(Result::class, $result); | ||
$this->assertFalse($result->wasFound()); | ||
$this->assertEquals(0, $result->getUseCount()); | ||
} | ||
|
||
public function testNon200Response() | ||
{ | ||
$password = 'correctbatteryhorse'; | ||
$responseMock = $this->createMock(Response::class); | ||
$responseMock->expects($this->once())->method('getStatusCode')->willReturn(404); | ||
$responseMock->expects($this->never())->method('getBody'); | ||
$request = new Request('GET', 'https://api.pwnedpasswords.com/range/C4FA0'); | ||
$this->client->expects($this->once()) | ||
->method('sendRequest') | ||
->with($request) | ||
->willReturn($responseMock); | ||
|
||
$result = $this->checker->check($password); | ||
$this->assertInstanceOf(Result::class, $result); | ||
$this->assertFalse($result->wasFound()); | ||
$this->assertEquals(0, $result->getUseCount()); | ||
} | ||
} |
Oops, something went wrong.