Steven Hale
Steven Hale

Reputation: 226

Struggling with ASCII Loop for Caesar Cipher

This is my first question posted to stackoverflow, any and all help/critique/assistance is appreciated...I need all the help I can get haha.

I am very new to programming.

The aim is to create a Caesar cipher that encrypts and decrypts user string input to an ord, adds the user input offset_value, and then changes it back to a char.

I am using ASCII characters. The issue is that I need to isolate the encryption and decryption to ASCII 32 ('a') - ASCII 126 ('~'). I'm not sure how to create a function that circles back through the 94 characters.

So, for example, if the char is 'Z' which is ASCII ord 94, if we add the user input offset_value, which could be 90 that would make the ord 184. Which is outside the range.

This leads into the real problem, the brute force encryption. It works...sort of. It needs to display every possible result with the offset_value changing between 1 and 94. For example, what happens if we decrypt each letter with an offset_value of x (x being any number from 1-94).

Instead it just keeps going up and up.

Does any of this make sense?

My code is below. I know I haven't created any functions yet, but I will.

Thanks in advance guys.

choice = 0
list1 = [1, 2, 3, 4]
list2 = list(range(1, 95))
new_ord = 0
index = 1
encryption = ''
decryption = ''
offset_value = 1


#while loop allows for multiple use, option 4 ends loop
while choice != 4:
    print('*** Menu ***')
    print('\r')
    print('1. Encrypt string')
    print('2. Decrypt string')
    print('3. Brute force decryption')
    print('4. Quit')
    print('\r')
    choice = int(input('What would you like to do [1,2,3,4]? '))

    #invalid user input loop, valid entry ends loop
    while choice not in list1:
        print('\r')
        print('Invalid choice, please enter either 1, 2, 3 or 4.')
        print('\r')
        choice = int(input('What would you like to do [1,2,3,4]? '))

    #user chooses 'encrypt string', stores data
    if choice == 1:
        print('\r')
        string_to_encrypt = str(input('Please enter string to encrypt: '))
        offset_value = int(input('Please enter offset value (1 to 94): '))

        #invalid user input loop, valid entry ends loop
        while offset_value not in list2:
            offset_value = int(input('Please enter offset value (1 to 94): '))

        #encryption loop for length of string_to_encrypt
        for letter in string_to_encrypt:
            encryption = encryption + chr((ord(letter) + offset_value))

        #prints encrypted string
        print('\r')
        print('Encrypted string:')
        print(encryption)
        print('\r')

        #clears ecryption data
        encryption = ''

    #user chooses 'decrypt string', stores data
    elif choice == 2:
        print('\r')
        string_to_decrypt = str(input('Please enter string to decrypt: '))
        offset_value = int(input('Please enter offset value (1 to 94): '))

        #invalid user input loop, valid entry ends loop
        while offset_value not in list2:
            offset_value = int(input('Please enter offset value (1 to 94): '))

        #decryption loop for length of string_to_decrypt
        for letter in string_to_decrypt:
                decryption = decryption + chr((ord(letter) - offset_value))

        #prints decrypted string
        print('\r')
        print('Decrypted string:')
        print(decryption)
        print('\r')

        #clears decryption data
        decryption = ''

    #user chooses 'Brute Force Decryption
    elif choice == 3:
        string_to_decrypt = str(input('Please enter string to decrypt: '))
        for number in range(94):
            for letter in string_to_decrypt:
                decryption = decryption + chr((ord(letter) - offset_value))
            print('Offset: ', index, '= Decrypted String: ', decryption)
            offset_value = offset_value + 1
            index = index + 1
            decryption = ''

    #user chooses 'quit'
        print('Goodbye.')

Upvotes: 2

Views: 3813

Answers (3)

martineau
martineau

Reputation: 123443

Rather than rewrite your program for you, I'm going to describe how to tackle a problem like this, which is mostly involves doing a little math.

The essence of your question boils down to how to offset a given range of integer values a given amount and wrapping the results so they're all still within the original range of values.

This can be done mathematically for each value in the range by subtracting the minimum value in the range, adding the offset to that, calculating the sum modulo the difference between the high and low values of the range (+1), and then finally adding the minimum value back to compensate for initially subtracting it. The crucial part is the modulo % operation to constrain the sum.

The code in the offset_range() function below shows how to do all that inside a list comprehension which is a shorthand way to create lists in Python.

That and the print_range() function also serve to illustrate how functions get defined in Python.

def print_range(name, range_):
    """ Pretty-print given range of character ordinals. """
    ints = ', '.join('{:3d}'.format(val) for val in range_)
    chrs = ', '.join('{:>}'.format(repr(chr(val))) for val in range_)
    print('{}: [{}]'.format(name, ints))
    print('{}  [{}]'.format(len(name)*' ', chrs))

def offset_range(offset_value, range_):
    """ Add offset value to integers in given range and wrap result. """
    min_val, max_val = range_[0], range_[-1]  # Get upper and lower limits.
    diff_plus_1 = (max_val - min_val) + 1  # Their difference + 1 for computation.
    # Use a list comprehension to create new list.
    offset_range = [(((val - min_val) + offset_value) % diff_plus_1) + min_val
                        for val in range_]
    return offset_range

