📚 Math in Society
⇩ Download ▾

16.2 Substitution Ciphers

One simple encryption method is called a substitution cipher.

Diagram of the Caesar cipher with a shift of three. A row of lettered boxes A, B, C, D, E, F sits above a second row of the same boxes, and arrows run from each letter in the top row down to the letter three places later in the bottom row, so A maps to D, the highlighted B maps to the highlighted E, and C maps to F. Faded boxes X, Y, Z before the top row and G, H, I after the bottom row show the alphabet carrying on and wrapping around.A simple example of a substitution cipher is called the Caesar cipher, sometimes called a shift cipher. In this approach, each letter is replaced with a letter some fixed number of positions later in the alphabet. For example, if we use a shift of 3, then the letter A would be replaced with D, the letter 3 positions later in the alphabet. The entire mapping would look like: [1]

import string
ALPHA = string.ascii_uppercase

MESSAGE = "We ride at noon"      # EDIT
SHIFT = 3                        # EDIT: the encryption KEY

def caesar(text, shift):
    out = ""
    for ch in text.upper():
        if ch in ALPHA:
            out += ALPHA[(ALPHA.index(ch) + shift) % 26]
        elif ch.isdigit():
            out += ch
    return out

def blocks(s, n=3):
    return " ".join(s[i:i + n] for i in range(0, len(s), n))

print(f"the mapping for a shift of {SHIFT}")
print("   Original: " + " ".join(ALPHA))
print("   Maps to:  " + " ".join(caesar(ALPHA, SHIFT)))

secret = caesar(MESSAGE, SHIFT)
print(f"\nplaintext   {MESSAGE}")
print(f"encrypted   {secret}")
print(f"in blocks   {blocks(secret)}      <- hides the word lengths")
print(f"decrypted   {caesar(secret, -SHIFT)}   (same machine, shift {-SHIFT})")
print(f"round trip works? {caesar(secret, -SHIFT) == caesar(MESSAGE, 0)}")

print("\nthe book's other two messages, decrypted:")
for cipher, key, note in (("GZDKNKYDXMFWJXA", 5, "= 'Buy fifty shares' plus a filler letter"),
                          ("BNWMVXWNH", 9, "= 'Send money'")):
    print(f"   {blocks(cipher):<22} shift {key:>2}  ->  {caesar(cipher, -key):<18} {note}")

print("\nwhy the message gets chopped into equal blocks:")
print("   keeping word breaks:  " + " ".join(caesar(w, SHIFT) for w in "I am a spy".split()))
print("   in blocks of three:   " + blocks(caesar("I am a spy", SHIFT)))
print("   a one-letter word in the first version can only be an encrypted A or I")
# Try it: change SHIFT to 13. Encrypt twice with 13 and you get back where you started.

Original: ABCDEFGHIJKLMNOPQRSTUVWXYZ

Maps to: DEFGHIJKLMNOPQRSTUVWXYZABC

Woodcut of an Alberti cipher disk: a fixed outer ring of 24 cells holding the capitals A, B, C, D, E, F, G, I, L, M, N, O, P, Q, R, S, T, V, X and Z followed by the digits 1, 2, 3 and 4, and inside it a rotatable ring of 24 lowercase letters in scrambled, non-alphabetical order. Turning the inner wheel lines a different lowercase letter up against each capital, which is what sets the cipher shift.

Notice that in both the ciphers above, the extra part of the alphabet wraps around to the beginning. Because of this, a handy version of the shift cipher is a cipher disc, such as the Alberti cipher disk shown here[2] from the 1400s. In a cipher disc, the inner wheel could be turned to change the cipher shift. This same approach is used for “secret decoder rings.”

The security of a cryptographic method is very important to the person relying on their message being kept secret. The security depends on two factors:

  1. The security of the method being used
  2. The security of the encryption key used

In the case of a shift cipher, the method is “a shift cipher is used.” The encryption key is the specific amount of shift used.

Suppose an army is using a shift cipher to send their messages, and one of their officers is captured by their enemy. It is likely the method and encryption key could become compromised. It is relatively hard to change encryption methods, but relatively easy to change encryption keys.

During World War II, the Germans’ Enigma encryption machines were captured, but having details on the encryption method only slightly helped the Allies, since the encryption keys were still unknown and hard to discover. Ultimately, the security of a message cannot rely on the method being kept secret; it needs to rely on the key being kept secret.

With that in mind, let’s analyze the security of the Caesar cipher.

To make a brute force attack harder, we could make a more complex substitution cipher by using something other than a shift of the alphabet. By choosing a random mapping, we could get a more secure cipher, with the tradeoff that the encryption key is harder to describe; the key would now be the entire mapping, rather than just the shift amount.

