Skip to content

Commit

Permalink
Merge 3c66aa1 into 908ed8e
Browse files Browse the repository at this point in the history
  • Loading branch information
nicklog authored Jan 28, 2019
2 parents 908ed8e + 3c66aa1 commit 378969b
Show file tree
Hide file tree
Showing 9 changed files with 460 additions and 122 deletions.
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
composer.lock
vendor
/composer.lock
/vendor/
/cache/
/tests/Logs
bundles/*
!bundles/.

Expand Down
24 changes: 17 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
"email": "jordan@hall05.co.uk"
}
],
"require": {
"php": "^7.1",
"psr/http-client": "^1.0",
"psr/cache": "^1.0",
"psr/http-message": "^1.0",
"php-http/guzzle6-adapter": "^1.1|2.0",
"php-http/discovery": "^1.6",
"nyholm/psr7": "^1.0",
"paragonie/certainty": "^1|^2",
"divineomega/do-file-cache-psr-6": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^6.5",
"fzaninotto/faker": "^1.7",
Expand All @@ -27,11 +38,10 @@
"DivineOmega\\PasswordExposed\\Tests\\": "tests/"
}
},
"license": "LGPL-3.0-only",
"require": {
"php": ">=5.6",
"guzzlehttp/guzzle": "^6.3",
"paragonie/certainty": "^1|^2",
"divineomega/do-file-cache-psr-6": "^2.0"
}
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"license": "LGPL-3.0-only"
}
164 changes: 164 additions & 0 deletions src/AbstractPasswordExposedChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

namespace DivineOmega\PasswordExposed;

use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriFactoryInterface;

/**
* Class AbstractPasswordExposedChecker
*
* @package DivineOmega\PasswordExposed
*/
abstract class AbstractPasswordExposedChecker implements PasswordExposedCheckerInterface
{

/** @var int */
protected const CACHE_EXPIRY_SECONDS = 2592000;

/**
* @inheritdoc
*/
public function passwordExposed(string $password): string
{
return $this->passwordExposedByHash($this->getHash($password));
}

/**
* @inheritdoc
*/
public function passwordExposedByHash(string $hash): string
{
$cacheKey = substr($hash, 0, 2) . '_' . substr($hash, 2, 3);

try {
$cacheItem = $this->getCache()->getItem($cacheKey);
} catch (\Exception $e) {
$cacheItem = null;
}

if ($cacheItem !== null && $cacheItem->isHit()) {
/** @var string $responseBody */
$responseBody = $cacheItem->get();
} else {
try {
/** @var ResponseInterface $response */
$response = $this->makeRequest($hash);

if ($response->getStatusCode() !== 200) {
return PasswordExposedCheckerInterface::UNKNOWN;
}
} catch (ClientExceptionInterface $e) {
return PasswordExposedCheckerInterface::UNKNOWN;
}

/** @var string $responseBody */
$responseBody = $response->getBody()->getContents();

if ($cacheItem !== null) {
$cacheItem->set($responseBody);
$cacheItem->expiresAfter(self::CACHE_EXPIRY_SECONDS);
$this->getCache()->save($cacheItem);
}
}

return $this->getPasswordStatus($hash, $responseBody);
}

/**
* @inheritdoc
*/
public function isExposed(string $password): ?bool
{
return $this->isExposedByHash($this->getHash($password));
}

/**
* @inheritdoc
*/
public function isExposedByHash(string $hash): ?bool
{
$status = $this->passwordExposedByHash($hash);

if ($status === PasswordExposedCheckerInterface::EXPOSED) {
return true;
}

if ($status === PasswordExposedCheckerInterface::NOT_EXPOSED) {
return false;
}

return null;
}

/**
* @param $hash
*
* @return ResponseInterface
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
protected function makeRequest(string $hash): ResponseInterface
{
$uri = $this->getUriFactory()->createUri('https://api.pwnedpasswords.com/range/' . substr($hash, 0, 5));
$request = $this->getRequestFactory()->createRequest('GET', $uri);

return $this->getClient()->sendRequest($request);
}

/**
* @param $string
*
* @return string
*/
protected function getHash(string $string): string
{
return sha1($string);
}

/**
* @param string $hash
* @param string $responseBody
*
* @return string
*/
protected function getPasswordStatus($hash, $responseBody): string
{
$hash = strtoupper($hash);
$hashSuffix = substr($hash, 5);

$lines = explode("\r\n", $responseBody);

foreach ($lines as $line) {
[$exposedHashSuffix, $occurrences] = explode(':', $line);
if (hash_equals($hashSuffix, $exposedHashSuffix)) {
return PasswordStatus::EXPOSED;
}
}

return PasswordStatus::NOT_EXPOSED;
}

/**
* @return ClientInterface
*/
abstract protected function getClient(): ClientInterface;

/**
* @return CacheItemPoolInterface
*/
abstract protected function getCache(): CacheItemPoolInterface;

/**
* @return RequestFactoryInterface
*/
abstract protected function getRequestFactory(): RequestFactoryInterface;

/**
* @return UriFactoryInterface
*/
abstract protected function getUriFactory(): UriFactoryInterface;
}
Loading

0 comments on commit 378969b

Please sign in to comment.