# crackie

A simple, silly library to generate combinations.

I wrote this when I forgot part of a password, and decided to try run ~500,000
combinations on the parts I remembered. It worked! Also I decided to publish it
because I had never created a Python package before, so this would learn me
something.

## Usage

Say you have forgotten a password, but remember the basics of it. For example,
you remember that it was made up of two words, with some characters altered.

Let's see how to use this library to try all potential combinations:

```python
# Of course we start with some imports.

# We'll be using `each_possible_combination`
from crackie import each_possible_combination

# And we'll use an external program to test each individual
# combination. We'll call this program with `Popen`.
from subprocess import Popen, DEVNULL

# I forgot my password! I remember it was made up of
# the words "verd" and "uberous". Not sure how I mixed up
# lower and upper case... oh, and there might have been
# some additional spicy characters. It could be `VERDUBEROUS`,
# `V3rd_ubEr0U5`, `v3rd ub3r0U5!`... something along those
# lines.

# This list of lists models the possibilities for each
# character in the password string. First there's a "V",
# not sure if big or small. Then the same with "E", but
# this could instead be a "3"? Etc.
variations = [
    ["V", "v"],
    ["E", "e", "3"],
    ["R", "r"],
    ["D", "d"],
    ["", "-", "_", " "],
    ["U", "u"],
    ["B", "b", "8"],
    ["E", "e", "3"],
    ["R", "r"],
    ["O", "o", "0"],
    ["U", "u"],
    ["S", "s", "5", "$"],
    ["", "!", "1"],
]

# Provide a way to test each individual combination. In this
# example we want to find out a gpg key, so here's some code
# to make that work. In your case it'll be completely different.
# Or not.
def try_password(password):
    p = Popen(
        ['gpg', '--pinentry-mode', 'loopback', '--decrypt', '--passphrase', password, 'e2e_tests/tmp/cryptext'],
        stderr=DEVNULL,
        stdout=DEVNULL
    )
    p.wait()
    return p.returncode == 0

# Some printing out to reassure us that the program
# is running. The `count` will increment as we go
# and be shown at the end.
count = 0
print("Testing combinations...")

# This is the actual search for combinations, using
# `each_possible_combination`.
for candidate in each_possible_combination(variations):
    # Print out some dots to show progress
    print(".", end="", flush=True)
    count += 1

    # Each variation is given as a list. We'll need it
    # as a string.
    candidate_string = ''.join(candidate)

    # Finally, we check if the combination is good!
    # If not, the loop will move on to the next one.
    if try_password(candidate_string):
        # On success, announce victory and exit.
        print(f"\nFound the password: `{candidate_string}`. Tried {count} combinations");
        break
```

## Development

This project uses [uv](https://docs.astral.sh/uv/getting-started/installation/).
End-to-end tests use Docker.

There is a Makefile with shortcuts to run the tests, as follows:

```sh
$ make # Runs pytests, doctests, and end-to-end tests
$ make test # Runs pytests and doctests
$ make test_e2e # Runs end-to-end tests
```

The so-called "e2e test" extracts the example shown above in this document,
and runs it to make sure it reflects reality. It sets up a Docker container
with the required tools (GnuPG) and an old-ish version of Python (to
ensure compatibility). Perhaps overkill, but for some time the example had
some mistakes and I wanted to ensure it didn't happen again.
