Known Plaintext Attacks Are Caesar-ous Business

by snooze

In my first article published in 2600, I provided a simple way of implementing the Vigenère cipher in Python.

I will pick up this discussion on how laughably easy it is to undermine the security of Vigenère, the once thought-to-be uncrackable cryptosystem.

So, what is a known plaintext attack?  It is essentially a way to derive a secret key when a plaintext and its subsequent ciphertext are known to the cryptanalyst.  I have hardcoded a key specifically for this article, created a simple plaintext, and used the rotateChar function from the code above to achieve the following:

$ python3 ./vigcipher.py
Enter your plaintext to be encrypted: twenty six hundred
Enter your alphabetical key; exits on invalid character: saucer
Ciphertext: lwypxp kir jyevryf

Now, let's pretend we don't actually know that the secret key is saucer.

How can we get from l in the ciphertext to t in the plaintext?  w to w, y to e, so on and so forth?  Quite simple, actually.

We can iterate through our string character by character and use the rotateChar function, providing a single character "secret key" starting from 0 and continuing through 25.

Doing this allows us to account for all letters in our alphabet.  If a match between the plaintext index and the rotated ciphertext index is found, we append the character in our alphabet that resides at the secret key index.  We will keep these characters in a list called rotList.

If the current character of our string is not in our alphabet, a space for example, we simply append it to rotList.

# alpha is equal to "abcdefghijklmnopqrstuvwxyz"
rotList = []
for i in range(len(plainText)):
 for j in range(0, 26):
  if plainText[i] in alpha:
   if rotateChar(plainText[i], j) == cipherText[i]:
    rotList.append(alpha[j])
    break
  else:
   rotList.append(plainText[i])
   break

Nested for loops are computationally intensive and thus larger key sizes would make this take a long time.  However, with an all lowercase key with a length of six characters, this is cracked in less than a second on my aging laptop.

Simply printing out rotList at the end gives us the following:

Plaintext: twenty six hundred
Ciphertext: lwypxp kir jyevryf
Attempting brute force....
['s', 'a', 'u', 'c', 'e', 'r', ' ', 's', 'a', 'u', ' ', 'c', 'e', 'r', 's', 'a', 'u', 'c']

And thus, Vigenère is defeated.

Until next time!

# See blog post at https://snoozesecurity.blogspot.com/2020/12/ill-take-some-vigenere-with-my-caesar.html

from itertools import cycle
import string
alpha = string.ascii_lowercase

plainText = input("Enter your plaintext to be encrypted: ")
userKey = input("Enter your alphabetical key; exits on invalid character: ").lower()
cipherText = ''
cycKey = cycle(userKey)

# Caesar/ROT Function

def rotateChar(s: str, rotate: int):
  out = ''
  boolUpper = s.isupper()
  s = s.lower()
  if s not in alpha:
    out = s
  elif s in alpha and alpha.index(s) + rotate > 25:
    if boolUpper:
      out = alpha[((alpha.index(s) + rotate) - 25) - 1].upper()
    else:
      out = alpha[((alpha.index(s) + rotate) - 25) - 1]
  else:
    if boolUpper:
      out = alpha[alpha.index(s) + rotate].upper()
    else:
      out = alpha[alpha.index(s) + rotate]
  return out

# Check validity of key; for demonstration purposes I only accept alphabet characters

for char in userKey:
  if char.lower() not in alpha:
    print("Invalid key; quitting.")
    quit()

# Create nested list(s) with the proper ROT number for each string in the plaintext

refList = []

for char, rot in zip([char for char in plainText if char.lower() in alpha], cycKey):
  if char.lower() in alpha:
    refList.append([char, alpha.index(rot)])

# Iterate through original plaintext and rotate when a legal character is at index 0 of refList then pop index 0.

for char in plainText:
  if refList and char == refList[0][0]:
    cipherText += rotateChar(char, refList[0][1])
    refList.pop(0)
  else:
    cipherText += char

print("Ciphertext:", cipherText)

Code: vigcipher.py

Return to $2600 Index