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 jyevryfNow, 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]) breakNested 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