The Algorithms - Python Python TheAlgorithms/Python

Cipher Algorithms in TheAlgorithms/Python

Seven ciphers from ancient Rome to Diffie-Hellman: each a different answer to the question of what makes a message unreadable.

7 stops ~16 min Verified 2026-05-05
What you will learn
  • How Caesar cipher shifts each character by a fixed key modulo the alphabet length
  • How Vigenere cipher extends Caesar by cycling through a keyword to vary the shift per character
  • How RSA encrypts blocks using modular exponentiation with a public key pair
  • How Diffie-Hellman lets two parties derive a shared secret without transmitting it
  • How Playfair cipher encrypts letter pairs using a 5x5 key table with three positional rules
  • How Hill cipher multiplies text vectors by a key matrix modulo 36
  • How the Enigma machine passes each letter through plugboard, three rotors, and a reflector
Prerequisites
  • Comfort reading Python loops, modular arithmetic, and string indexing
  • Familiarity with the concept of a key in symmetric encryption
  • No prior cryptography knowledge required
1 / 7

Caesar cipher: shift by key, wrap with modulo

ciphers/caesar_cipher.py:71

Caesar cipher shifts each character's alphabet index by a fixed key and wraps using modulo, leaving non-alphabet characters unchanged.

Caesar cipher is the simplest substitution cipher: every letter in the plaintext is replaced by the letter a fixed number of positions later in the alphabet. The alpha.index(character) + key expression finds the current letter's position and adds the shift. The % len(alpha) wraps the result back into range when the shift would go past the end of the alphabet, which is why a key of 8000 in the doctest still produces a valid output. Characters not in the alphabet, such as spaces and punctuation, are appended unchanged. The implementation accepts an optional custom alphabet, making it usable for any character set, not only standard English letters. For a full tour of the Caesar cipher including the decrypt function and brute-force attack, see the deeper tour.

Key takeaway

Caesar cipher encodes one character at a time using (index + key) % alphabet_length; the modulo handles wrap-around regardless of key size.

    # Set default alphabet to lower and upper case english chars
    alpha = alphabet or ascii_letters

    # The final result string
    result = ""

    for character in input_string:
        if character not in alpha:
            # Append without encryption if character is not in the alphabet
            result += character
        else:
            # Get the index of the new key and make sure it isn't too large
            new_key = (alpha.index(character) + key) % len(alpha)

            # Append the encoded character to the alphabet
            result += alpha[new_key]

    return result
2 / 7

Vigenere cipher: cycle through a keyword to vary the shift

ciphers/vigenere_cipher.py:36

Vigenere cycles through a keyword; each plaintext character is shifted by the corresponding keyword character's alphabet position.

Caesar cipher's weakness is that every letter shifts by the same amount, so frequency analysis breaks it quickly. Vigenere's fix is to vary the shift by cycling through a keyword. The key_index variable tracks which keyword character to use next. For each plaintext letter, LETTERS.find(key[key_index]) gives the shift value for this position, and the result is reduced modulo 26. After each alphabetic character is processed, key_index advances and wraps at the keyword length, so a six-letter keyword like HDarji produces six different shifts cycling A-H, B-D, C-a, and so on. Non-alphabet characters such as spaces and punctuation are appended untouched and do not advance the key index. This single shared function handles both encryption and decryption based on the mode argument.

Key takeaway

Vigenere defeats simple frequency analysis by using a different shift for each position, derived by cycling through the keyword.

def translate_message(key: str, message: str, mode: str) -> str:
    translated = []
    key_index = 0
    key = key.upper()

    for symbol in message:
        num = LETTERS.find(symbol.upper())
        if num != -1:
            if mode == "encrypt":
                num += LETTERS.find(key[key_index])
            elif mode == "decrypt":
                num -= LETTERS.find(key[key_index])

            num %= len(LETTERS)

            if symbol.isupper():
                translated.append(LETTERS[num])
            elif symbol.islower():
                translated.append(LETTERS[num].lower())

            key_index += 1
            if key_index == len(key):
                key_index = 0
        else:
            translated.append(symbol)
    return "".join(translated)
3 / 7

RSA: encrypt blocks with modular exponentiation

ciphers/rsa_cipher.py:39

RSA encryption converts each message block to an integer, then computes block to the power of e modulo n.

