Hey everyone,
It's been a couple of weeks since our first look into cryptography with the simple Caesar and ROT13 ciphers. Today, we're taking the next step up in complexity and exploring a couple of classic polyalphabetic ciphers. Unlike the Caesar cipher which uses a single, fixed shift, these ciphers use a keyword to create multiple different shifts, making them much more secure.
The Vigenère cipher is a method of encrypting text by using a series of Caesar ciphers based on the letters of a keyword. Each letter in the plaintext is shifted forward for encryption or backward for decryption by the position of the corresponding key letter.
To see how this works in practice, let's walk through an example by hand.
THISISATEST
KEY
Step 1: Assign numbers 0-25 to the alphabet.
Letter | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Number | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Step 2: Align the repeated key with the plaintext.
Plaintext: T H I S I S A T E S T
Key: K E Y K E Y K E Y K E
Step 3: Convert letters to numbers.
19, 7, 8, 18, 8, 18, 0, 19, 4, 18, 19
10, 4, 24, 10, 4, 24, 10, 4, 24, 10, 4
Step 4: Add the numbers and take the result modulo 26.
Position | Plaintext Num | Key Num | Sum | Sum mod 26 | Encrypted Letter |
---|---|---|---|---|---|
1 | 19 | 10 | 29 | 3 | D |
2 | 7 | 4 | 11 | 11 | L |
3 | 8 | 24 | 32 | 6 | G |
4 | 18 | 10 | 28 | 2 | C |
5 | 8 | 4 | 12 | 12 | M |
6 | 18 | 24 | 42 | 16 | Q |
7 | 0 | 10 | 10 | 10 | K |
8 | 19 | 4 | 23 | 23 | X |
9 | 4 | 24 | 28 | 2 | C |
10 | 18 | 10 | 28 | 2 | C |
11 | 19 | 4 | 23 | 23 | X |
The final encrypted text is DLGCMQKXCCX
. Now, let's see how we can automate this process with a simple Python script.
vigenere.py
#!/usr/bin/env python3
import argparse
def vigenere_cipher(text, key, decrypt=False):
"""
Encrypts or decrypts a given text using the Vigenere cipher with a given key.
Handles both uppercase and lowercase letters, preserving case.
Non-letter characters are left unchanged.
:param text: The text to be encrypted or decrypted.
:param key: The key to be used for encryption or decryption.
:param decrypt: A boolean indicating whether to decrypt the text (default is False).
:return: The encrypted or decrypted text.
"""
if not text or not key:
raise ValueError("Text and key cannot be empty.")
result = []
key_idx = 0
for char in text:
if char.isupper():
base = ord("A")
key_char = key[key_idx % len(key)].upper()
shift = ord(key_char) - base
if decrypt:
new_char = chr((ord(char) - base - shift) % 26 + base)
else:
new_char = chr((ord(char) - base + shift) % 26 + base)
result.append(new_char)
key_idx += 1
elif char.islower():
base = ord("a")
key_char = key[key_idx % len(key)].lower()
shift = ord(key_char) - base
if decrypt:
new_char = chr((ord(char) - base - shift) % 26 + base)
else:
new_char = chr((ord(char) - base + shift) % 26 + base)
result.append(new_char)
key_idx += 1
else:
result.append(char)
return "".join(result)
def main():
parser = argparse.ArgumentParser(description="Vigenère Cipher")
parser.add_argument("text", help="Text to encrypt/decrypt")
parser.add_argument("key", help="Key")
parser.add_argument(
"--decrypt", action="store_true", help="Decrypt instead of encrypt"
)
args = parser.parse_args()
try:
result = vigenere_cipher(args.text, args.key, args.decrypt)
print(f"Result: {result}")
except ValueError as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
A clever variant of the Vigenère is the Beaufort cipher. The most interesting thing about it is that it's reciprocal, which means the exact same process is used for both encryption and decryption. Instead of adding the key to the plaintext like in Vigenère, it subtracts the plaintext from the key.
The setup is the same, but the formula changes.
THISISATEST
KEY
Step 1-3: Set up the numbers just like before.
19, 7, 8, 18, 8, 18, 0, 19, 4, 18, 19
10, 4, 24, 10, 4, 24, 10, 4, 24, 10, 4
Step 4: Subtract the plaintext number from the key number, modulo 26.
Position | Plaintext Num | Key Num | Difference | Diff mod 26 | Encrypted Letter |
---|---|---|---|---|---|
1 | 19 | 10 | 10 - 19 = -9 | 17 | R |
2 | 7 | 4 | 4 - 7 = -3 | 23 | X |
3 | 8 | 24 | 24 - 8 = 16 | 16 | Q |
4 | 18 | 10 | 10 - 18 = -8 | 18 | S |
5 | 8 | 4 | 4 - 8 = -4 | 22 | W |
6 | 18 | 24 | 24 - 18 = 6 | 6 | G |
7 | 0 | 10 | 10 - 0 = 10 | 10 | K |
8 | 19 | 4 | 4 - 19 = -15 | 11 | L |
9 | 4 | 24 | 24 - 4 = 20 | 20 | U |
10 | 18 | 10 | 10 - 18 = -8 | 18 | S |
11 | 19 | 4 | 4 - 19 = -15 | 11 | L |
The final encrypted text is RXQSWGKLUSL
.
beaufort.py
#!/usr/bin/env python3
import argparse
def beaufort_cipher(text, key):
"""
Applies the Beaufort cipher to a given text using a given key.
The Beaufort cipher is reciprocal, so the same process handles
encryption and decryption.
It processes uppercase and lowercase letters, preserving case,
and leaves non-letter characters unchanged.
:param text: The text to be processed.
:param key: The key for the cipher.
:return: The processed text.
"""
if not text or not key:
raise ValueError("Text and key cannot be empty.")
result = []
key_idx = 0
for char in text:
if char.isupper():
base = ord("A")
# Get the current key character and calculate its shift value (0-25)
key_char = key[key_idx % len(key)].upper()
key_shift = ord(key_char) - base
# Calculate the plaintext character's shift value (0-25)
text_shift = ord(char) - base
# Apply the Beaufort formula: (key - plaintext) % 26
new_char = chr((key_shift - text_shift) % 26 + base)
result.append(new_char)
key_idx += 1
elif char.islower():
base = ord("a")
# Get the current key character and calculate its shift value (0-25)
key_char = key[key_idx % len(key)].lower()
key_shift = ord(key_char) - base
# Calculate the plaintext character's shift value (0-25)
text_shift = ord(char) - base
# Apply the Beaufort formula: (key - plaintext) % 26
new_char = chr((key_shift - text_shift) % 26 + base)
result.append(new_char)
key_idx += 1
else:
# Append non-alphabetic characters without change
result.append(char)
return "".join(result)
def main():
parser = argparse.ArgumentParser(description="Beaufort Cipher")
parser.add_argument("text", help="Text to apply the cipher to")
parser.add_argument("key", help="Cipher key")
args = parser.parse_args()
try:
result = beaufort_cipher(args.text, args.key)
print(f"Result: {result}")
except ValueError as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
Now that we've seen how keywords can create more complex ciphers, maybe next time we'll look at something that doesn't just substitute single letters, but pairs of them...
As always,
Michael Garcia a.k.a. TheCrazyGM
Congratulations @thecrazygm! You have completed the following achievement on the Hive blockchain And have been rewarded with New badge(s)
Your next target is to reach 550 posts.
You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word
STOP
Thank you for the cryptographic education, my friend. It's still a little fuzzy to my understanding, because I haven't worked with it, but it's interesting regardless. 😁🙏💚✨🤙
Loved the bit about ROT13 being its own inverse when you run it twice. Using pyhton ord and chr to handle the alphabet wrap around make the shift clean, and it clicks quick for me. Its fun how a simple Caesar trick feels like a secret handshake for letters.