ascii_range = list(range(ord('a'), ord('~')+1))
print_range('ascii_range', ascii_range)

print()
offset_range = offset_range(1, ascii_range)
print_range('offset_range', offset_range)

Output:

ascii_range: [ 97,  98,  99, 100, 101, 102, 103, 104, 105, ... 122, 123, 124, 125, 126]
             ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', ... 'z', '{', '|', '}', '~']

offset_range: [ 98,  99, 100, 101, 102, 103, 104, 105, ... 122, 123, 124, 125, 126,  97]
              ['b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', ... 'z', '{', '|', '}', '~', 'a']

Upvotes: 2

Paul M.
Paul M.

Reputation: 10799

Some solutions using ordinal % len(alphabet) have been offered already. I want to present a slightly different solution. The whole solution revolves around str.maketrans, which can provide an easy way of implementing ciphers.

If you pass in two equal-length strings into str.maketrans, it returns a dictionary which maps the ordinal values of the characters in the first, to the ordinal values of the characters in the second in the order in which they appear.

For example:

>>> str.maketrans("ABC", "DEF")
{65: 68, 66: 69, 67: 70}
>>> 

Uppercase 'A' (65) -> Uppercase 'D' (68). Etc. You can already see how this could be used to implement your Caesar-cipher - as long as you can generate two strings of equal-length, where the second string is offset by some amount from the first one (and then wraps around to the beginning), you can easily generate one of these translation tables (the documentation calls it that in several places, but it's just a plain old Python dictionary) and have it do most of the heavy lifting.

My solution:

get_alphabet, given a minimum and maximum ordinal, returns a string containing all the characters in the desired alphabet.

get_offset_alphabet, given an alphabet (string) and an offset (integer), returns the passed in alphabet but offset and wrapped. This is achieved by creating an endless character-iterator using itertools.cycle, slicing off and discarding some amount from the beginning of the iterator depending on the offset, and then constructing an equal-length string from the next n-characters from the iterator (where 'n' is the length of the original alphabet). This also supports negative offsets.

In the main, we get our two alphabets - the first is unaltered and the second is offset and wrapped. We create a translation table (dictionary), assert that all the characters in our plaintext can be mapped to some other character according to our translation table, and then perform the actual translation to yield the ciphertext. str.maketrans also has a third optional parameter which can be used to map characters which have no mapping to None, which I didn't take advantage of here.

def get_alphabet(min_ordinal, max_ordinal):
    assert isinstance(min_ordinal, int)
    assert isinstance(max_ordinal, int)
    assert 0 <= min_ordinal <= 255
    assert 0 <= max_ordinal <= 255
    assert min_ordinal <= max_ordinal
    return "".join(map(chr, range(min_ordinal, max_ordinal+1)))

def get_offset_alphabet(alphabet, *, offset):
    assert isinstance(alphabet, str)
    assert isinstance(offset, int)
    assert alphabet

    from itertools import cycle, islice
    char_iterator = cycle(alphabet)
    _ = list(islice(char_iterator, [offset, len(alphabet)+offset][offset<0]))
    return "".join(islice(char_iterator, len(alphabet)))

def main():

    from_alphabet = get_alphabet(32, 126)
    to_alphabet = get_offset_alphabet(from_alphabet, offset=1)

    translation = str.maketrans(from_alphabet, to_alphabet)

    plaintext = "Hello World"
    assert all(ord(char) in translation for char in plaintext)

    ciphertext = plaintext.translate(translation)
    print(ciphertext)

    return 0


if __name__ == "__main__":
    import sys
    sys.exit(main())

Upvotes: 1

Maarten Bodewes
Maarten Bodewes

Reputation: 93958

The Caesar cipher uses an alphabet with an index starting at 0. So you need some function to convert one from the range 32 to 126 to 0 to 126 - 32 = 98. Then you need to perform calculations mod 99 as there are 99 elements with index 0 to 98.

Generally, the idea of programming is to conquer by dividing the issue into multiple functions. Most answers already will assume that you are fluent and do it one liners. However, this will generally only confuse you, and let you learn how not to program.

So below I've written out the encryption procedure. Now if you create the decryption procedure then you can re-use the decrypt method for your brute force attempt. You'll see that it is now much easier to do things correctly.

LO_CHAR = ord('A')
HI_CHAR = ord('~')
CHARS = HI_CHAR - LO_CHAR + 1

def char_to_index(char):
    o = ord(char)
    if (o < LO_CHAR or o > HI_CHAR):
       raise
    return o - LO_CHAR

def index_to_char(index):
    if (index < 0 or index >= CHARS):
       raise
    return chr(index + LO_CHAR)

def encrypt_index(key, index):
    return (index + key) % CHARS

def encrypt(key, plaintext):
    ciphertext = ''
    for plain_char in plaintext:
        plain_index = char_to_index(plain_char)
        cipher_index = encrypt_index(key, plain_index)
        cipher_char = index_to_char(cipher_index)
        ciphertext = ciphertext + cipher_char
    return ciphertext

... happy programming ...

Upvotes: 1

Related Questions