RSA's security rests on the difficulty of factoring the product of two large primes. The public key is the pair (n, e) where n is that product and e is the public exponent. Encryption is a single line: pow(block, e, n) raises the message block to the power e modulo n. Python's built-in pow with three arguments uses fast modular exponentiation, so even with 1024-bit numbers this completes quickly. The block conversion step before this function splits the string into fixed-size integer blocks. Decryption uses the same pow(block, d, n) operation with the private exponent d. The mathematical guarantee is that (m^e)^d mod n == m for any message integer m. For the full key generation and decryption flow, see the deeper tour.

Key takeaway

RSA encryption is a single modular exponentiation: pow(block, e, n). Security comes from the impossibility of reversing that without knowing d.

def encrypt_message(
    message: str, key: tuple[int, int], block_size: int = DEFAULT_BLOCK_SIZE
) -> list[int]:
    encrypted_blocks = []
    n, e = key

    for block in get_blocks_from_text(message, block_size):
        encrypted_blocks.append(pow(block, e, n))
    return encrypted_blocks
4 / 7

Diffie-Hellman: derive a shared secret via modular exponentiation

ciphers/diffie_hellman.py:225

Each party computes generator raised to their private key modulo a prime; applying the other party's public key to your private key gives both parties the same shared secret.

Diffie-Hellman solves the problem of two parties agreeing on a shared secret over an insecure channel without transmitting that secret. Alice's public key is g^a mod p where g is the group generator, a is her private key, and p is the prime. Bob's public key is g^b mod p. When Alice receives Bob's public key and applies her own private key she gets (g^b)^a mod p. Bob gets (g^a)^b mod p. Both expressions equal g^(ab) mod p, so both parties hold the same value without ever transmitting it. The is_valid_public_key guard uses the Legendre symbol check from NIST SP 800-56 to reject invalid keys that could leak the private exponent. The shared secret is then hashed with SHA-256 before use.

Key takeaway

