2.8 MORE HASH, MORE TIME

EXERCISE 2.8 MORE HASH, MORE TIME

Choosing a complex-to-invert password is the responsibility of the user, but the systems storing the passwords can also slow down attackers by using a more complicated hashing function. Repeat any of the preceding exercies that use MD5, but now use SHA-1 and SHA-256 instead. Record how much longer it takes to get through the brute-force operations. Finally, try out bruteforce using scrypt. You might not get very far!


I will use exercise 2.5 by changing the hash function from MD5 to SHA-1, SHA-256, and Scrypt.

# ex2_8.py 

import hashlib
import secrets
import timeit
from string import ascii_lowercase
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidKey 

def h_sha1(x: str) -> str:
    '''returns the sha1 digest of the string x.'''
    return hashlib.sha1(x.encode('utf-8')).hexdigest()

def h_sha256(x: str) -> str:
    '''returns the sha256 digest of the string x.'''
    return hashlib.sha256(x.encode('utf-8')).hexdigest()

def h_scrypt(x: str) -> dict[str,bytes]:
    '''returns the Scrypt digest of the string x.'''
    salt = secrets.token_bytes(16)
    kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1, backend=default_backend())
    key = kdf.derive(x.encode('utf-8'))
    return {
        "salt": salt, 
        "key": key, 
    }

def h_scrypt_verify(x: str, salt: bytes, key: bytes):
    kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1, backend=default_backend())
    kdf.verify(x.encode('utf-8'), key)

hashing_functions = {
    "sha1": h_sha1, 
    "sha256": h_sha256, 
    "scrypt": h_scrypt, 
}

def single_run(hashing_function_name: str):
    h = hashing_functions[hashing_function_name]

    preimage_seed = secrets.choice(ascii_lowercase)
    test_hash : dict[str,bytes] | str = h(preimage_seed)

    for single_letter in ascii_lowercase: 
        if hashing_function_name in ['sha1', 'sha256']:
            if h(single_letter) == test_hash: 
                # found a match
                break
        elif hashing_function_name == "scrypt": 
            try:
                # we know that test_hash is a dict[str, bytes].
                h_scrypt_verify(single_letter, **test_hash)
                break
            except InvalidKey:
                pass 
    else:
        # Executes when the above loop terminates through exhaustion of the iterable. 
        # But not when the loop is terminated by a break statement. 
        raise AssertionError("the above loop should always break out!!")    
    return 

if __name__ == '__main__':
    COUNTER = 10
    for hashing_function_name in hashing_functions.keys():
        total_execution_time = timeit.timeit(
            stmt=f"single_run('{hashing_function_name}')", 
            setup="from __main__ import single_run", 
            number=COUNTER, 
        )
        print(f"Called single_run('{hashing_function_name}') {COUNTER} times. Total execution time (in seconds): {total_execution_time} sec")
        print(f"Average execution time: {total_execution_time/COUNTER} sec")
        print("\n--------------------------------\n")

When COUNTER \(= 10\):

When COUNTER \(= 30\):