Fun With Python: Intro To Cryptography - Vigenère and Beaufort

(edited)

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.

screenshot-20250921-053629.png


The Vigenère Cipher

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.

How it Works: A Manual Example

To see how this works in practice, let's walk through an example by hand.

  • Plaintext: THISISATEST
  • Key: KEY

Step 1: Assign numbers 0-25 to the alphabet.

LetterABCDEFGHIJKLMNOPQRSTUVWXYZ
Number012345678910111213141516171819202122232425

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.

  • Plaintext Numbers: 19, 7, 8, 18, 8, 18, 0, 19, 4, 18, 19
  • Key Numbers: 10, 4, 24, 10, 4, 24, 10, 4, 24, 10, 4

Step 4: Add the numbers and take the result modulo 26.

PositionPlaintext NumKey NumSumSum mod 26Encrypted Letter
11910293D
2741111L
3824326G
41810282C
5841212M
618244216Q
70101010K
81942323X
9424282C
101810282C
111942323X

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()

The Beaufort Cipher

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.

How it Works: A Manual Example

The setup is the same, but the formula changes.

  • Plaintext: THISISATEST
  • Key: KEY

Step 1-3: Set up the numbers just like before.

  • Plaintext Numbers: 19, 7, 8, 18, 8, 18, 0, 19, 4, 18, 19
  • Key Numbers: 10, 4, 24, 10, 4, 24, 10, 4, 24, 10, 4

Step 4: Subtract the plaintext number from the key number, modulo 26.

PositionPlaintext NumKey NumDifferenceDiff mod 26Encrypted Letter
1191010 - 19 = -917R
2744 - 7 = -323X
382424 - 8 = 1616Q
4181010 - 18 = -818S
5844 - 8 = -422W
6182424 - 18 = 66G
701010 - 0 = 1010K
81944 - 19 = -1511L
942424 - 4 = 2020U
10181010 - 18 = -818S
111944 - 19 = -1511L

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

0.12178812 BEE
3 comments

Congratulations @thecrazygm! You have completed the following achievement on the Hive blockchain And have been rewarded with New badge(s)

You published more than 500 posts.
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

0.00021956 BEE

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. 😁🙏💚✨🤙

0.00021470 BEE

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.

0.00021337 BEE