HIBP’s Pwned Passwords API Usage

When Troy Hunt blogged about the adoption of HIBP’s Pwned Password, it got me thinking about an ideal general approach that anyone can take to implement this feature without obstructing the current setup.

Let’s first discuss what this service is about.

Troy has been collecting and verifying data from many headline-worthy breaches. Over the years, he has accumulated a considerable amount of compromised email and password hashes.

Originally, he created an API for developers to query breaches associated with an email address.

In August 2017, Pwned Passwords was implemented. It is an API that allows the querying of a breached password.

With access to such information, developers across the internet are able to warn their users if their current password is found in the database.

Pwned Passwords is not just a database of breached passwords. Its ingenuity comes from how the API was implemented.

k-anonymity model

This means that it is difficult for the API provider to correlate the requested password to an individual.

A typical request looks like this:

https://api.pwnedpasswords.com/range/21BD1

\‘21BD1’ is the first five characters of an SHA-1 hash.

By requesting for a range, it eliminates the uniqueness of an individual’s password since there are at least a dozen of SHA-1 hashes prefixed with ‘21BD1’.

But there is a catch. It is crucial for Troy to find the optimal value. If the number of characters is too small, then the response size would be large which leads to bad performance.

On the other hand, if the number of characters increases, then it reduces the level of anonymity.

Implementation

The implementation is fairly straightforward. There are 2 ways of getting a breached password.

  1. Pwned Passwords API
  2. Downloadable Pwned Passwords

However, I favor using API more than the latter, and here is why.

  1. When using API, the pwned passwords are updated whenever HIBP’s database updates. Therefore you do not have to download the offline version and keep it updated.
  2. In addition, you do not have to store the pwned passwords on the server which translate to more storage space (yay!). However, you are trading off bandwidth which brings me to the next point.
  3. Calling API on the client’s browser instead of the server
    • No interference with the current setup. The check for breached passwords acts as an additional check and validation could still be performed on the server.
    • Currently, there are no rate limits for the API. However, in the future, if the rate limit were to be implemented, we would not have to worry since the request is from the user’s IP address.
    • It could be argued that the check could be bypassed using a browser proxy. This however does not actually compromise any security. Our main purpose is to warn users that the password that they are using is found in other breaches. If they are tech-savvy enough to use a browser proxy, they would have understood the risk.
    • Since the API uses the first characters of an SHA-1 hash, we are required to perform a hashing function on the plaintext password before requesting API. And since the calculation is performed on the client’s browser, we have offloaded this calculation to the user’s device.

Proof of Concept

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jshashes/1.0.7/hashes.min.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
</head>
<body>

<script>
function checkBreachedPassword() {
  var password = document.getElementById("pass").value;
  var passwordDigest = new Hashes.SHA1().hex(password);
  var digestFive = passwordDigest.substring(0, 5).toUpperCase();
  var queryURL = "https://api.pwnedpasswords.com/range/" + digestFive;
  var checkDigest = passwordDigest.substring(5, 41).toUpperCase();
  var result;

$.ajax({
    url: queryURL,
    type: 'GET',
    async: false,
    success: function(res) {
    	if (res.search(checkDigest) > -1){
        result = false;
        document.getElementById("result").innerHTML = "Result: Breached"
      } else {
        result = true;
        document.getElementById("result").innerHTML = "Result: Not Breached <redirected>"
      }
    }
  });
  return result;
}
</script>

<form action="#" onsubmit="return checkBreachedPassword();" style="margin:auto auto 50px auto;">
  Enter password: 
  <input id="pass" type="password" name="password">
  <input type="submit" value="Submit">
</form>

<p id="result">Result: </p>

</body>
</html>

Above is an example of such implementation. The form uses the onsubmit event handler. If a breached password was entered, the form would not be submitted.

You can try this by clicking the result tab and enter a simple password like ‘123’.

Now try to fat-finger the password field and the form should be submitted to # which leads to the refreshing of the frame.