While there were only 25 possible shift cipher keys (35 if we had included numbers), there are about 1040 possible substitution ciphers[3]. That’s much more than a trillion trillions. It would be essentially impossible, even with supercomputers, to try every Bar chart of the typical frequency of letters in English text, with the letters a through z in alphabetical order along the horizontal axis and relative frequency from 0 to 0.14 up the vertical axis. The bar for e is by far the tallest at about 0.127, followed by t at about 0.09, a about 0.082, o about 0.075, i about 0.07, n about 0.067, s about 0.063, h about 0.061 and r about 0.06, while j, q, x and z are almost flat against the axis.possible combination. Having a huge number of possible encryption keys is one important part of key security.

Unfortunately, this cipher is still not secure, because of a technique called frequency analysis, discovered by Arab mathematician Al-Kindi in the 9th century. English and other languages have certain letters than show up more often in writing than others.[4] For example, the letter E shows up the most frequently in English. The chart to the right shows the typical distribution of characters.

import string
from collections import Counter
ALPHA = string.ascii_uppercase

# Typical letter frequencies in English text, in percent.
ENGLISH = dict(zip(ALPHA, [8.2, 1.5, 2.8, 4.3, 12.7, 2.2, 2.0, 6.1, 7.0, 0.15, 0.77, 4.0,
                           2.4, 6.7, 7.5, 1.9, 0.095, 6.0, 6.3, 9.1, 2.8, 0.98, 2.4, 0.15,
                           2.0, 0.074]))

PLAINTEXT = ("The security of any encryption method should depend only on the "
             "encryption key being difficult to discover")     # EDIT
SECRET_SHIFT = 12                                              # EDIT

clean = "".join(c for c in PLAINTEXT.upper() if c in ALPHA)
cipher = "".join(ALPHA[(ALPHA.index(c) + SECRET_SHIFT) % 26] for c in clean)
print(f"intercepted ({len(cipher)} letters):\n   {cipher[:78]}...\n")

counts = Counter(cipher)
print("most common letters in the ciphertext vs most common letters in English")
top = counts.most_common(5)
eng_top = sorted(ENGLISH, key=ENGLISH.get, reverse=True)[:5]
for (c, n), e in zip(top, eng_top):
    print(f"   {c} appears {n:>3} times ({n / len(cipher):5.1%})     English favourite: "
          f"{e} ({ENGLISH[e]:.1f}%)")
print(f"   guess from the top letter alone: {top[0][0]} stands for E, "
      f"so the shift is {(ALPHA.index(top[0][0]) - 4) % 26}")

# Score every shift: how far is its letter profile from English?
scored = []
for s in range(26):
    guess = "".join(ALPHA[(ALPHA.index(c) - s) % 26] for c in cipher)
    g = Counter(guess)
    chi = sum((g.get(L, 0) - len(cipher) * ENGLISH[L] / 100) ** 2
              / (len(cipher) * ENGLISH[L] / 100) for L in ALPHA)
    scored.append((chi, s, guess))
scored.sort()

print("\nthe three shifts that look most like English (lower score = better fit)")
for chi, s, guess in scored[:3]:
    print(f"   shift {s:>2}  score {chi:>8.1f}   {guess[:56]}")
best = scored[0]
print(f"\nbroken automatically: the key is {best[1]} (the real key was {SECRET_SHIFT})"
      f"   correct? {best[1] == SECRET_SHIFT}")
print(f"   {best[2]}")
print("\nThis is why a plain shift - and even a random substitution - is not secure:")
print("   the cipher never changes E's disguise, so E's fingerprint survives encryption.")

In addition to looking at individual letters, certain pairs of letters show up more frequently, such as the pair “th.” By analyzing how often different letters and letter pairs show up an encrypted message, the substitution mapping used can be deduced[5].

[1] en.Wikipedia.org/w/index.php?title=File:Caesar3.svg&page=1. PD

[2] en.Wikipedia.org/wiki/File:Alberti_cipher_disk.JPG

[3] There are 35 choices for what A maps to, then 34 choices for what B maps to, and so on, so the total number of possibilities is 35*34*33*…*2*1 = 35! = about 1040

[4] en.Wikipedia.org/w/index.php?title=File:English_letter_frequency_(alphabetic).svg&page=1 PD

[5] For an example of how this is done, see en.Wikipedia.org/wiki/Frequency_analysis

Adapted from Math in Society by David Lippman, hosted on LibreTexts (math.libretexts.org) and licensed under CC BY-SA 3.0. Changes were made. License: CC-BY-SA-3.0.

These eBooks are a prerelease and are not yet certified conformant with WCAG 2.1 AA or ADA Title II. Every page is built against an automated accessibility gate, and the published editions will meet ADA Title II requirements when they release in late September 2026. If something is unusable, please tell us.