def crack(ciphertext, *fitness_functions, min_key=0, max_key=26): """Break ``ciphertext`` by enumerating keys between ``min_key`` and ``max_key``. Example: >>> decryptions = crack("KHOOR", fitness.english.quadgrams) >>> print(decryptions[0]) HELLO Args: ciphertext (str): The text to decrypt *fitness_functions (variable length argument list): Functions to score decryption with Keyword Args: min_key (int): Key to start with max_key (int): Key to stop at (exclusive) Returns: Sorted list of decryptions Raises: ValueError: If min_key exceeds max_key ValueError: If no fitness_functions are given """ if min_key >= max_key: raise ValueError("min_key cannot exceed max_key") decryptions = [] for key in range(min_key, max_key): plaintext = decrypt(key, ciphertext) decryptions.append( Decryption(plaintext, key, score(plaintext, *fitness_functions))) return sorted(decryptions, reverse=True)
def next_node_inner_climb(node): # Swap 2 characters in the key a, b = random.sample(range(len(node)), 2) node[a], node[b] = node[b], node[a] plaintext = decrypt(node, ciphertext) node_score = score(plaintext, *fitness_functions) return node, node_score, Decryption(plaintext, ''.join(node), node_score)
def crack(ciphertext, *fitness_functions, key_period=None, max_key_period=30): """Break ``ciphertext`` by finding (or using the given) key_period then breaking ``key_period`` many Caesar ciphers. Example: >>> decryptions = crack("OMSTV", fitness.ChiSquared(analysis.frequency.english.unigrams)) >>> print(decryptions[0]) HELLO Args: ciphertext (str): The text to decrypt *fitness_functions (variable length argument list): Functions to score decryption with Keyword Args: key_period (int): The period of the key max_key_period (int): The maximum period the key could be Returns: Sorted list of decryptions Raises: ValueError: If key_period or max_key_period are less than or equal to 0 ValueError: If no fitness_functions are given """ if max_key_period <= 0 or (key_period is not None and key_period <= 0): raise ValueError("Period values must be positive integers") original_text = ciphertext # Make the assumption that non alphabet characters have not been encrypted # TODO: This is fairly poor code. Once languages are a thing, there should be some nice abstractions for this stuff ciphertext = remove(ciphertext, string.punctuation + string.whitespace + string.digits) periods = [int(key_period)] if key_period else key_periods(ciphertext, max_key_period) # Decrypt for every valid period period_decryptions = [] for period in periods: if period >= len(ciphertext): continue # Collect the best decryptions for every column column_decryptions = [shift.crack(col, *fitness_functions)[0] for col in split_columns(ciphertext, period)] key = _build_key(decrypt.key for decrypt in column_decryptions) plaintext = decrypt(key, original_text) period_decryptions.append(Decryption(plaintext, key, score(plaintext, *fitness_functions))) return sorted(period_decryptions, reverse=True)
def test_score_with_single_function(): """Single function accepted instead of iterable""" assert score("example", lambda _: 15) == 15
def test_score_invalid(): """Score raises ValueError when given no scoring functions""" with pytest.raises(ValueError): score("example")
def test_score_is_averaged_positive_and_negative(): """Score is averaged over number of functions used""" assert score("example", lambda _: -10, lambda _: 2) == -4