usrbinenv python3 basic usage arguments ciphertext str the text to decode encoded with the caesar cipher optional arguments cipheralphabet list the alphabet used for the cipher each letter is a string separated by commas frequenciesdict dict a dictionary of word frequencies where keys are the letters and values are a percentage representation of the frequency as a decimalfloat casesensitive bool a boolean value true if the case matters during decryption false if it doesn t returns a tuple in the form of mostlikelycipher mostlikelycipherchisquaredvalue decodedmostlikelycipher where mostlikelycipher is an integer representing the shift of the smallest chisquared statistic most likely key mostlikelycipherchisquaredvalue is a float representing the chisquared statistic of the most likely shift decodedmostlikelycipher is a string with the decoded cipher decoded by the mostlikelycipher key the chisquared test the caesar cipher the caesar cipher is a very insecure encryption algorithm however it has been used since julius caesar the cipher is a simple substitution cipher where each character in the plain text is replaced by a character in the alphabet a certain number of characters after the original character the number of characters away is called the shift or key for example plain text hello key 1 cipher text ifmmp each letter in hello has been shifted one to the right in the eng alphabet as you can imagine this doesn t provide lots of security in fact decrypting ciphertext by bruteforce is extremely easy even by hand however one way to do that is the chisquared test the chisquared test each letter in the english alphabet has a frequency or the amount of times it shows up compared to other letters usually expressed as a decimal representing the percentage likelihood the most common letter in the english language is e with a frequency of 0 11162 or 11 162 the test is completed in the following fashion 1 the ciphertext is decoded in a brute force way every combination of the 26 possible combinations 2 for every combination for each letter in the combination the average amount of times the letter should appear the message is calculated by multiplying the total number of characters by the frequency of the letter for example in a message of 100 characters e should appear around 11 162 times 3 then to calculate the margin of error the amount of times the letter should appear with the amount of times the letter does appear we use the chisquared test the following formula is used let n be the number of times the letter actually appears p be the predicted value of the number of times the letter should appear see 2 let v be the chisquared test result referred to here as chisquared valuestatistic n p2 v p 4 each chi squared value for each letter is then added up to the total the total is the chisquared statistic for that encryption key 5 the encryption key with the lowest chisquared value is the most likely to be the decoded answer further reading http practicalcryptography comcryptanalysistextcharacterisationchisquared statistic https en wikipedia orgwikiletterfrequency https en wikipedia orgwikichisquaredtest https en m wikipedia orgwikicaesarcipher doctests decryptcaesarwithchisquared dof pz aol jhlzhy jpwoly zv wvwbshy pa pz avv lhzf av jyhjr doctest normalizewhitespace 7 3129 228005747531 why is the caesar cipher so popular it is too easy to crack decryptcaesarwithchisquared crybd cdbsxq 10 233 35343938980898 short string decryptcaesarwithchisquared crybd cdbsxq casesensitivetrue 10 233 35343938980898 short string decryptcaesarwithchisquared12 traceback most recent call last attributeerror int object has no attribute lower if the argument is none or the user provided an empty dictionary frequencies of letters in the english language how much they show up custom frequencies dictionary chi squared statistic values cycle through all of the shifts decrypt the message with the shift try to index the letter in the alphabet append the character if it isn t in the alphabet loop through each letter in the decoded message with the shift get the amount of times the letter occurs in the message get the excepcted amount of times the letter should appear based on letter frequencies complete the chi squared statistic formula add the margin of error to the total chi squared statistic get the amount of times the letter occurs in the message get the excepcted amount of times the letter should appear based on letter frequencies complete the chi squared statistic formula add the margin of error to the total chi squared statistic add the data to the chisquaredstatisticvalues dictionary get the most likely cipher by finding the cipher with the smallest chi squared statistic get all the data from the most likely cipher key decoded message return the data on the most likely shift usr bin env python3 basic usage arguments ciphertext str the text to decode encoded with the caesar cipher optional arguments cipher_alphabet list the alphabet used for the cipher each letter is a string separated by commas frequencies_dict dict a dictionary of word frequencies where keys are the letters and values are a percentage representation of the frequency as a decimal float case_sensitive bool a boolean value true if the case matters during decryption false if it doesn t returns a tuple in the form of most_likely_cipher most_likely_cipher_chi_squared_value decoded_most_likely_cipher where most_likely_cipher is an integer representing the shift of the smallest chi squared statistic most likely key most_likely_cipher_chi_squared_value is a float representing the chi squared statistic of the most likely shift decoded_most_likely_cipher is a string with the decoded cipher decoded by the most_likely_cipher key the chi squared test the caesar cipher the caesar cipher is a very insecure encryption algorithm however it has been used since julius caesar the cipher is a simple substitution cipher where each character in the plain text is replaced by a character in the alphabet a certain number of characters after the original character the number of characters away is called the shift or key for example plain text hello key 1 cipher text ifmmp each letter in hello has been shifted one to the right in the eng alphabet as you can imagine this doesn t provide lots of security in fact decrypting ciphertext by brute force is extremely easy even by hand however one way to do that is the chi squared test the chi squared test each letter in the english alphabet has a frequency or the amount of times it shows up compared to other letters usually expressed as a decimal representing the percentage likelihood the most common letter in the english language is e with a frequency of 0 11162 or 11 162 the test is completed in the following fashion 1 the ciphertext is decoded in a brute force way every combination of the 26 possible combinations 2 for every combination for each letter in the combination the average amount of times the letter should appear the message is calculated by multiplying the total number of characters by the frequency of the letter for example in a message of 100 characters e should appear around 11 162 times 3 then to calculate the margin of error the amount of times the letter should appear with the amount of times the letter does appear we use the chi squared test the following formula is used let n be the number of times the letter actually appears p be the predicted value of the number of times the letter should appear see 2 let v be the chi squared test result referred to here as chi squared value statistic n p 2 v p 4 each chi squared value for each letter is then added up to the total the total is the chi squared statistic for that encryption key 5 the encryption key with the lowest chi squared value is the most likely to be the decoded answer further reading http practicalcryptography com cryptanalysis text characterisation chi squared statistic https en wikipedia org wiki letter_frequency https en wikipedia org wiki chi squared_test https en m wikipedia org wiki caesar_cipher doctests decrypt_caesar_with_chi_squared dof pz aol jhlzhy jpwoly zv wvwbshy pa pz avv lhzf av jyhjr doctest normalize_whitespace 7 3129 228005747531 why is the caesar cipher so popular it is too easy to crack decrypt_caesar_with_chi_squared crybd cdbsxq 10 233 35343938980898 short string decrypt_caesar_with_chi_squared crybd cdbsxq case_sensitive true 10 233 35343938980898 short string decrypt_caesar_with_chi_squared 12 traceback most recent call last attributeerror int object has no attribute lower if the argument is none or the user provided an empty dictionary frequencies of letters in the english language how much they show up custom frequencies dictionary chi squared statistic values cycle through all of the shifts decrypt the message with the shift try to index the letter in the alphabet append the character if it isn t in the alphabet loop through each letter in the decoded message with the shift get the amount of times the letter occurs in the message get the excepcted amount of times the letter should appear based on letter frequencies complete the chi squared statistic formula add the margin of error to the total chi squared statistic get the amount of times the letter occurs in the message get the excepcted amount of times the letter should appear based on letter frequencies complete the chi squared statistic formula add the margin of error to the total chi squared statistic add the data to the chi_squared_statistic_values dictionary get the most likely cipher by finding the cipher with the smallest chi squared statistic get all the data from the most likely cipher key decoded message return the data on the most likely shift
from __future__ import annotations