Diffie-Hellman allows two parties to arrive at the same shared secret without transmitting it, using the commutativity of modular exponentiation.

    def generate_public_key(self) -> str:
        public_key = pow(self.generator, self.__private_key, self.prime)
        return hex(public_key)[2:]

    def is_valid_public_key(self, key: int) -> bool:
        # check if the other public key is valid based on NIST SP800-56
        return (
            2 <= key <= self.prime - 2
            and pow(key, (self.prime - 1) // 2, self.prime) == 1
        )

    def generate_shared_key(self, other_key_str: str) -> str:
        other_key = int(other_key_str, base=16)
        if not self.is_valid_public_key(other_key):
            raise ValueError("Invalid public key")
        shared_key = pow(other_key, self.__private_key, self.prime)
        return sha256(str(shared_key).encode()).hexdigest()
5 / 7

Playfair cipher: three rules for letter-pair substitution

ciphers/playfair_cipher.py:100

Playfair processes pairs of letters; each pair is substituted using one of three rules based on whether the letters share a row, column, or neither.

Playfair, developed by Charles Wheatstone in 1854, encrypts two letters at a time rather than one. This makes single-letter frequency analysis ineffective. The key generates a 5x5 table of the 25 letters (I and J are merged). For each pair, divmod(table.index(char), 5) extracts the row and column. Three rules govern substitution: if both letters are in the same row, each shifts right one column with wrap; if in the same column, each shifts down one row with wrap; otherwise the letters form the corners of a rectangle and are replaced by the opposite corners. The rectangle rule on lines 115-116 swaps columns: each letter takes its row but the other letter's column. Decryption reverses the directional shifts. The guard against repeated-letter pairs in prepare_input is why an X sometimes appears in decrypted output.

Key takeaway

Playfair encrypts letter pairs using three positional rules on a 5x5 key table, making single-letter frequency attacks ineffective.

    table = generate_table(key)
    plaintext = prepare_input(plaintext)
    ciphertext = ""

    for char1, char2 in chunker(plaintext, 2):
        row1, col1 = divmod(table.index(char1), 5)
        row2, col2 = divmod(table.index(char2), 5)

        if row1 == row2:
            ciphertext += table[row1 * 5 + (col1 + 1) % 5]
            ciphertext += table[row2 * 5 + (col2 + 1) % 5]
        elif col1 == col2:
            ciphertext += table[((row1 + 1) % 5) * 5 + col1]
            ciphertext += table[((row2 + 1) % 5) * 5 + col2]
        else:  # rectangle
            ciphertext += table[row1 * 5 + col2]
            ciphertext += table[row2 * 5 + col1]

    return ciphertext
6 / 7

Hill cipher: matrix multiplication as the encryption key

ciphers/hill_cipher.py:120

Hill cipher converts text to numeric vectors and multiplies each by the key matrix modulo 36 to produce ciphertext.

Hill cipher applies linear algebra to cryptography: the key is an NxN matrix and each plaintext batch of N characters becomes a column vector. The encryption step is key_matrix dot text_vector mod 36. The result vector is mapped back to characters. This makes Hill cipher a polyalphabetic substitution where each output character depends on N input characters simultaneously, not one at a time. The doctest shows that encrypting 'hello' with a 2x2 key matrix produces '85FF00', six characters because process_text pads the input to an even length. Digits appear in the output because the implementation uses a 36-character alphabet (26 letters plus 10 digits) to make the key matrix invertible modulo 36. Decryption requires the modular inverse of the key matrix, which is why the determinant must be coprime with 36.

Key takeaway

Hill cipher encrypts batches of N characters at once by treating each batch as a vector and multiplying by the key matrix modulo the alphabet size.

    def encrypt(self, text: str) -> str:
        """
        >>> hill_cipher = HillCipher(np.array([[2, 5], [1, 6]]))
        >>> hill_cipher.encrypt('testing hill cipher')
        'WHXYJOLM9C6XT085LL'
        >>> hill_cipher.encrypt('hello')
        '85FF00'
        """
        text = self.process_text(text.upper())
        encrypted = ""

        for i in range(0, len(text) - self.break_key + 1, self.break_key):
            batch = text[i : i + self.break_key]
            vec = [self.replace_letters(char) for char in batch]
            batch_vec = np.array([vec]).T
            batch_encrypted = self.modulus(self.encrypt_key.dot(batch_vec)).T.tolist()[
                0
            ]
            encrypted_batch = "".join(
                self.replace_digits(num) for num in batch_encrypted
            )
            encrypted += encrypted_batch

        return encrypted
7 / 7

Enigma: plugboard, three rotors, reflector, reverse

ciphers/enigma_machine2.py:240

Enigma passes each letter through a plugboard substitution, three sequential rotor permutations, a reflector, the rotors in reverse, and the plugboard again.

The Enigma machine's security came from the combination of three substitution layers applied in sequence. Each rotor is a fixed permutation of the 26-letter alphabet. A letter enters rotor 1 at an offset determined by the rotor's current position, passes through its permutation, then continues to rotor 2 and rotor 3 the same way. The reflector on line 261 maps every letter to a partner, ensuring the path is reversible. After the reflector, the signal passes back through all three rotors in reverse order using index() instead of subscript access. The comment on line 259 explains the key property: because the reflector maps every letter to a different letter, encrypting the ciphertext with the same rotor settings recovers the plaintext without needing a separate decryption mode. Rotor positions advance after each letter, so the same plaintext letter encodes differently each time.

Key takeaway

The reflector makes Enigma self-inverse: the same machine and settings decrypt ciphertext, because every letter's forward path is the reverse of another letter's forward path.

    for symbol in text:
        if symbol in abc:
            # 1st plugboard --------------------------
            if symbol in plugboard:
                symbol = plugboard[symbol]

            # rotor ra --------------------------
            index = abc.index(symbol) + rotorpos1
            symbol = rotor1[index % len(abc)]

            # rotor rb --------------------------
            index = abc.index(symbol) + rotorpos2
            symbol = rotor2[index % len(abc)]

            # rotor rc --------------------------
            index = abc.index(symbol) + rotorpos3
            symbol = rotor3[index % len(abc)]

            # reflector --------------------------
            # this is the reason you don't need another machine to decipher

            symbol = reflector[symbol]

            # 2nd rotors
            symbol = abc[rotor3.index(symbol) - rotorpos3]
            symbol = abc[rotor2.index(symbol) - rotorpos2]
            symbol = abc[rotor1.index(symbol) - rotorpos1]

            # 2nd plugboard
            if symbol in plugboard:
                symbol = plugboard[symbol]
Your codebase next

Create code tours for your project

Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.

Install Intraview Free