Most of the time users don't even know that their password is compromised in a data leak and they keep using it while registering at new sites.
This is a very common reason which can lead to your user account getting compromised.
To protect against it we can use Pwned Password API to check for user password leak during registration. Pwned Password API uses a k-Anonymity model to check for leaks without having to send actual user passwords to their server.
How to
Step 1:
Create a SHA-1 hash of the password you want to test, and split the generated hash into 2 parts. First one contain first 5 letter and the other remaining.
const hash = sha1("admin@123")
// 23D42F5F3F66498B2C8FF4C20B8C5AC826E47146
const prefix = hash.slice(0, 5)
// 23D42
const suffix = hash.slice(5)
// F5F3F66498B2C8FF4C20B8C5AC826E47146
Step 2:
Now we need to hit the Pwned Password API with the first 5 letters of the hash.
Syntax:
GET https://api.pwnedpasswords.com/range/{first 5 hash chars}
Example:
GET https://api.pwnedpasswords.com/range/23D42
If there is any match, API will respond with a 200 containing suffix of all matched prefixes, followed by a count of how many times it appears in the data set.
Example Response:
F2304657BE5F09BF4C3B7437F5DDEE82E33:1
F2B1D579ED038F01B8D4D83361F9B00408A:2
F3422D62BF236E1E34035CCAED54E84EE8B:2
F3A46B097AB734EA1F28ED557413CEC03A5:3
F3F5E4A3788114EFBF26F4F7382864D6862:3
F4117F7EFB4ED0FE681051726EEA090E5AC:1
F43720FD73143B6E8A4A6B8C7E5A267AF32:1
F4E9AC75719EEE37231CE1A53CFF738C11B:2
F58377AC2990EAA2B6FEB595F973EE84ACD:1
F5BC40F7128B2A0A14D73781A000DD11959:2
F5C9F91934B26E4398080C1644003C546D6:9
F5D6D61A69CE393EE0B4B85E665162D5039:1
F5F1BB6CE22185813667B0EBE169A5EF7CD:12
F5F3F66498B2C8FF4C20B8C5AC826E47146:3423
F60C7D4EF1803230E8FD7E403047BCBAD55:1
F6117DEE59623D69DE06FCA2438F7BB8F85:2
F6601EF3EF8FDE535545A0FF350D4465D12:7
F6890FEC65093B4FF688C9812749F344673:3
F778202A2E4005DDF1127B277EFA268A8C2:1
...
Step 3:
We can now process the response and search for the suffix and see how many times it appeared in the dataset.
In our case for password admin@123
we can see its suffix F5F3F66498B2C8FF4C20B8C5AC826E47146
appeared 3423 times which is bad.
We can then remind the user to choose a different password depending on our application stack.
Tip:
It's always good to add Add-Padding: true in the request Header as it further enhances privacy by returning 800-1000 results regardless of the number of hash suffixes returned by the service. Read the full blog post on padding.
Implementations
We can either implement it manually by following the above steps or use a package.
This can be either in the frontend or backend as we wish.
Laravel, a very popular PHP framework recently added inbuild support for it.
PHP (Laravel)
In Laravel 8.39 and above we can now just use the Password Rule Object to check for compromised passwords.
Example:
<?php
$request->validate([
'password' => [
'required',
'confirmed',
Password::min(8)
->mixedCase()
->letters()
->numbers()
->symbols()
->uncompromised(), // Check for leaks
],
]);
Under the hood Laravel will call the Pwned password API and handle the checking for you.
Here is the part of code which is responsible in case you want to use this in older Laravel versions.
<?php
namespace Illuminate\Validation;
use Exception;
use Illuminate\Contracts\Validation\UncompromisedVerifier;
use Illuminate\Support\Str;
class NotPwnedVerifier implements UncompromisedVerifier
{
/**
* The HTTP factory instance.
*
* @var \Illuminate\Http\Client\Factory
*/
protected $factory;
/**
* Create a new uncompromised verifier.
*
* @param \Illuminate\Http\Client\Factory $factory
* @return void
*/
public function __construct($factory)
{
$this->factory = $factory;
}
/**
* Verify that the given data has not been compromised in public breaches.
*
* @param array $data
* @return bool
*/
public function verify($data)
{
$value = $data['value'];
$threshold = $data['threshold'];
if (empty($value = (string) $value)) {
return false;
}
[$hash, $hashPrefix] = $this->getHash($value);
return ! $this->search($hashPrefix)
->contains(function ($line) use ($hash, $hashPrefix, $threshold) {
[$hashSuffix, $count] = explode(':', $line);
return $hashPrefix.$hashSuffix == $hash && $count > $threshold;
});
}
/**
* Get the hash and its first 5 chars.
*
* @param string $value
* @return array
*/
protected function getHash($value)
{
$hash = strtoupper(sha1((string) $value));
$hashPrefix = substr($hash, 0, 5);
return [$hash, $hashPrefix];
}
/**
* Search by the given hash prefix and returns all occurrences of leaked passwords.
*
* @param string $hashPrefix
* @return \Illuminate\Support\Collection
*/
protected function search($hashPrefix)
{
try {
$response = $this->factory->withHeaders([
'Add-Padding' => true,
])->get(
'https://api.pwnedpasswords.com/range/'.$hashPrefix
);
} catch (Exception $e) {
report($e);
}
$body = (isset($response) && $response->successful())
? $response->body()
: '';
return Str::of($body)->trim()->explode("\n")->filter(function ($line) {
return Str::contains($line, ':');
});
}
}
Javascript
In JS this can either be implemented on the frontend or on the backend (NodeJS) as per requirement, here is a well-maintained library for this task.
https://github.com/wKovacs64/hibp
Top comments (0)