def decrypt_caesar_with_chi_squared(
    ciphertext: str,
    cipher_alphabet: list[str] | None = None,
    frequencies_dict: dict[str, float] | None = None,
    case_sensitive: bool = False,
) -> tuple[int, float, str]:
    
    alphabet_letters = cipher_alphabet or [chr(i) for i in range(97, 123)]

    
    if not frequencies_dict:
        
        frequencies = {
            "a": 0.08497,
            "b": 0.01492,
            "c": 0.02202,
            "d": 0.04253,
            "e": 0.11162,
            "f": 0.02228,
            "g": 0.02015,
            "h": 0.06094,
            "i": 0.07546,
            "j": 0.00153,
            "k": 0.01292,
            "l": 0.04025,
            "m": 0.02406,
            "n": 0.06749,
            "o": 0.07507,
            "p": 0.01929,
            "q": 0.00095,
            "r": 0.07587,
            "s": 0.06327,
            "t": 0.09356,
            "u": 0.02758,
            "v": 0.00978,
            "w": 0.02560,
            "x": 0.00150,
            "y": 0.01994,
            "z": 0.00077,
        }
    else:
        
        frequencies = frequencies_dict

    if not case_sensitive:
        ciphertext = ciphertext.lower()

    
    chi_squared_statistic_values: dict[int, tuple[float, str]] = {}

    
    for shift in range(len(alphabet_letters)):
        decrypted_with_shift = ""

        
        for letter in ciphertext:
            try:
                
                new_key = (alphabet_letters.index(letter.lower()) - shift) % len(
                    alphabet_letters
                )
                decrypted_with_shift += (
                    alphabet_letters[new_key].upper()
                    if case_sensitive and letter.isupper()
                    else alphabet_letters[new_key]
                )
            except ValueError:
                
                decrypted_with_shift += letter

        chi_squared_statistic = 0.0

        
        for letter in decrypted_with_shift:
            if case_sensitive:
                letter = letter.lower()
                if letter in frequencies:
                    
                    occurrences = decrypted_with_shift.lower().count(letter)

                    
                    
                    expected = frequencies[letter] * occurrences

                    
                    chi_letter_value = ((occurrences - expected) ** 2) / expected

                    
                    chi_squared_statistic += chi_letter_value
            else:
                if letter.lower() in frequencies:
                    
                    occurrences = decrypted_with_shift.count(letter)

                    
                    
                    expected = frequencies[letter] * occurrences

                    
                    chi_letter_value = ((occurrences - expected) ** 2) / expected

                    
                    chi_squared_statistic += chi_letter_value

        
        chi_squared_statistic_values[shift] = (
            chi_squared_statistic,
            decrypted_with_shift,
        )

    
    
    def chi_squared_statistic_values_sorting_key(key: int) -> tuple[float, str]:
        return chi_squared_statistic_values[key]

    most_likely_cipher: int = min(
        chi_squared_statistic_values,
        key=chi_squared_statistic_values_sorting_key,
    )

    
    (
        most_likely_cipher_chi_squared_value,
        decoded_most_likely_cipher,
    ) = chi_squared_statistic_values[most_likely_cipher]

    
    return (
        most_likely_cipher,
        most_likely_cipher_chi_squared_value,
        decoded_most_likely_cipher,
    )