In [1]:
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
In [2]:
def shift_cipher(plaintext, key):
    """Apply shift cipher to plaintext, 
    shifting each letter by key places in alphabet"""
    ciphertext = []
    
    for i in range(len(plaintext)):
        place_alpha = alphabet.find(plaintext[i])
        shifted_place = (place_alpha + key) % 26
        ciphertext.append(alphabet[shifted_place])
    return ciphertext
In [3]:
"".join(shift_cipher("ATTACKATDAWNZ", 3))
Out[3]:
'DWWDFNDWGDZQC'
In [4]:
# Vigenere cipher
# now key will be a list of numbers 
# This cipher still has some weakness, since there is still some correlation of letters that persists

def vig_cipher(plaintext, key):
    """Apply vigenere cipher to plaintext, shifting letter i by key[i % length(key)] places """
    ciphertext = []
    
    for i in range(len(plaintext)):
        place_alpha = alphabet.find(plaintext[i])
        shifted_place = \
            (place_alpha + key[i % len(key)]) % 26
        ciphertext.append(alphabet[shifted_place])
    return "".join(ciphertext)
In [5]:
vig_cipher("ATTACKATDAWNZ", [3,1,5,25])
Out[5]:
'DUYZFLFSGBBMC'
In [7]:
# to decode, use negative of encoding key
vig_cipher('DUYZFLFSGBBMC', [-3,-1,-5,-25])
Out[7]:
'ATTACKATDAWNZ'
In [10]:
# applying vig_cipher twice
vig_cipher(vig_cipher("ATTACKATDAWNZ", [3,1,5,25]),\
           [2,5,7,4])
Out[10]:
'FZFDHQMWIGIQE'
In [11]:
# decoding the encryption from above
vig_cipher(vig_cipher('FZFDHQMWIGIQE',[-2,-5,-7,-4])\
           ,[-3,-1,-5,-25])
Out[11]:
'ATTACKATDAWNZ'
In [13]:
# the double enciphering done above is equivalent to single enciphering with key the sum of the two keys
vig_cipher("ATTACKATDAWNZ",[5,6,12,29])
Out[13]:
'FZFDHQMWIGIQE'
In [19]:
# double enciphering with two different key lengths is better
vig_cipher(vig_cipher("ATTACKATDAWNZ", [3,1,5,25]),[2,5,7,4,5])
Out[19]:
'FZFDKNKZKGDRJ'
In [24]:
# one-time pad = vigenere cipher with key length equal to
# message length

# This encryption system is completely secure (information theoretically secure),
# provided that the same key is never used to encrypt two messages (hence "one-time")

def one_time_pad(plaintext, key):
    """Shifts character at plaintext[i] by key[i] places.  Key must be at least as long as plaintext"""
    if (len(plaintext) > len(key)):
        print("Error: key size is too small")
        return
    
    ciphertext = []
    
    for i in range(len(plaintext)):
        place_alpha = alphabet.find(plaintext[i])
        shifted_place = (place_alpha + key[i]) % 26
        ciphertext.append(alphabet[shifted_place])
    return "".join(ciphertext)
In [21]:
one_time_pad("DANA",[5,6,12,29])
Out[21]:
'IGZD'
In [25]:
# now encrypt a different message with the same key
one_time_pad("FILE",[5,6,12,29])
Out[25]:
'KOXH'
In [26]:
# concatenating the above two one-time pad ciphertexts is same as
# concatenating the two plaintexts and applying vig_cipher with same key
vig_cipher("DANAFILE", [5,6,12,29])
Out[26]:
'IGZDKOXH'