|
| 1 | +def generate_square(keyword: str) -> list: |
| 2 | + """ |
| 3 | + Generate a 5x5 Playfair square (I/J combined) from a keyword. |
| 4 | + """ |
| 5 | + keyword = keyword.upper().replace("J", "I") |
| 6 | + seen = set() |
| 7 | + square = [] |
| 8 | + for char in keyword: |
| 9 | + if char.isalpha() and char not in seen: |
| 10 | + seen.add(char) |
| 11 | + square.append(char) |
| 12 | + for char in "ABCDEFGHIKLMNOPQRSTUVWXYZ": |
| 13 | + if char not in seen: |
| 14 | + seen.add(char) |
| 15 | + square.append(char) |
| 16 | + return [square[i : i + 5] for i in range(0, 25, 5)] |
| 17 | + |
| 18 | + |
| 19 | +def find_position(square, char): |
| 20 | + """ |
| 21 | + Return (row, col) of char in given 5x5 square. |
| 22 | + """ |
| 23 | + if char == "J": |
| 24 | + char = "I" |
| 25 | + for r in range(5): |
| 26 | + for c in range(5): |
| 27 | + if square[r][c] == char: |
| 28 | + return r, c |
| 29 | + return None |
| 30 | + |
| 31 | + |
| 32 | +def two_square_encrypt(plaintext: str, key1: str, key2: str) -> str: |
| 33 | + """ |
| 34 | + Encrypt plaintext using the Two-Square cipher. |
| 35 | +
|
| 36 | + >>> two_square_encrypt("HELLO", "KEYWORD", "EXAMPLE") |
| 37 | + 'CEYLBX' |
| 38 | + """ |
| 39 | + plaintext = plaintext.upper().replace("J", "I") |
| 40 | + plaintext = "".join([c for c in plaintext if c.isalpha()]) |
| 41 | + |
| 42 | + # split into digraphs |
| 43 | + pairs = [] |
| 44 | + i = 0 |
| 45 | + while i < len(plaintext): |
| 46 | + a = plaintext[i] |
| 47 | + b = plaintext[i + 1] if i + 1 < len(plaintext) else "X" |
| 48 | + if a == b: |
| 49 | + b = "X" |
| 50 | + i += 1 |
| 51 | + else: |
| 52 | + i += 2 |
| 53 | + pairs.append((a, b)) |
| 54 | + |
| 55 | + square1 = generate_square(key1) |
| 56 | + square2 = generate_square(key2) |
| 57 | + |
| 58 | + ciphertext = "" |
| 59 | + for a, b in pairs: |
| 60 | + r1, c1 = find_position(square1, a) |
| 61 | + r2, c2 = find_position(square2, b) |
| 62 | + ciphertext += square2[r1][c2] |
| 63 | + ciphertext += square1[r2][c1] |
| 64 | + return ciphertext |
| 65 | + |
| 66 | + |
| 67 | +def two_square_decrypt(ciphertext: str, key1: str, key2: str) -> str: |
| 68 | + """ |
| 69 | + Decrypt ciphertext using the Two-Square cipher. |
| 70 | +
|
| 71 | + >>> two_square_decrypt("CEYLBX", "KEYWORD", "EXAMPLE") |
| 72 | + 'HELLOX' |
| 73 | + """ |
| 74 | + ciphertext = ciphertext.upper().replace("J", "I") |
| 75 | + ciphertext = "".join([c for c in ciphertext if c.isalpha()]) |
| 76 | + |
| 77 | + if len(ciphertext) % 2 != 0: |
| 78 | + raise ValueError("Ciphertext must have even length") |
| 79 | + |
| 80 | + square1 = generate_square(key1) |
| 81 | + square2 = generate_square(key2) |
| 82 | + |
| 83 | + plaintext = "" |
| 84 | + for i in range(0, len(ciphertext), 2): |
| 85 | + a, b = ciphertext[i], ciphertext[i + 1] |
| 86 | + r1, c1 = find_position(square2, a) |
| 87 | + r2, c2 = find_position(square1, b) |
| 88 | + plaintext += square1[r1][c2] |
| 89 | + plaintext += square2[r2][c1] |
| 90 | + return plaintext |
| 91 | + |
| 92 | + |
| 93 | +if __name__ == "__main__": |
| 94 | + from doctest import testmod |
| 95 | + |
| 96 | + testmod() |
| 97 | + |
| 98 | + # Example usage |
| 99 | + plaintext = "HELLO" |
| 100 | + key1 = "KEYWORD" |
| 101 | + key2 = "EXAMPLE" |
| 102 | + encrypted = two_square_encrypt(plaintext, key1, key2) |
| 103 | + decrypted = two_square_decrypt(encrypted, key1, key2) |
| 104 | + |
| 105 | + print("\n\n") |
| 106 | + print("Plaintext:", plaintext) |
| 107 | + print("Encrypted:", encrypted) |
| 108 | + print("Decrypted:", decrypted) |
0 commit comments