Roundup Tracker

Issue 2551171 discussed using have I been powned (HIBP) for checking accounts and passwords. This implements a simple way to check passwords. It should help fulfil NIST Special Publication 800-63B - secret verification requirements.

It does not use an API token and doesn't provide a rate limit on the number of requests it does against the HIBP endpoint, so it may fail if loading hibp too frequently.

Adding this function allows checking the entered password for exposure:

   1 import hashlib
   2 import requests
   3 from roundup.exceptions import Reject
   4 
   5 def check_pw_hibp(password, mode="change"):
   6    # encode to unicode string and return a hex encoded sha1 hash
   7    pwhash = hashlib.sha1(password.encode()).hexdigest()
   8    # search hibp using the first 5 hash characters
   9    try:
  10        rqst = requests.get('https://api.pwnedpasswords.com/range/' + pwhash[:5],
  11                         timeout=2.5) # timeout 2.5 seconds
  12    except requests.exceptions.Timeout:
  13        return False # don't let API failure stop login or change
  14    # turn string like FA31ED547EE63D61B812DABE2F37D2AAFE2:2\r\n<another hash>
  15    # into list element of FA31ED547EE63D61B812DABE2F37D2AAFE2
  16    res_hash_list = [ x.split(':')[0] for x in rqst.text.split('\r\n') ]
  17    # hibp returns hash residual after removing first 5 requested chars
  18    # so search for it.
  19    if pwhash.upper()[5:] in res_hash_list:
  20       if mode == 'change':
  21          # reject change of password to the password value
  22          raise Reject("Password has been exposed on have i been powned. ")
  23       elif mode == "login":
  24          # Evaluates to true - have been found on hibp. Allow
  25          # the user to login (and change password) but provide an error message.
  26          return "Warning password has been exposed on have i been powned. Change password"
  27    return False # false == not found in hibp

Using the technique in TestPasswordComplexity and calling this with mode="change" allows you to check it when it gets changed.

You can put this in the login flow using the technique in LoginWithEmail with mode="login" so the password is checked for exposure on every login. Use code like:

   1   r = check_pw_hibp(self.form['__login_password'].value)
   2   if r:
   3      self.client.add_error_message(r)

in the login action wrapper.

Probably checking in both places is a good idea. One makes sure the user isn't starting off with an exposed password and the other verifies that it is still unexposed.

Note that a request to HIBP/api.pwnedpasswords.com that times out will not cause the action (login/change) to fail. I consider this a nice addition to password security but not critical. YMMV.

Also I am not checking other issues reported by the requests library, so it may need some extra exception handling. Updates are welcome.