๐ Exercises In Digital Discipleship, Part II: Classical Cryptography
This article provides a series of easy exercises originally written 11/10/2022 on a separate blog, and repurposed for the digital discipleship program created by SevenShepherd. If you are new or havenโt gone through our fundamentals course, you may want to check that out before attempting this exercise.
โ ๏ธ Under Construction
I. Monoalphabetic Substitution Ciphers
The ClassicalCryptography
class below just supplies some getters, setters, deleters and error checking around the shift instance variable / constant.
class ClassicalCryptography:
def __init__(self, shift=13):
self.constraints(shift)
self.__shift = shift
@property
def shift(self):
return self.__shift
@staticmethod
def constraints(value: int) -> None:
if not isinstance(value, int):
raise TypeError("Must be an integer!")
if not 0 <= value <= 26:
raise ValueError("Shift must be 0-26.")
@shift.setter
def shift(self, value):
self.constraints(value)
self.__shift = value
@shift.deleter
def shift(self):
raise TypeError("Cannot delete shift.")
from ciphers.baseclass import ClassicalCryptography
class MonoalphabeticSubstitutionCipher(ClassicalCryptography):
def __init__(self, *args):
super().__init__(*args)
This is the class method we will be using to implement the other substitution ciphers when weโre not expanding them to explain their inner details.
def succinct(self, msg, decrypt=False, rotation=None):
"""
Monoalphabetic Substitution Cipher (one-liner /w list comprehension)
"""
if rotation: self.shift = rotation
# case handler
case = lambda l: ord('A') if l.isupper() else ord('a')
# right shift, encryption & decryption, characters only (one-liner)
return ''.join([(chr(((ord(l) - case(l)) + ((26 - self.shift) if decrypt else self.shift)) % 26 + case(l)) if l.isalpha() else l) for l in msg])
The code snippet below is an expanded version of the one liner presented above. This example is given so you can see the process that is taking place clearly.
def expanded(self, msg, decrypt=False, rotation=None):
"""
E_n(x) = (x+n) % 26
"""
if rotation: self.shift = rotation
# case handler
case = lambda l: ord('A') if l.isupper() else ord('a')
res = []
# iterate through each character in string
for character in msg:
# encrypt alphabetic characters only
if character.isalpha():
# transform letters into numbers
# retrieve integer representing unicode code point
# subtract correct case sensitive decimal representation
temp = ord(character) - case(character)
# handle decryption logic
if decrypt is True:
# identical to original formula in else
# reversed to achieve opposite affect
temp = (temp + (26 - self.shift)) % 26
else:
# this is where the actual modular arithmetic
# from the main formula takes place, now that
# we have the characters transformed properly
temp = (temp + self.shift) % 26
# restore unicode representation of current integer
temp = temp + case(character)
# apply inverse of ord for ciphertext character
temp = chr(temp)
res.append(temp)
else:
res.append(character)
return ''.join(res)
from ciphers.monoalphabetic import MonoalphabeticSubstitutionCipher
def main():
plaintext = "The LORD is my shepherd, I lack nothing."
monoalphabetic = MonoalphabeticSubstitutionCipher()
for i in range(1,25+1):
succinct = monoalphabetic.succinct(plaintext, rotation=i)
print(f"ROT{i:>02}: {succinct}")
expanded = monoalphabetic.expanded(plaintext, rotation=13)
print(f"secret message: {expanded}")
if __name__ == "__main__":
main()
ROT01: Uif MPSE jt nz tifqifse, J mbdl opuijoh.
ROT02: Vjg NQTF ku oa ujgrjgtf, K ncem pqvjkpi.
ROT03: Wkh ORUG lv pb vkhskhug, L odfn qrwklqj.
ROT04: Xli PSVH mw qc wlitlivh, M pego rsxlmrk.
ROT05: Ymj QTWI nx rd xmjumjwi, N qfhp stymnsl.
ROT06: Znk RUXJ oy se ynkvnkxj, O rgiq tuznotm.
ROT07: Aol SVYK pz tf zolwolyk, P shjr uvaopun.
ROT08: Bpm TWZL qa ug apmxpmzl, Q tiks vwbpqvo.
ROT09: Cqn UXAM rb vh bqnyqnam, R ujlt wxcqrwp.
ROT10: Dro VYBN sc wi crozrobn, S vkmu xydrsxq.
ROT11: Esp WZCO td xj dspaspco, T wlnv yzestyr.
ROT12: Ftq XADP ue yk etqbtqdp, U xmow zaftuzs.
ROT13: Gur YBEQ vf zl furcureq, V ynpx abguvat.
ROT14: Hvs ZCFR wg am gvsdvsfr, W zoqy bchvwbu.
ROT15: Iwt ADGS xh bn hwtewtgs, X aprz cdiwxcv.
ROT16: Jxu BEHT yi co ixufxuht, Y bqsa dejxydw.
ROT17: Kyv CFIU zj dp jyvgyviu, Z crtb efkyzex.
ROT18: Lzw DGJV ak eq kzwhzwjv, A dsuc fglzafy.
ROT19: Max EHKW bl fr laxiaxkw, B etvd ghmabgz.
ROT20: Nby FILX cm gs mbyjbylx, C fuwe hinbcha.
ROT21: Ocz GJMY dn ht nczkczmy, D gvxf ijocdib.
ROT22: Pda HKNZ eo iu odaldanz, E hwyg jkpdejc.
ROT23: Qeb ILOA fp jv pebmeboa, F ixzh klqefkd.
ROT24: Rfc JMPB gq kw qfcnfcpb, G jyai lmrfgle.
ROT25: Sgd KNQC hr lx rgdogdqc, H kzbj mnsghmf.
secret message: Gur YBEQ vf zl furcureq, V ynpx abguvat.
โ Affine Cipher
from ciphers.baseclass import ClassicalCryptography
class AffineCipher(ClassicalCryptography):
def __init__(self, *args):
super().__init__(*args)
@staticmethod
def succinct(msg, a, b, decrypt=False):
"""Affine Cipher (one-liner /w list comprehension)"""
case = lambda l: ord('A') if l.isupper() else ord('a')
# right shift, encryption & decryption, characters only (one-liner)
return ''.join([(chr( ((pow(a, -1, 26) * ((ord(l) - case(l)) - b)) if decrypt else (a * (ord(l) - case(l)) + b)) % 26 + case(l)) if l.isalpha() else l) for l in msg])
@staticmethod
def expanded(msg, a, b, decrypt=False):
"""
E(x) = (ax + b) % m
"""
case = lambda l: ord('A') if l.isupper() else ord('a')
res = []
# iterate through each character in string
for character in msg:
# encrypt alphabetic characters only
if character.isalpha():
# transform letters into numbers
temp = ord(character) - case(character)
if decrypt is False:
# apply E_n(x) = (x+n) % 26
temp = (a * temp + b) % 26
else:
# apply a**-1(x-b) % 26
temp = (pow(a, -1, 26) * (temp - b)) % 26
# restore unicode representation of current integer
temp = temp + case(character)
# apply inverse of ord for ciphertext character
temp = chr(temp)
res.append(temp)
else:
res.append(character)
return ''.join(res)
from ciphers.affine import AffineCipher
def main():
plaintext = "The LORD is my shepherd, I lack nothing."
affine = AffineCipher()
ciphertext1 = affine.succinct(plaintext, 7, 8)
ciphertext2 = affine.expanded(plaintext, 7, 8)
print(f"ciphertext: {ciphertext1}")
print(f"ciphertext: {ciphertext2}")
plaintext1 = affine.succinct(ciphertext1, 7, 8, True)
plaintext2 = affine.expanded(ciphertext2, 7, 8, True)
print(f"plaintext: {plaintext1}")
print(f"plaintext: {plaintext2}")
if __name__ == "__main__":
main()
ciphertext: Lfk HCXD me ou efkjfkxd, M hiwa vclfmvy.
ciphertext: Lfk HCXD me ou efkjfkxd, M hiwa vclfmvy.
plaintext: The LORD is my shepherd, I lack nothing.
plaintext: The LORD is my shepherd, I lack nothing.
โ Atbash
These methods belong to the same AffineCipher
class in the previous section.
def atbash(self, msg):
"""Atbash is an affine cipher with a = b = 25"""
return self.succinct(msg, 25, 25)
@staticmethod
def atbash_static(msg):
"""Atbash is an affine cipher with a = b = 25"""
case = lambda l: ord('A') if l.isupper() else ord('a')
# right shift, encryption, characters only (one-liner)
return ''.join([(chr((25 * (ord(l) - case(l)) + 25) % 26 + case(l)) if l.isalpha() else l) for l in msg])
from ciphers.affine import AffineCipher
def main():
plaintext = "The LORD is my shepherd, I lack nothing."
affine = AffineCipher()
ciphertext1 = affine.succinct(plaintext, 25, 25)
ciphertext2 = affine.expanded(plaintext, 25, 25)
ciphertext3 = affine.atbash(plaintext)
ciphertext4 = affine.atbash_static(plaintext)
print(f"ciphertext: {ciphertext1}")
print(f"ciphertext: {ciphertext2}")
print(f"ciphertext: {ciphertext3}")
print(f"ciphertext: {ciphertext4}")
plaintext1 = affine.succinct(ciphertext1, 25, 25)
plaintext2 = affine.expanded(ciphertext2, 25, 25)
plaintext3 = affine.atbash(ciphertext3)
plaintext4 = affine.atbash_static(ciphertext4)
print(f"plaintext: {plaintext1}")
print(f"plaintext: {plaintext2}")
print(f"plaintext: {plaintext3}")
print(f"plaintext: {plaintext4}")
if __name__ == "__main__":
main()
ciphertext: Gsv OLIW rh nb hsvksviw, R ozxp mlgsrmt.
ciphertext: Gsv OLIW rh nb hsvksviw, R ozxp mlgsrmt.
ciphertext: Gsv OLIW rh nb hsvksviw, R ozxp mlgsrmt.
ciphertext: Gsv OLIW rh nb hsvksviw, R ozxp mlgsrmt.
plaintext: The LORD is my shepherd, I lack nothing.
plaintext: The LORD is my shepherd, I lack nothing.
plaintext: The LORD is my shepherd, I lack nothing.
plaintext: The LORD is my shepherd, I lack nothing.
โ Caesar Cipher
The Caesar cipher is a monoalphabetic substitution cipher. The transformation is represented by a cipher alphabet which is the plain alphabet with a rotatation or shift of 3 to the left or 23 to the right. The method is named after Julius Caesar, who used it in his private correspondence. Since our program operates with only right shifts, we use 23 to encrypt and 3 to decrypt.
โIf he had anything confidential to say, he wrote it in cipher, that is, by so changing the order of the letters of the alphabet, that not a word could be made out. If anyone wishes to decipher these, and get at their meaning, he must substitute the fourth letter of the alphabet, namely D, for A, and so with the others.โ โ Suetonius, Life of Julius Caesar 56
plaintext = "abcdefghijklmnopqrstuvwxyz"
cipherObj = ClassicalCryptography(23)
ciphertext = cipherObj.monoalphabetic(plaintext)
print(f"secret message: {ciphertext}")
xyzabcdefghijklmnopqrstuvw
@staticmethod
def caesar_cipher(msg, decrypt=False):
"""Caesar cipher"""
# case handler
case = lambda l: ord('A') if l.isupper() else ord('a')
# right shift, encryption & decryption, characters only (one-liner)
return ''.join([(chr(((ord(l) - case(l)) + (3 if decrypt else 23)) % 26 + case(l)) if l.isalpha() else l) for l in msg])
@staticmethod
def caesar_cipher_expanded(msg, decrypt=False):
"""
E_n(x) = (x+23) % 26
E_n(x) = (x-3) % 26
"""
case = lambda l: ord('A') if l.isupper() else ord('a')
res = []
# iterate through each character in string
for character in msg:
# encrypt alphabetic characters only
if character.isalpha():
# transform letters into numbers
temp = ord(character) - case(character)
# handle decryption logic
if decrypt is True:
# apply E_n(x) = (x+3) % 26
temp = (temp + 3) % 26
else:
# apply E_n(x) = (x+23) % 26
temp = (temp + 23) % 26
# restore unicode representation of current integer
temp = temp + case(character)
# apply inverse of ord for ciphertext character
temp = chr(temp)
res.append(temp)
else:
res.append(character)
return ''.join(res)
@staticmethod
def caesar_translate(msg):
# make translation table
trans = str.maketrans(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
"xyzabcdefghijklmnopqrstuvwXYZABCDEFGHIJKLMNOPQRSTUVW"
)
# translate
return msg.translate(trans)
โ ROT13
plaintext = "abcdefghijklmnopqrstuvwxyz"
cipherObj = ClassicalCryptography(13)
ciphertext = cipherObj.monoalphabetic(plaintext)
print(f"secret message: {ciphertext}")
nopqrstuvwxyzabcdefghijklm
@staticmethod
def rot13(msg):
# case handler
case = lambda l: ord('A') if l.isupper() else ord('a')
# right shift, encryption & decryption, characters only (one-liner)
return ''.join([(chr(((ord(l) - case(l)) + 13) % 26 + case(l)) if l.isalpha() else l) for l in msg])
@staticmethod
def rot13_expanded(msg):
"""
E_n(x) = (x+13) % 26
"""
case = lambda l: ord('A') if l.isupper() else ord('a')
res = []
# iterate through each character in string
for character in msg:
# encrypt alphabetic characters only
if character.isalpha():
# transform letters into numbers
temp = ord(character) - case(character)
# handles encryption as well as decryption
temp = (temp + 13) % 26
# restore unicode representation of current integer
temp = temp + case(character)
# apply inverse of ord for ciphertext character
temp = chr(temp)
res.append(temp)
else:
res.append(character)
return ''.join(res)
@staticmethod
def rot13_translate(msg):
# make translation table
trans = str.maketrans(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
"nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"
)
# translate
return msg.translate(trans)
II. Polyalphabetic Substitution Ciphers
โ Beaufort Cipher
@staticmethod
def beaufort_cipher_Expanded(msg, key, decrypt=False):
"""
C_i = E_k(M_i) = (K_i - M_i) % 26
M_i = D_k(C_i) = (K_i - C_i) % 26
"""
# case handler
case = lambda l: ord('A') if l.isupper() else ord('a')
cyclekey = cycle(key)
result = []
# iterate through each character in string
for character in msg:
# encrypt alphabetic characters only
if character.isalpha():
kcharacter = next(cyclekey)
# transform letters into numbers
temp = ord(character) - case(character)
tempk = ord(kcharacter) - case(kcharacter)
# handle decryption logic
if not decrypt:
# C_i = E_k(M_i) = (K_i - M_i) % 26
temp = (tempk - temp) % 26
else:
# M_i = D_k(C_i) = (K_i - C_i) % 26
temp = (tempk - temp) % 26
# restore unicode representation of current integer
temp = temp + case(character)
# apply inverse of ord for ciphertext character
temp = chr(temp)
result.append(temp)
else:
result.append(character)
return ''.join(result)
โ Vigenรจre Cipher
from ciphers.baseclass import ClassicalCryptography
from itertools import cycle
class VigenereCipher(ClassicalCryptography):
def __init__(self, *args):
super().__init__(*args)
@staticmethod
def succinct(msg, key, decrypt=False):
"""
Vigenรจre Polyalphabetic Substitution Cipher (two-liner :D)
Walrus operator works in Python 3.8+
"""
case = lambda l: ord('A') if l.isupper() else ord('a')
wheel = cycle(key)
if not decrypt:
# encryption only, characters only (one-liner)
return ''.join([(chr(((ord(c)-case(c)) + (ord(k := next(wheel))-case(k))) % 26 + case(c)) if c.isalpha() else c) for c in msg])
else:
# decryption only, characters only (one-liner)
return ''.join([(chr(((ord(c)-case(c)) - (ord(k := next(wheel))-case(k))) % 26 + case(c)) if c.isalpha() else c) for c in msg])
Modular arithmetic
@staticmethod
def expanded(msg, key, decrypt=False):
"""
C_i = E_k(M_i) = (M_i + K_i) % 26
M_i = D_k(C_i) = (C_i - K_i) % 26
"""
# case handler
case = lambda l: ord('A') if l.isupper() else ord('a')
cyclekey = cycle(key)
result = []
# iterate through each character in string
for character in msg:
# encrypt alphabetic characters only
if character.isalpha():
# don't cycle on non-characters
kcharacter = next(cyclekey)
# transform letters into numbers
# retrieve integer representing unicode code point
# subtract correct case sensitive decimal representation
temp = ord(character) - case(character)
tempk = ord(kcharacter) - case(kcharacter)
# handle decryption logic
if not decrypt:
# C_i = E_k(M_i) = (M_i + K_i) % 26
temp = (temp + tempk) % 26
else:
# M_i = D_k(C_i) = (C_i - K_i) % 26
temp = (temp - tempk) % 26
# restore unicode representation of current integer
temp += case(character)
# apply inverse of ord for ciphertext character
temp = chr(temp)
result.append(temp)
else:
result.append(character)
return ''.join(result)
from ciphers.vigenere import VigenereCipher
def main():
plaintext = "The LORD is my shepherd, I lack nothing."
vigenere = VigenereCipher()
ciphertext1 = vigenere.succinct(plaintext, "Psalm 23:1")
ciphertext2 = vigenere.expanded(plaintext, "Psalm 23:1")
print(f"ciphertext: {ciphertext1}")
print(f"ciphertext: {ciphertext2}")
plaintext1 = vigenere.succinct(ciphertext1, "Psalm 23:1", True)
plaintext2 = vigenere.expanded(ciphertext2, "Psalm 23:1", True)
print(f"plaintext: {plaintext1}")
print(f"plaintext: {plaintext2}")
if __name__ == "__main__":
main()
ciphertext: Ize WAEI of qn khpbujxq, M ascv zbynvrv.
ciphertext: Ize WAEI of qn khpbujxq, M ascv zbynvrv.
plaintext: The LORD is my shepherd, I lack nothing.
plaintext: The LORD is my shepherd, I lack nothing.
โ Vigenรจre Cipher Beaufort Variant
@staticmethod
def beaufort(msg, key, decrypt=False):
"""
C_i = E_k(M_i) = (M_i - K_i) % 26
M_i = D_k(C_i) = (C_i + K_i) % 26
"""
# case handler
case = lambda l: ord('A') if l.isupper() else ord('a')
cyclekey = cycle(key)
result = []
# iterate through each character in string
for character in msg:
# encrypt alphabetic characters only
if character.isalpha():
kcharacter = next(cyclekey)
# transform letters into numbers
# retrieve integer representing unicode code point
# subtract correct case sensitive decimal representation
temp = ord(character) - case(character)
tempk = ord(kcharacter) - case(kcharacter)
# handle decryption logic
if not decrypt:
# M_i = D_k(C_i) = (C_i - K_i) % 26
temp = (temp - tempk) % 26
else:
# C_i = E_k(M_i) = (M_i + K_i) % 26
temp = (temp + tempk) % 26
# restore unicode representation of current integer
temp = temp + case(character)
# apply inverse of ord for ciphertext character
temp = chr(temp)
result.append(temp)
else:
result.append(character)
return ''.join(result)
from ciphers.vigenere import VigenereCipher
def main():
plaintext = "The LORD is my shepherd, I lack nothing."
vigenere = VigenereCipher()
ciphertext1 = vigenere.beaufort(plaintext, "Psalm 23:1")
ciphertext2 = vigenere.succinct(plaintext, "Psalm 23:1", True)
ciphertext3 = vigenere.expanded(plaintext, "Psalm 23:1", True)
print(f"ciphertext: {ciphertext1}")
print(f"ciphertext: {ciphertext2}")
print(f"ciphertext: {ciphertext3}")
plaintext1 = vigenere.beaufort(ciphertext1, "Psalm 23:1", True)
plaintext2 = vigenere.succinct(ciphertext2, "Psalm 23:1")
plaintext3 = vigenere.expanded(ciphertext3, "Psalm 23:1")
print(f"plaintext: {plaintext1}")
print(f"plaintext: {plaintext2}")
print(f"plaintext: {plaintext3}")
if __name__ == "__main__":
main()
ciphertext: Epe ACEY cf ij ahtduzlq, E wicz bbobvjr.
ciphertext: Epe ACEY cf ij ahtduzlq, E wicz bbobvjr.
ciphertext: Epe ACEY cf ij ahtduzlq, E wicz bbobvjr.
plaintext: The LORD is my shepherd, I lack nothing.
plaintext: The LORD is my shepherd, I lack nothing.
plaintext: The LORD is my shepherd, I lack nothing
โ Running key
III. Transposition Ciphers
โ Columnar
โ Double
โ Rail fence
โ Route
IV. Steganographic Message Encoding
โ Baconian Cipher
@staticmethod
def baconian_cipher_26(msg):
# encode only, characters only (one-liner)
return ' '.join([(dict(((c,f"{i:05b}".translate(str.maketrans("01", "AB"))) for i,c in enumerate(ascii_lowercase)))[c.lower()] if c.isalpha() else '') for c in msg])
@staticmethod
def baconian_cipher_26_expanded(msg):
def char_map():
trans = str.maketrans("01", "AB")
for i, c in enumerate(ascii_lowercase):
binary = f"{i:05b}"
yield (c,binary.translate(trans))
result = []
for character in msg:
if character.isalpha():
result.append(dict(char_map())[character.lower()])
else:
result.append(character)
return ' '.join(result)
V. One-Time-Pad