Practical 11, Part 1: The Substitution Ciphers
Chapter Fifteen
Syllabus topic Module 2, "Implementing Substitution and Transposition Ciphers: Design and implement algorithms to encrypt and decrypt messages using classical substitution and transposition techniques."
Pages 112 to 123 of 206
Aim
To design and implement the classical substitution ciphers, to encrypt and decrypt with each, and to break the one that can be broken by counting letters.
What you need to know before you start
A substitution cipher replaces each letter with another letter. A transposition cipher keeps the letters and moves them. This chapter is the first kind; the next chapter is the second.
Five terms, used throughout:
- Plaintext, the message. Ciphertext, the message after encryption.
- Key, the secret that decides which of the many possible encryptions was used.
- Key space, how many keys there are. It is the first thing to compute about any cipher, because it is the cost of trying them all.
- Kerckhoffs's principle: assume the attacker knows the algorithm. Only the key is secret. A cipher whose security depends on nobody knowing how it works has no security at all.
Cipher 1: Caesar, and its whole key space
Shift every letter forward by a fixed number of places, wrapping round from Z to A.
c = (p + k) mod 26
p = (c - k) mod 26
"""Practical 11: the Caesar cipher, and breaking it by trying every key."""
def caesar(text, shift):
out = []
for ch in text:
if "A" <= ch <= "Z":
out.append(chr((ord(ch) - 65 + shift) % 26 + 65))
elif "a" <= ch <= "z":
out.append(chr((ord(ch) - 97 + shift) % 26 + 97))
else:
out.append(ch)
return "".join(out)
PLAIN = "MEET ME AT THE LIBRARY AT FOUR"
KEY = 3
cipher = caesar(PLAIN, KEY)
print("plaintext :", PLAIN)
print("key :", KEY)
print("ciphertext :", cipher)
print("decrypted :", caesar(cipher, -KEY))
print()
print("the whole key space, all 25 of it:")
for k in range(1, 26):
guess = caesar(cipher, -k)
mark = " <-- English" if "MEET" in guess else ""
print(" k = %2d %s%s" % (k, guess, mark))plaintext : MEET ME AT THE LIBRARY AT FOUR
key : 3
ciphertext : PHHW PH DW WKH OLEUDUB DW IRXU
decrypted : MEET ME AT THE LIBRARY AT FOUR
the whole key space, all 25 of it:
k = 1 OGGV OG CV VJG NKDTCTA CV HQWT
k = 2 NFFU NF BU UIF MJCSBSZ BU GPVS
k = 3 MEET ME AT THE LIBRARY AT FOUR <-- English
k = 4 LDDS LD ZS SGD KHAQZQX ZS ENTQ
k = 5 KCCR KC YR RFC JGZPYPW YR DMSP
k = 6 JBBQ JB XQ QEB IFYOXOV XQ CLRO
k = 7 IAAP IA WP PDA HEXNWNU WP BKQN
k = 8 HZZO HZ VO OCZ GDWMVMT VO AJPM
k = 9 GYYN GY UN NBY FCVLULS UN ZIOL
k = 10 FXXM FX TM MAX EBUKTKR TM YHNK
k = 11 EWWL EW SL LZW DATJSJQ SL XGMJ
k = 12 DVVK DV RK KYV CZSIRIP RK WFLI
k = 13 CUUJ CU QJ JXU BYRHQHO QJ VEKH
k = 14 BTTI BT PI IWT AXQGPGN PI UDJG
k = 15 ASSH AS OH HVS ZWPFOFM OH TCIF
k = 16 ZRRG ZR NG GUR YVOENEL NG SBHE
k = 17 YQQF YQ MF FTQ XUNDMDK MF RAGD
k = 18 XPPE XP LE ESP WTMCLCJ LE QZFC
k = 19 WOOD WO KD DRO VSLBKBI KD PYEB
k = 20 VNNC VN JC CQN URKAJAH JC OXDA
k = 21 UMMB UM IB BPM TQJZIZG IB NWCZ
k = 22 TLLA TL HA AOL SPIYHYF HA MVBY
k = 23 SKKZ SK GZ ZNK ROHXGXE GZ LUAX
k = 24 RJJY RJ FY YMJ QNGWFWD FY KTZW
k = 25 QIIX QI EX XLI PMFVEVC EX JSYVPractical 11, Part 1: The Substitution Ciphers
Twenty-five keys. That is the entire key space, and the program printed all of it. A cipher a computer breaks in twenty-five tries, and a person breaks in about five, is a cipher with no security, and the exercise is worth doing precisely because it shows what a small key space means.
% 26 is what does the wrapping, and in Python it also handles the negative shift used for decryption: -3 % 26 is 23, not -3, so caesar(cipher, -KEY) works without a special case. In C or Java it would not, and you would have to add 26 first.
Cipher 2: the general monoalphabetic cipher, and how to break it
Caesar's weakness is that its key is one number. Let the key be a whole permutation of the alphabet instead, any letter to any letter, and the key space becomes 26 factorial.
"""A monoalphabetic cipher, and the frequency attack that breaks it."""
import random
from collections import Counter
ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# The order of letters by frequency in ordinary English text, commonest first.
# It is used only as a STARTING GUESS for the attack, never as a key.
ENGLISH_ORDER = "ETAOINSHRDLCUMWFGYPBVKJXQZ"
def make_key(seed):
letters = list(ALPHA)
random.Random(seed).shuffle(letters)
return "".join(letters)
def substitute(text, table):
return "".join(table.get(c, c) for c in text.upper())
MESSAGE = (
"THE EXAMINATION TIMETABLE FOR THE THIRD YEAR IS ON THE NOTICE BOARD "
"OUTSIDE THE DEPARTMENT OFFICE AND EVERY STUDENT MUST CHECK THE ROOM "
"NUMBER BEFORE THE FIRST PAPER BECAUSE THE ROOMS HAVE CHANGED THIS TIME"
)
KEY = make_key(5)
enc = {p: c for p, c in zip(ALPHA, KEY)}
dec = {c: p for p, c in zip(ALPHA, KEY)}
print("key (A to Z maps to):", KEY)
print()
cipher = substitute(MESSAGE, enc)
print("ciphertext:")
print(" ", cipher)
print()
print("decrypted with the key:")
print(" ", substitute(cipher, dec))
print()
print("how big is the key space? 26 factorial =", end=" ")
f = 1
for i in range(1, 27):
f *= i
print(f)
print("so brute force is out of the question. Count letters instead.")
print()
counts = Counter(c for c in cipher if c.isalpha())
total = sum(counts.values())
print("%-8s %6s %8s" % ("letter", "count", "percent"))
for ch, n in counts.most_common(8):
print("%-8s %6d %7.1f%%" % (ch, n, 100 * n / total))
print()
order = "".join(ch for ch, _ in counts.most_common())
guess_table = {c: p for c, p in zip(order, ENGLISH_ORDER)}
print("guessing the commonest ciphertext letter is E, the next is T, and so on:")
print(" ", substitute(cipher, guess_table))
print()
right = sum(1 for c, p in guess_table.items() if dec.get(c) == p)
print("letters the frequency guess got right straight away: %d of %d"
% (right, len(guess_table)))Practical 11, Part 1: The Substitution Ciphers
key (A to Z maps to): CKEMJNZWSYGDRPVFBHOAQULXIT
ciphertext:
AWJ JXCRSPCASVP ASRJACKDJ NVH AWJ AWSHM IJCH SO VP AWJ PVASEJ KVCHM VQAOSMJ AWJ MJFCHARJPA VNNSEJ CPM JUJHI OAQMJPA RQOA EWJEG AWJ HVVR PQRKJH KJNVHJ AWJ NSHOA FCFJH KJECQOJ AWJ HVVRO WCUJ EWCPZJM AWSO ASRJ
decrypted with the key:
THE EXAMINATION TIMETABLE FOR THE THIRD YEAR IS ON THE NOTICE BOARD OUTSIDE THE DEPARTMENT OFFICE AND EVERY STUDENT MUST CHECK THE ROOM NUMBER BEFORE THE FIRST PAPER BECAUSE THE ROOMS HAVE CHANGED THIS TIME
how big is the key space? 26 factorial = 403291461126605635584000000
so brute force is out of the question. Count letters instead.
letter count percent
J 29 16.9%
A 21 12.2%
W 12 7.0%
V 12 7.0%
H 12 7.0%
C 11 6.4%
S 11 6.4%
P 9 5.2%
guessing the commonest ciphertext letter is E, the next is T, and so on:
TAE EPNRSHNTSOH TSRETNUBE MOI TAE TASIL GENI SD OH TAE HOTSCE UONIL OWTDSLE TAE LEFNITREHT OMMSCE NHL EYEIG DTWLEHT RWDT CAECV TAE IOOR HWRUEI UEMOIE TAE MSIDT FNFEI UECNWDE TAE IOORD ANYE CANHKEL TASD TSRE
letters the frequency guess got right straight away: 4 of 22403,291,461,126,605,635,584,000,000 keys. Four hundred million million million million. No computer will ever try them all, so by the key-space measure this cipher is unbreakable. It is not, and the reason is the whole lesson of this practical.
A substitution cipher does not hide the frequencies. Whatever E becomes, it becomes the same letter every time, so the commonest letter in the ciphertext is almost certainly E. Count them and you have a start.
The program did exactly that: it counted the ciphertext letters, and matched the commonest to E, the next to T, and so on down the standard English order. That guess alone put 4 of the 22 letters in the right place, and look at what it produced: TAE EPNRSHNTSOH TSRETNUBE. The word THE has appeared twice in the first four letters, and TSRETNUBE is TIMETABLE with three letters still wrong.
Practical 11, Part 1: The Substitution Ciphers
That is how the attack really goes. The counts give you the four or five commonest letters, those give you a few short words, the short words give you more letters, and the message unravels. It is finished by hand, not by the program, and an examiner may well ask you to finish it.
Two other clues, worth a line each in the journal: a one-letter word in English is A or I; and the commonest three-letter pattern is THE.
So the key space is not the measure of a cipher's strength. It is an upper bound on the work, and structure that survives encryption can bring the real work far below it.
Cipher 3: Playfair, which encrypts two letters at a time
Playfair attacks the frequency problem directly: encrypt pairs. There are 676 pairs rather than 26 letters, so a frequency count of single letters tells you much less.
The key is a word. Write it into a five by five table with the repeats removed, fill the rest of the alphabet after it, and put I and J in one square, because 26 letters do not fit in 25 squares.
Then take the plaintext two letters at a time, and for each pair:
- Same row: replace each with the letter to its right, wrapping round.
- Same column: replace each with the letter below it, wrapping round.
- Neither (a rectangle): replace each with the letter in its own row, in the other one's column.
A doubled letter inside a pair is split with an X, and an odd-length message gets an X on the end.
"""The Playfair cipher: a 5 by 5 table, and two letters enciphered at a time."""
def build_table(key):
seen, letters = [], []
for ch in (key + "ABCDEFGHIKLMNOPQRSTUVWXYZ").upper():
if not ch.isalpha():
continue
ch = "I" if ch == "J" else ch # I and J share one square
if ch not in seen:
seen.append(ch)
letters.append(ch)
return [letters[r * 5:r * 5 + 5] for r in range(5)]
def position(table, ch):
for r in range(5):
for c in range(5):
if table[r][c] == ch:
return r, c
def digraphs(text):
text = [("I" if c == "J" else c) for c in text.upper() if c.isalpha()]
out, i = [], 0
while i < len(text):
a = text[i]
b = text[i + 1] if i + 1 < len(text) else "X"
if a == b:
b = "X" # a doubled letter is split with X
i += 1
else:
i += 2
out.append(a + b)
return out
def crypt(table, pair, direction):
(r1, c1), (r2, c2) = position(table, pair[0]), position(table, pair[1])
if r1 == r2:
rule = "same row"
c1, c2 = (c1 + direction) % 5, (c2 + direction) % 5
elif c1 == c2:
rule = "same column"
r1, r2 = (r1 + direction) % 5, (r2 + direction) % 5
else:
rule = "rectangle"
c1, c2 = c2, c1
return table[r1][c1] + table[r2][c2], rule
KEY = "MONARCHY"
table = build_table(KEY)
print("key:", KEY)
print("the table (I and J share a square):")
for row in table:
print(" " + " ".join(row))
print()
PLAIN = "ATTACK AT DAWN"
pairs = digraphs(PLAIN)
print("plaintext :", PLAIN)
print("digraphs :", " ".join(pairs))
print()
print("%-10s %-12s %-10s" % ("pair", "rule", "becomes"))
cipher = ""
for p in pairs:
out, rule = crypt(table, p, +1)
cipher += out
print("%-10s %-12s %-10s" % (p, rule, out))
print()
print("ciphertext:", cipher)
back = "".join(crypt(table, cipher[i:i + 2], -1)[0] for i in range(0, len(cipher), 2))
print("decrypted :", back)Practical 11, Part 1: The Substitution Ciphers
key: MONARCHY
the table (I and J share a square):
M O N A R
C H Y B D
E F G I K
L P Q S T
U V W X Z
plaintext : ATTACK AT DAWN
digraphs : AT TA CK AT DA WN
pair rule becomes
AT rectangle RS
TA rectangle SR
CK rectangle DE
AT rectangle RS
DA rectangle BR
WN same column NY
ciphertext: RSSRDERSBRNY
decrypted : ATTACKATDAWNThe rule column is there on purpose: the examiner can see which of the three rules fired for each pair, and so can you when one of them is wrong. Five of the six pairs here were rectangles and one was a column.
Note that AT became RS twice. Playfair hides single-letter frequencies, not pair frequencies. It is much stronger than a monoalphabetic cipher and it is still breakable by the same kind of reasoning applied to digraphs.
Decryption is the same function with the direction reversed, which is the +1 and -1 in the program: right becomes left, below becomes above, and the rectangle rule is its own inverse.
Cipher 4: Hill, which uses arithmetic instead of a table
Hill turns letters into numbers, takes them in blocks, and multiplies each block by a matrix, modulo 26.
C = K * P mod 26
P = K inverse * C mod 26
The interesting part is the inverse, because a matrix modulo 26 does not always have one.
"""The Hill cipher: letters as numbers, and a matrix as the key."""
def inverse_mod(a, m):
"""The x with a*x = 1 (mod m), found by trying: m is only 26."""
for x in range(1, m):
if (a * x) % m == 1:
return x
return None
KEY = [[3, 3], [2, 5]] # a 2 by 2 key matrix
det = (KEY[0][0] * KEY[1][1] - KEY[0][1] * KEY[1][0]) % 26
det_inv = inverse_mod(det, 26)
print("key matrix:", KEY)
print("determinant mod 26 :", det)
print("its inverse mod 26 :", det_inv, " (because %d * %d = %d = 1 mod 26)"
% (det, det_inv, det * det_inv))
INV = [[(det_inv * KEY[1][1]) % 26, (-det_inv * KEY[0][1]) % 26],
[(-det_inv * KEY[1][0]) % 26, (det_inv * KEY[0][0]) % 26]]
print("inverse key matrix :", INV)
print()
def apply_matrix(m, text):
nums = [ord(c) - 65 for c in text]
out = ""
for i in range(0, len(nums), 2):
a, b = nums[i], nums[i + 1]
x = (m[0][0] * a + m[0][1] * b) % 26
y = (m[1][0] * a + m[1][1] * b) % 26
out += chr(x + 65) + chr(y + 65)
return out
PLAIN = "HELPX"
PLAIN = PLAIN + "X" * (len(PLAIN) % 2) # pad to an even length
print("plaintext :", PLAIN)
print("as numbers :", [ord(c) - 65 for c in PLAIN])
cipher = apply_matrix(KEY, PLAIN)
print("ciphertext :", cipher, [ord(c) - 65 for c in cipher])
print("decrypted :", apply_matrix(INV, cipher))
print()
print("one block by hand: HE is [7, 4]")
print(" x = (3*7 + 3*4) mod 26 = %d mod 26 = %d -> %s"
% (3 * 7 + 3 * 4, (3 * 7 + 3 * 4) % 26, chr((3 * 7 + 3 * 4) % 26 + 65)))
print(" y = (2*7 + 5*4) mod 26 = %d mod 26 = %d -> %s"
% (2 * 7 + 5 * 4, (2 * 7 + 5 * 4) % 26, chr((2 * 7 + 5 * 4) % 26 + 65)))
print()
print("a key only works if its determinant has an inverse mod 26:")
print("%-22s %6s %10s" % ("matrix", "det", "usable"))
for m in ([[3, 3], [2, 5]], [[2, 4], [1, 3]], [[1, 2], [3, 4]]):
d = (m[0][0] * m[1][1] - m[0][1] * m[1][0]) % 26
print("%-22s %6d %10s" % (str(m), d, "yes" if inverse_mod(d, 26) else "NO"))Practical 11, Part 1: The Substitution Ciphers
key matrix: [[3, 3], [2, 5]]
determinant mod 26 : 9
its inverse mod 26 : 3 (because 9 * 3 = 27 = 1 mod 26)
inverse key matrix : [[15, 17], [20, 9]]
plaintext : HELPXX
as numbers : [7, 4, 11, 15, 23, 23]
ciphertext : HIATIF [7, 8, 0, 19, 8, 5]
decrypted : HELPXX
one block by hand: HE is [7, 4]
x = (3*7 + 3*4) mod 26 = 33 mod 26 = 7 -> H
y = (2*7 + 5*4) mod 26 = 34 mod 26 = 8 -> I
a key only works if its determinant has an inverse mod 26:
matrix det usable
[[3, 3], [2, 5]] 9 yes
[[2, 4], [1, 3]] 2 NO
[[1, 2], [3, 4]] 24 NOPractical 11, Part 1: The Substitution Ciphers
The last table is the part worth marks. A key matrix is usable only if its determinant has a multiplicative inverse modulo 26, and that happens only when the determinant shares no factor with 26. Since 26 is 2 times 13, a determinant that is even, or a multiple of 13, has no inverse: the second matrix has determinant 2 and the third 24, and neither can ever be decrypted. The program would happily encrypt with either, and the message would be lost for good.
Hill's strength is that it spreads: change one letter of the plaintext and every letter of that block changes. Its weakness is that it is linear, so an attacker with a few plaintext and ciphertext pairs can solve for the key by linear algebra.
Cipher 5: Vigenere, a Caesar whose shift keeps changing
Write the key under the plaintext, repeating it, and shift each letter by its own key letter.
"""The Vigenere cipher: a Caesar shift that changes with every letter."""
def vigenere(text, key, sign=+1):
out, k = [], 0
for ch in text.upper():
if not ch.isalpha():
out.append(ch)
continue
shift = ord(key[k % len(key)].upper()) - 65
out.append(chr((ord(ch) - 65 + sign * shift) % 26 + 65))
k += 1
return "".join(out)
PLAIN = "ATTACKATDAWN"
KEY = "LEMON"
cipher = vigenere(PLAIN, KEY)
print("plaintext :", PLAIN)
print("key :", KEY, "repeated:", (KEY * 3)[:len(PLAIN)])
print("ciphertext :", cipher)
print("decrypted :", vigenere(cipher, KEY, -1))
print()
print("%-8s %6s %8s %8s %8s" % ("plain", "key", "p", "k", "cipher"))
k = 0
for ch in PLAIN:
kc = KEY[k % len(KEY)]
p, s = ord(ch) - 65, ord(kc) - 65
print("%-8s %6s %8d %8d %8s" % (ch, kc, p, s, chr((p + s) % 26 + 65)))
k += 1
print()
print("the same plaintext letter does NOT always give the same ciphertext letter:")
for letter in "AT":
pairs = [(i, cipher[i]) for i, ch in enumerate(PLAIN) if ch == letter]
print(" %s appears at %s and becomes %s"
% (letter, [i for i, _ in pairs], "".join(c for _, c in pairs)))
print()
print("but a repeated key repeats the pattern, and that is how it is broken:")
long_plain = "THETHETHETHETHETHE"
print(" plaintext :", long_plain)
print(" ciphertext :", vigenere(long_plain, KEY))
import math
period = math.lcm(len(KEY), 3)
print(" the key is %d letters and THE is 3, so the whole pattern repeats every %d"
% (len(KEY), period))
c = vigenere(long_plain, KEY)
print(" and it does: positions 0 and %d are %r and %r"
% (period, c[0:3], c[period:period + 3]))plaintext : ATTACKATDAWN
key : LEMON repeated: LEMONLEMONLE
ciphertext : LXFOPVEFRNHR
decrypted : ATTACKATDAWN
plain key p k cipher
A L 0 11 L
T E 19 4 X
T M 19 12 F
A O 0 14 O
C N 2 13 P
K L 10 11 V
A E 0 4 E
T M 19 12 F
D O 3 14 R
A N 0 13 N
W L 22 11 H
N E 13 4 R
the same plaintext letter does NOT always give the same ciphertext letter:
A appears at [0, 3, 6, 9] and becomes LOEN
T appears at [1, 2, 7] and becomes XFF
but a repeated key repeats the pattern, and that is how it is broken:
plaintext : THETHETHETHETHETHE
ciphertext : ELQHUPXTSGSIFVRELQ
the key is 5 letters and THE is 3, so the whole pattern repeats every 15
and it does: positions 0 and 15 are 'ELQ' and 'ELQ'Practical 11, Part 1: The Substitution Ciphers
A became four different letters: L, O, E and N. That is the whole improvement over a monoalphabetic cipher, and it is why Vigenere resisted frequency analysis for three centuries.
And the last block is how it falls. The key repeats, so a plaintext pattern that lines up with the key twice gives the same ciphertext twice. Here the key is five long and THE is three, so the pattern repeats every fifteen letters, and ELQ appears at position 0 and again at position 15. Measure the distance between repeated blocks in a long ciphertext, take the common factors, and you have the key length. That is the Kasiski examination, and once the key length is known the ciphertext splits into that many separate Caesar ciphers, each broken by counting.
Cipher 6: the one-time pad, which cannot be broken at all
Make the key as long as the message, choose it at random, use it once, and exclusive-or it with the message.
"""The one-time pad, and why it alone cannot be broken."""
def xor(data, key):
return bytes(a ^ b for a, b in zip(data, key))
MESSAGE = b"ATTACK AT DAWN"
KEY = bytes([0x2f, 0x71, 0x0a, 0x9c, 0x5b, 0xe4, 0x33, 0x18,
0xa7, 0x06, 0xd2, 0x4f, 0x91, 0x68])
cipher = xor(MESSAGE, KEY)
print("message :", MESSAGE.decode())
print("message hex:", MESSAGE.hex())
print("key hex :", KEY.hex())
print("cipher hex :", cipher.hex())
print("decrypted :", xor(cipher, KEY).decode())
print()
print("the same ciphertext, with a DIFFERENT key, gives a different sensible message:")
OTHER = b"RETREAT AT ONCE"[:len(MESSAGE)]
fake_key = xor(cipher, OTHER)
print(" other message :", OTHER.decode())
print(" the key that would produce it:", fake_key.hex())
print(" and it really does:", xor(cipher, fake_key).decode())
print()
print("so a ciphertext of %d bytes is consistent with EVERY %d-byte message."
% (len(cipher), len(cipher)))
print("That is why the one-time pad is unbreakable, and why it is almost never used:")
print("the key is as long as the message, must be random, and must never be reused.")
print()
print("reusing a key destroys it. Two messages under ONE key:")
M1 = b"ATTACK AT DAWN"
M2 = b"RETREAT BY SEA"
c1, c2 = xor(M1, KEY), xor(M2, KEY)
print(" cipher 1 xor cipher 2 :", xor(c1, c2).hex())
print(" message 1 xor message 2:", xor(M1, M2).hex())
print(" the key has cancelled out, and the attacker now has the two plaintexts")
print(" exclusive-ored together, with no key involved at all.")Practical 11, Part 1: The Substitution Ciphers
message : ATTACK AT DAWN
message hex: 41545441434b204154204441574e
key hex : 2f710a9c5be43318a706d24f9168
cipher hex : 6e255edd18af1359f326960ec626
decrypted : ATTACK AT DAWN
the same ciphertext, with a DIFFERENT key, gives a different sensible message:
other message : RETREAT AT ONC
the key that would produce it: 3c600a8f5dee4779b272b6418865
and it really does: RETREAT AT ONC
so a ciphertext of 14 bytes is consistent with EVERY 14-byte message.
That is why the one-time pad is unbreakable, and why it is almost never used:
the key is as long as the message, must be random, and must never be reused.
reusing a key destroys it. Two messages under ONE key:
cipher 1 xor cipher 2 : 13110013060a746116796412120f
message 1 xor message 2: 13110013060a746116796412120f
the key has cancelled out, and the attacker now has the two plaintexts
exclusive-ored together, with no key involved at all.Read the middle section twice, because it is the proof. The same fourteen bytes of ciphertext decrypt to ATTACK AT DAWN under one key and to RETREAT AT ONC under another, and the program produced the second key by exclusive-oring the ciphertext with the message it wanted. There is nothing in the ciphertext that prefers one over the other, and the same is true of every other fourteen-byte message. An attacker with unlimited computing power learns nothing except the length. That is called perfect secrecy, and the one-time pad is the only cipher that has it.
And the last section is why nobody uses it. Encrypt two messages with one key and the key cancels: the attacker gets the two plaintexts exclusive-ored together, which is a puzzle a person can often solve by hand. The printed hex proves the cancellation exactly: cipher1 xor cipher2 and message1 xor message2 are the same bytes.
So the one-time pad is unbreakable and almost useless: the key is as long as the message, so if you have a safe way to deliver the key you had a safe way to deliver the message.
The comparison to put in the journal
| Cipher | Key | Key space | Broken by |
|---|---|---|---|
| Caesar | one number | 25 | trying all 25 |
| Monoalphabetic | a permutation | 26 factorial | counting letters |
| Playfair | a word, as a 5 by 5 table | large | counting pairs |
| Hill | a matrix, invertible mod 26 | large | known plaintexts, by algebra |
| Vigenere | a word | 26 to the key length | Kasiski, then counting |
| One-time pad | random, message length, used once | enormous | nothing, if the rules hold |
Practical 11, Part 1: The Substitution Ciphers
Procedure
- Implement Caesar. Encrypt, decrypt, and print all 25 decryptions of one ciphertext.
- Implement the general monoalphabetic cipher with a shuffled alphabet. Print the key, encrypt, decrypt.
- Compute 26 factorial and say what it means for brute force.
- Count the ciphertext letters, map the commonest to E and downward, and record how much of the message becomes readable.
- Build the Playfair table from a key word, and encrypt a message with the rule for each pair printed.
- Implement Hill with a 2 by 2 key, compute the determinant and its inverse modulo 26, decrypt, and test three matrices for usability.
- Implement Vigenere, print the per-letter table, and show one plaintext letter becoming several different ciphertext letters.
- Implement the one-time pad. Produce a second key that decrypts the same ciphertext to a different message, and show the key cancelling when it is reused.
Observations
| Cipher | Result |
|---|---|
| Caesar, key 3, plaintext | MEET ME AT THE LIBRARY AT FOUR |
| Caesar, key 3, ciphertext | PHHW PH DW WKH OLEUDUB DW IRXU |
| Caesar brute force | all 25 shifts printed, the English one obvious |
| Monoalphabetic key space | 26 factorial, about 4.03 times 10 to the 26 |
| Commonest ciphertext letter | J at 16.9 per cent, which is E |
| Frequency guess | 4 of 22 letters right at once |
| Playfair table from MONARCHY | I and J share one square |
| Playfair, ATTACK AT DAWN | RSSRDERSBRNY, 5 rectangles, 1 column |
| Hill key [[3, 3], [2, 5]] | determinant 9, inverse 3, HELPXX becomes HIATIF |
| Hill, determinant 2 or 24 | no inverse modulo 26, undecryptable |
| Vigenere, key LEMON | ATTACKATDAWN becomes LXFOPVEFRNHR |
| Vigenere, the letter A | becomes L, O, E and N at different positions |
| One-time pad | one ciphertext, two keys, two sensible messages |
| Key reused | cipher1 xor cipher2 equals message1 xor message2 exactly |
Result
Six substitution ciphers were implemented, and each was used to encrypt and then decrypt a message. Caesar's entire key space of 25 was searched and the plaintext recovered. The monoalphabetic cipher's key space was computed as 26 factorial, and the cipher was nevertheless attacked by letter frequency, which placed 4 of 22 letters correctly and made the word THE readable at once. Playfair was built from a key word with the rule applied to each pair recorded, Hill was used with an invertible matrix and two uninvertible ones were identified before use, and Vigenere was shown to map one plaintext letter to four different ciphertext letters while repeating its pattern with the key length. The one-time pad was shown to have perfect secrecy by producing a second key that decrypts the same ciphertext to a different sensible message, and to be destroyed by key reuse, with the cancellation printed in hexadecimal.
Practical 11, Part 1: The Substitution Ciphers
Where marks are lost
A negative modulus in C or Java. Python's % returns a non-negative result; most other languages do not. Add 26 before taking the modulus.
Forgetting that Playfair merges I and J. 26 letters do not fit in 25 squares, and a table with both is wrong.
Not splitting a doubled letter in Playfair. A pair like LL has no rule. Insert an X.
A Hill key with an even determinant. It encrypts and can never be decrypted. Check the determinant before using the key.
Claiming a large key space means a strong cipher. 26 factorial fell to a letter count in one page.
Reusing a one-time pad key. It stops being a one-time pad and the key cancels out.
Calling the frequency attack automatic. It gives a start. Finishing it is a person reading the partly decrypted text.
Not stating Kerckhoffs's principle. The examiner is entitled to assume the algorithm is public. Only the key is secret.
For the journal
Aim; the five terms and Kerckhoffs's principle; each cipher in turn with its key, its formula, its program, and its output showing both encryption and decryption; the 25-line Caesar brute force; 26 factorial and the frequency attack with the partly decrypted text; the Playfair table and the rule for each pair; the Hill determinant, its inverse and the table of usable and unusable matrices; the Vigenere per-letter table and the repeated block; the one-time pad's two keys and the key-reuse cancellation; the comparison table; the observation table; the result.
Quick revision
- Substitution replaces letters; transposition moves them.
- Kerckhoffs: the algorithm is public, only the key is secret.
- Caesar: c = (p + k) mod 26, key space 25, broken by trying all of them.
- Monoalphabetic: key space 26 factorial, broken by counting letters, because E is always the same letter.
- Playfair: a 5 by 5 table from a key word, I and J together, pairs, three rules, X splits a double.
- Hill: blocks times a matrix mod 26. The key is usable only if the determinant has an inverse mod 26.
- Vigenere: a repeating key, so one letter becomes several. Broken by Kasiski, then by counting.
- One-time pad: random key, as long as the message, used once. Perfect secrecy.
- Reusing a pad key cancels it and gives the attacker the two plaintexts exclusive-ored.
- A big key space is an upper bound on the work, not a guarantee.
Questions you must be able to answer
1. State Kerckhoffs's principle and why it matters. That a cipher must be secure even when the attacker knows the algorithm, so only the key is secret. It matters because algorithms leak, get reverse engineered and get published, while a key can be changed.
Practical 11, Part 1: The Substitution Ciphers
2. How large is Caesar's key space, and what does that make it worth? Twenty-five useful keys. A program prints all of them in a moment, as this one did, so it has no security.
3. The monoalphabetic cipher has 26 factorial keys. Why is it still weak? Because each plaintext letter always becomes the same ciphertext letter, so the frequency pattern of English survives encryption. Counting letters gives the commonest few immediately.
4. Why does Playfair put I and J in the same square? Because the table is 5 by 5, which is 25 squares, and there are 26 letters.
5. What happens to a doubled letter in Playfair? The pair is split by inserting an X between them, because no rule applies to a pair of identical letters.
6. When is a Hill key matrix unusable? When its determinant modulo 26 has no multiplicative inverse, which is when the determinant shares a factor with 26. A determinant of 2 or of 24 cannot be inverted, and the message cannot be recovered.
7. Why is Vigenere stronger than Caesar, and how is it broken? Because the shift changes with each letter, so one plaintext letter becomes several different ciphertext letters. It is broken by finding the key length from repeated blocks, which is the Kasiski examination, and then attacking each position as a separate Caesar cipher.
8. What does perfect secrecy mean, and which cipher here has it? That the ciphertext gives an attacker no information about the plaintext beyond its length. The one-time pad has it: the same ciphertext decrypts to every possible message of that length under some key, and this chapter produced two of them.
9. Why is the one-time pad almost never used? Because the key must be random, as long as the message, and never reused. Delivering that key securely is as hard as delivering the message.
10. What goes wrong if a one-time pad key is used twice? Exclusive-oring the two ciphertexts cancels the key and leaves the two plaintexts exclusive-ored together, which an attacker can often separate by hand. The chapter prints both sides of that equality.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.