Skip to content

Commit

Permalink
Merge eaa4045 into 908ed8e
Browse files Browse the repository at this point in the history
  • Loading branch information
nicklog authored Feb 4, 2019
2 parents 908ed8e + eaa4045 commit b5df5d0
Show file tree
Hide file tree
Showing 14 changed files with 588 additions and 129 deletions.
22 changes: 22 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4

[*.yml]
indent_size = 2

# Ignore paths
[/{vendor, cache, bundles, assets}/**]
charset = none
end_of_line = none
insert_final_newline = none
trim_trailing_whitespace = none
indent_style = none
indent_size = none
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
8 changes: 4 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ php:
- 'hhvm'
matrix:
allow_failures:
- php: nightly
- php: nightly
install:
- composer install
script:
- ./vendor/bin/phpunit --coverage-clover ./tests/Logs/clover.xml
- ./vendor/bin/psalm
- ./vendor/bin/phpunit --coverage-clover ./tests/Logs/clover.xml
- ./vendor/bin/psalm
after_script:
- php vendor/bin/php-coveralls -v
- php vendor/bin/php-coveralls -v
40 changes: 33 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,42 @@
"name": "divineomega/password_exposed",
"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.",
"type": "library",
"license": "LGPL-3.0-only",
"homepage": "https://github.com/DivineOmega/password_exposed",
"support": {
"issues": "https://github.com/DivineOmega/password_exposed/issues",
"source": "https://github.com/DivineOmega/password_exposed/releases",
"wiki": "https://github.com/DivineOmega/password_exposed/wiki"
},
"authors": [
{
"name": "Jordan Hall",
"email": "jordan@hall05.co.uk"
},
{
"name": "Contributors",
"homepage": "https://github.com/DivineOmega/password_exposed/graphs/contributors"
}
],
"require": {
"php": "^7.1",
"psr/http-client": "^1.0",
"psr/cache": "^1.0",
"psr/http-message": "^1.0",
"psr/http-message-implementation": "^1.0",
"psr/http-factory-implementation": "^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",
"vimeo/psalm": "^1",
"kriswallsmith/buzz": "^1.0",
"symfony/cache": "^3.4|^4.0",
"php-coveralls/php-coveralls": "^2.1"
},
"autoload": {
Expand All @@ -27,11 +53,11 @@
"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"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
4 changes: 2 additions & 2 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
</whitelist>
</filter>
<php>

</php>
</phpunit>
</phpunit>
2 changes: 1 addition & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
totallyTyped="false"
>
<projectFiles>
<directory name="src" />
<directory name="src"/>
</projectFiles>
</psalm>
178 changes: 178 additions & 0 deletions src/AbstractPasswordExposedChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?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.
*/
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
{
$cache = $this->getCache();
$cacheKey = substr($hash, 0, 2).'_'.substr($hash, 2, 3);
$body = null;

try {
$cacheItem = $cache->getItem($cacheKey);

// try to get status from cache
if ($cacheItem->isHit()) {
$body = $cacheItem->get();
}
} catch (\Exception $e) {
$cacheItem = null;
}

// get status from api
if ($body === null) {
try {
/** @var ResponseInterface $response */
$response = $this->makeRequest($hash);

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

// cache status
if ($cacheItem !== null) {
$cacheLifeTime = $this->getCacheLifeTime();

if ($cacheLifeTime <= 0) {
$cacheLifeTime = self::CACHE_EXPIRY_SECONDS;
}

$cacheItem->set($body);
$cacheItem->expiresAfter($cacheLifeTime);
$cache->save($cacheItem);
}
} catch (ClientExceptionInterface $e) {
}
}

if ($body === null) {
return PasswordExposedCheckerInterface::UNKNOWN;
}

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

/**
* {@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
*
* @throws \Psr\Http\Client\ClientExceptionInterface
*
* @return ResponseInterface
*/
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) {
list($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 int
*/
abstract protected function getCacheLifeTime(): int;

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

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

0 comments on commit b5df5d0

Please sign in to comment.