Have You Been Pwned?


~4 minutes to read

Table of contents


Introduction

Have I Been Pwned? has a very simple and accessible API that’s perfect to work with as a beginner. There are different ways to retrieve information and the script below is only one glimpse into the numerous possibilities available. It is built in a way that makes it easy to add features and functionality thanks to Click, a wonderful “command line interface toolkit”.

Python to the rescue

The following script uses Click to build a tiny command-line interface instead of more traditional tools like argparse. You will also need requests to peek inside the API. Both can be obtained as follow in a terminal:

$ pip install click requests

And now, the pièce de résistance.

"""
Simple script that will take advantage of haveibeenpwned.com's API to
find out if your password has been breached.
"""
# Standard library imports
import hashlib

# Third party library imports
import click
import requests


# Needed to access API
PASS_URL = 'https://api.pwnedpasswords.com/range/'

# Script information
VERSION = '0.1.1'
LOGO = r'''
 __                        __   __
|  |--.---.-.--.--.-----. |__| |  |--.-----.-----.-----.
|     |  _  |  |  |  -__| |  | |  _  |  -__|  -__|     |
|__|__|___._|\___/|_____| |__| |_____|_____|_____|__|__|

                               __  _____
.-----.--.--.--.-----.-----.--|  ||__   |
|  _  |  |  |  |     |  -__|  _  |',  ,-'
|   __|________|__|__|_____|_____| |--|
|__|                               '--'  '''


def print_info(context, param, value):  # `param` required by `click`
    """Print information about the program and exit."""
    if not value or context.resilient_parsing:
        return
    click.secho(f'{LOGO}\n{"Python 3.6+ Checker".rjust(60)}\n'
                f'{("v" + VERSION).rjust(60)}', fg='blue', bold=True)
    context.exit()


def pass_to_sha1(password):
    """Will convert `password` with SHA-1 algorithm and return the short
    version with the first five characters and the long version with
    everything but the first five characters as a tuple."""

    my_password = password.encode()  # converts to byte string

    complete_hash = hashlib.sha1(my_password).hexdigest().upper()
    long_hash = complete_hash[5:]
    short_hash = complete_hash[:5]

    return short_hash, long_hash


def check_password(short_hash, long_hash):
    """Consult haveibeenpwned.com to see how many times the password has
    been breached."""
    click.secho('Please note that only the 5 first characters from your '
                'SHA-1 ENCRYPTED password are sent to haveibeenpwned.com.',
                fg='yellow', bold=True)
    response = requests.get(PASS_URL + short_hash)
    lines = response.text.split()
    for line in lines:
        if long_hash in line:
            num_times = 'occurrences' if int(line[36:]) > 1 else 'occurence'
            click.secho('Your password was found!', fg='red', bold=True)
            click.echo(f'→ {line[36:]} {num_times}')
            break
    else:
        click.secho('Your password was NOT found!', fg='green')


@click.group()
@click.option(
    '--info',
    is_flag=True,
    callback=print_info,
    expose_value=False,
    is_eager=True,
    help="Print some information about the program and exit.")
@click.version_option(version=VERSION, message="%(version)s")
def hibp():
    """A simple command-line interface to make use of
    haveibeenpwned.com's data.

    Type `python3 pwned.py usage` for more info (or whatever way you
    call Python 3)."""


@hibp.command()
def usage():
    """Give examples on how to use this script."""
    click.secho('Examples:', fg='blue')
    click.secho('python3 pwned.py check --help for more info.', fg='green')
    click.secho('python3 pwned.py check -p MyPasswordHere', fg='yellow')
    click.secho('python3 pwned.py check --password MyPasswordHere', fg='green')

    click.echo('')
    click.secho('For more complicated passwords, you have to use quotes and ',
                fg='blue')
    click.secho('escape symbols with \\ where appropriate:', fg='blue')
    click.secho('python3 pwned.py check -p "as0d9\\"asg0\'\'A=)SYD"',
                fg='green')


@hibp.command()
@click.option(
    '-p',
    '--password',
    default=None,
    help="Reveal if match exists with `password`.")
def check(password):
    """Tell if your password has been breached."""
    if password:
        short_hash, long_hash = pass_to_sha1(password)
        check_password(short_hash, long_hash)
    if not password:
        click.secho('Run `pwned.py check --help` for more info.', fg='red')


if __name__ == '__main__':
    hibp()

Example usage

have_i_been_pwned_script

Conclusion

If you are familiar with Python, you can easily make sense of how this little tool works by reading its code. You can also have a look at this repository on GitHub if you want to fork it or reuse it any other way you wish. I have personally found it much easier to work with Click instead of argparse and optparse. I haven’t got to try docopt yet, but it is almost equally as popular as Click on GitHub at the time of this writing and its Pythonic way makes it another great option to consider.