codebook.substitution
This module defines a number of classical substitution ciphers.
It features different types of substitution ciphers:
1""" 2This module defines a number of classical substitution ciphers. 3 4It features different types of substitution ciphers: 5 6 - Monoalphabetic: `caesar`, `generic`, `keyphrase` 7 - Polyalphabetic: `vigenere` 8 - Polygraphic: `playfair` 9""" 10import itertools 11from string import ascii_lowercase, ascii_uppercase 12 13from codebook.utils import ( 14 codegroup, 15 validate_cipher_alphabet, 16 validate_key, 17 validate_plaintext, 18) 19 20 21def _shifted_alphabet(n: int) -> str: 22 return ascii_uppercase[n:] + ascii_uppercase[:n] 23 24 25def _keyed_alphabet(key: str, *, from_start: bool) -> str: 26 seen = set(key) 27 28 if from_start: 29 remaining = [c for c in ascii_uppercase if c not in seen] 30 else: 31 start = ord(key[-1]) - 65 32 remaining = [c for c in _shifted_alphabet(start + 1) if c not in seen] 33 34 return key + "".join(remaining) 35 36 37@codegroup 38def caesar(plaintext: str, *, shift: int) -> str: 39 """Caesar cipher (page 10) 40 41 - `plaintext` is the message to be encrypted 42 - `shift` is the number of places to rotate the plain alphabet (right shift) 43 """ 44 alphabet = _shifted_alphabet(shift % 26) 45 return generic(plaintext, cipher_alphabet=alphabet) 46 47 48@codegroup 49def generic(plaintext: str, *, cipher_alphabet: str) -> str: 50 """Monoalphabetic substitution cipher (page 12) 51 52 - `plaintext` is the message to be encrypted 53 - `cipher_alphabet` is the cipher alphabet that represents the substitution 54 """ 55 plaintext = validate_plaintext(plaintext) 56 cipher_alphabet = validate_cipher_alphabet(cipher_alphabet) 57 58 mapping = str.maketrans(ascii_lowercase, cipher_alphabet) 59 60 return plaintext.translate(mapping) 61 62 63@codegroup 64def keyphrase(plaintext: str, *, key: str) -> str: 65 """Monoalphabetic substitution cipher using a keyword/keyphrase (page 13) 66 67 - `plaintext` is the message to be encrypted 68 - `key` is the keyword or keyphrase used to generate the cipher alphabet 69 70 As per the example in the book, the remainder of the alphabet starts 71 where the keyphrase ends. 72 For example, `JULIUS CAESAR` generates the `JULISCAERTVWXYZBDFGHKMNOPQ` alphabet 73 and not `JULISCAERBDFGHKMNOPQTVWXYZ`, which is a common alternative. 74 """ 75 key = validate_key(key) 76 77 alphabet = _keyed_alphabet(key, from_start=False) 78 return generic(plaintext, cipher_alphabet=alphabet) 79 80 81@codegroup 82def vigenere(plaintext: str, *, key: str) -> str: 83 """Vigenère cipher (page 48) 84 85 - `plaintext` is the message to be encrypted 86 - `key` defines the series of interwoven Caesar ciphers to be used 87 """ 88 plaintext = validate_plaintext(plaintext) 89 key = validate_key(key) 90 91 cycled_cipher_alphabet = itertools.cycle( 92 _shifted_alphabet(ord(c) - 65) for c in key 93 ) 94 95 seq = [next(cycled_cipher_alphabet)[ord(c) - 97] for c in plaintext] 96 return "".join(seq) 97 98 99@codegroup 100def playfair(plaintext: str, *, key: str) -> str: 101 """Playfair cipher (Appendix E, page 372) 102 103 - `plaintext` is the message to be encrypted 104 - `key` defines the 5x5 table used for encryption 105 106 As per the example in the book, `J` and `I` share a space on the table 107 and `x` is used as padding for *same-letter* digrams and for the *last space*, 108 if required. 109 """ 110 plaintext = validate_plaintext(plaintext) 111 key = validate_key(key) 112 113 # build matrix 114 cipher_alphabet = list(_keyed_alphabet(key, from_start=True)) 115 cipher_alphabet.remove("J") 116 117 from_char, to_char = {}, {} 118 for i, c in enumerate(cipher_alphabet): 119 y, x = divmod(i, 5) 120 from_char[c] = (x, y) 121 to_char[x, y] = c 122 123 from_char["J"] = from_char["I"] 124 125 # collect digraphs 126 digraphs = [] 127 i = 0 128 while i + 1 < len(plaintext): 129 a, b = plaintext[i], plaintext[i + 1] 130 if a != b: 131 digraphs.append(a + b) 132 i += 2 133 else: 134 digraphs.append(a + "x") 135 i += 1 136 137 if i < len(plaintext): 138 digraphs.append(plaintext[i] + "x") 139 140 # apply transfrom 141 seq = [] 142 for digraph in digraphs: 143 x, y = from_char[digraph[0].upper()] 144 v, w = from_char[digraph[1].upper()] 145 146 if y == w: 147 a = to_char[(x + 1) % 5, y] 148 b = to_char[(v + 1) % 5, y] 149 elif x == v: 150 a = to_char[x, (y + 1) % 5] 151 b = to_char[x, (w + 1) % 5] 152 else: 153 a = to_char[v, y] 154 b = to_char[x, w] 155 156 seq.append(a + b) 157 158 return "".join(seq)
@codegroup
def
caesar(plaintext: str, *, shift: int) -> str:
38@codegroup 39def caesar(plaintext: str, *, shift: int) -> str: 40 """Caesar cipher (page 10) 41 42 - `plaintext` is the message to be encrypted 43 - `shift` is the number of places to rotate the plain alphabet (right shift) 44 """ 45 alphabet = _shifted_alphabet(shift % 26) 46 return generic(plaintext, cipher_alphabet=alphabet)
Caesar cipher (page 10)
plaintext
is the message to be encryptedshift
is the number of places to rotate the plain alphabet (right shift)
@codegroup
def
generic(plaintext: str, *, cipher_alphabet: str) -> str:
49@codegroup 50def generic(plaintext: str, *, cipher_alphabet: str) -> str: 51 """Monoalphabetic substitution cipher (page 12) 52 53 - `plaintext` is the message to be encrypted 54 - `cipher_alphabet` is the cipher alphabet that represents the substitution 55 """ 56 plaintext = validate_plaintext(plaintext) 57 cipher_alphabet = validate_cipher_alphabet(cipher_alphabet) 58 59 mapping = str.maketrans(ascii_lowercase, cipher_alphabet) 60 61 return plaintext.translate(mapping)
Monoalphabetic substitution cipher (page 12)
plaintext
is the message to be encryptedcipher_alphabet
is the cipher alphabet that represents the substitution
@codegroup
def
keyphrase(plaintext: str, *, key: str) -> str:
64@codegroup 65def keyphrase(plaintext: str, *, key: str) -> str: 66 """Monoalphabetic substitution cipher using a keyword/keyphrase (page 13) 67 68 - `plaintext` is the message to be encrypted 69 - `key` is the keyword or keyphrase used to generate the cipher alphabet 70 71 As per the example in the book, the remainder of the alphabet starts 72 where the keyphrase ends. 73 For example, `JULIUS CAESAR` generates the `JULISCAERTVWXYZBDFGHKMNOPQ` alphabet 74 and not `JULISCAERBDFGHKMNOPQTVWXYZ`, which is a common alternative. 75 """ 76 key = validate_key(key) 77 78 alphabet = _keyed_alphabet(key, from_start=False) 79 return generic(plaintext, cipher_alphabet=alphabet)
Monoalphabetic substitution cipher using a keyword/keyphrase (page 13)
plaintext
is the message to be encryptedkey
is the keyword or keyphrase used to generate the cipher alphabet
As per the example in the book, the remainder of the alphabet starts
where the keyphrase ends.
For example, JULIUS CAESAR
generates the JULISCAERTVWXYZBDFGHKMNOPQ
alphabet
and not JULISCAERBDFGHKMNOPQTVWXYZ
, which is a common alternative.
@codegroup
def
vigenere(plaintext: str, *, key: str) -> str:
82@codegroup 83def vigenere(plaintext: str, *, key: str) -> str: 84 """Vigenère cipher (page 48) 85 86 - `plaintext` is the message to be encrypted 87 - `key` defines the series of interwoven Caesar ciphers to be used 88 """ 89 plaintext = validate_plaintext(plaintext) 90 key = validate_key(key) 91 92 cycled_cipher_alphabet = itertools.cycle( 93 _shifted_alphabet(ord(c) - 65) for c in key 94 ) 95 96 seq = [next(cycled_cipher_alphabet)[ord(c) - 97] for c in plaintext] 97 return "".join(seq)
Vigenère cipher (page 48)
plaintext
is the message to be encryptedkey
defines the series of interwoven Caesar ciphers to be used
@codegroup
def
playfair(plaintext: str, *, key: str) -> str:
100@codegroup 101def playfair(plaintext: str, *, key: str) -> str: 102 """Playfair cipher (Appendix E, page 372) 103 104 - `plaintext` is the message to be encrypted 105 - `key` defines the 5x5 table used for encryption 106 107 As per the example in the book, `J` and `I` share a space on the table 108 and `x` is used as padding for *same-letter* digrams and for the *last space*, 109 if required. 110 """ 111 plaintext = validate_plaintext(plaintext) 112 key = validate_key(key) 113 114 # build matrix 115 cipher_alphabet = list(_keyed_alphabet(key, from_start=True)) 116 cipher_alphabet.remove("J") 117 118 from_char, to_char = {}, {} 119 for i, c in enumerate(cipher_alphabet): 120 y, x = divmod(i, 5) 121 from_char[c] = (x, y) 122 to_char[x, y] = c 123 124 from_char["J"] = from_char["I"] 125 126 # collect digraphs 127 digraphs = [] 128 i = 0 129 while i + 1 < len(plaintext): 130 a, b = plaintext[i], plaintext[i + 1] 131 if a != b: 132 digraphs.append(a + b) 133 i += 2 134 else: 135 digraphs.append(a + "x") 136 i += 1 137 138 if i < len(plaintext): 139 digraphs.append(plaintext[i] + "x") 140 141 # apply transfrom 142 seq = [] 143 for digraph in digraphs: 144 x, y = from_char[digraph[0].upper()] 145 v, w = from_char[digraph[1].upper()] 146 147 if y == w: 148 a = to_char[(x + 1) % 5, y] 149 b = to_char[(v + 1) % 5, y] 150 elif x == v: 151 a = to_char[x, (y + 1) % 5] 152 b = to_char[x, (w + 1) % 5] 153 else: 154 a = to_char[v, y] 155 b = to_char[x, w] 156 157 seq.append(a + b) 158 159 return "".join(seq)
Playfair cipher (Appendix E, page 372)
plaintext
is the message to be encryptedkey
defines the 5x5 table used for encryption
As per the example in the book, J
and I
share a space on the table
and x
is used as padding for same-letter digrams and for the last space,
if required.