About commenting and readability
Some instructors like to insist on commenting every line of code. Hopefully your instructor is not one of those people, since commenting every line of code is a harmful practice that doubles the amount of code and makes it harder to read and maintain.
The most important kind of comment is the docstring, and you didn't write any. There should be one docstring for the program, and one for every function. Furthermore, you should strive to package as much of your code in functions as possible. That forces you to structure your code into small chunks, each with a specific documented purpose, and with obvious inputs and outputs.
You've defined a numberToBase()
function, which is good. But it is buried in a sea of free-floating code, which is bad.
You started off by defining 7 variables, which could potentially be used and altered anywhere in your program. That makes your code hard to understand. n
, in particular, would be better named attempts
, both to eliminate the need for a comment and to avoid confusion with the n
in numberToBase(n, b)
.
Benchmarking
What exactly are you measuring? If the user takes a long time to respond to the prompt, do you really want that to be included in the elapsed time?
Furthermore, a huge amount of the password-cracking time is spent in printing the words being tried. Do you care more about the cracking speed, or the time it takes for your terminal to render text?
Here's one kind of comment that would actually useful to future maintainers of your code:
# Disabled printing, because the cracking would go much slower!
# print(word)
Suggested solution
This solution uses illustrates how a readable, well documented program should be written.
"""
Brute-force password-cracking demonstration.
"""
import string
from time import time
def nth_possible_word(n, chars=string.printable):
"""
Generate the nth possible string composed of the given characters,
in order of increasing length. (By default, use the printable ASCII
characters.)
>>> nth_possible_word(0, chars='abc')
''
>>> nth_possible_word(1, chars='abc')
'a'
>>> [nth_possible_word(n, chars='ab') for n in range(10)]
['', 'a', 'b', 'aa', 'ab', 'ba', 'bb', 'aaa', 'aab', 'aba']
"""
word = ''
while n:
n -= 1
word += chars[n % len(chars)]
n //= len(chars)
return word[::-1]
def crack(password, max_attempts=10**9):
"""
Try all possible strings of increasing length until one of them matches
the given password, or max_attempts strings have been tried. Return a
tuple (word, n), where word is the password that was found (or None if
the password was not found), and n is the number of attempts made.
>>> crack('a')
('a', 12)
>>> crack('xyz')
('xyz', 343537)
>>> crack('xyz', 343537)
('xyz', 343537)
>>> crack('xyz', 343536)
(None, 343536)
"""
for attempt in range(max_attempts):
word = nth_possible_word(attempt)
# Disabled printing, because the cracking would go much slower!
# print(word)
if password == word:
return word, attempt + 1
return None, attempt + 1
def main():
password = input("Enter your password: ")
# print(string.printable)
start_time = time()
word, attempts = crack(password)
elapsed_secs = time() - start_time
if word is None:
print('Unsolved after {0} attempts!'.format(attempts))
elif word == '':
print('Your password is empty')
else:
print("""***Results***
Password: {0}
Attempts: {1}
Time: {2} sec""".format(word, attempts, elapsed_secs))
if __name__ == '__main__':
main()
Observations:
- The
main()
function is relatively short. You can get a feeling of what this program does by looking at it, without getting lost in the details. In particular, all of the input/output happens in main()
; all of the "calculations" happen within other functions.
- The code within the
nth_possible_word()
and crack()
functions might be difficult for a beginner to understand, but the docstrings clarify how the functions behave. You only look at the implementation when you need to.
Empty input is treated as a special case only for reporting (to replicate your output). For the purposes of cracking, a zero-length password isn't all that special.
By the way, your elif password == chars[n]:
special case was pretty pointless. It would only get activated if the password is '0'
.
Alternate solution
In a sense, your numberToBase()
technique is a way to iterate through all of the possible passwords to try. By using some Python features such as itertools
and yield
), you can iterate through all possible strings more expressively. If you replace the nth_possible_word()
and crack()
functions in the program above with the two functions below, the program will behave the same.
from itertools import count, islice, product
def passwords_to_try(chars=string.printable):
"""
Generate an infinite sequence of all possible strings composed of the
given characters, in order of increasing length. (By default, the
strings consist of printable ASCII characters.)
>>> p = passwords_to_try()
>>> next(p)
''
>>> next(p)
'0'
>>> next(p)
'1'
"""
for length in count():
for characters in product(chars, repeat=length):
yield ''.join(characters)
def crack(password, max_attempts=10**9):
"""
Try all possible strings of increasing length until one of them matches
the given password, or max_attempts strings have been tried. Return a
tuple (word, n), where word is the password that was found (or None if
the password was not found), and n is the number of attempts made.
>>> crack('a')
('a', 12)
>>> crack('xyz')
('xyz', 343537)
>>> crack('xyz', 343537)
('xyz', 343537)
>>> crack('xyz', 343536)
(None, 343536)
"""
for n, word in enumerate(islice(passwords_to_try(), max_attempts), 1):
# Disabled printing, because the cracking would go much slower!
# print(word)
if password == word:
return word, n
return None, n