|
13 | 13 |
|
14 | 14 | Reference: https://www.dcode.fr/playfair-cipher |
15 | 15 | """ |
| 16 | +from string import ascii_uppercase as UC, digits as DG |
| 17 | + |
16 | 18 | from ..__common__ import * |
17 | 19 |
|
18 | 20 |
|
19 | 21 | __examples__ = { |
20 | | - # Classic example from Wikipedia (key "PLAYFAIR EXAMPLE"): |
21 | | - # the EE in "TREE" is split with an X filler during encoding, so decoding |
22 | | - # exposes the filler: "TREESTUMP" → encoded → decoded as "TREXESTUMP" |
23 | | - 'enc(playfair-playfairexample)': {'HIDETHEGOLDINTHETREESTUMP': 'BMODZBXDNABEKUDMUIXMMOUVIF'}, |
24 | | - 'dec(playfair-playfairexample)': {'BMODZBXDNABEKUDMUIXMMOUVIF': 'HIDETHEGOLDINTHETREXESTUMP'}, |
25 | | - 'enc-dec(playfair-keyword)': ['INSTRUMENT'], |
| 22 | + 'dec(playfair_cipher-testkey-4)': None, |
| 23 | + 'dec(playfair)': {'SIHTHTDQCUUTUSHOHW': "THISISATESTSTRINGX", '1a': None, 'a1b': None}, |
| 24 | + 'dec(playfair-playfairexample)': {'BMODZBXDNABEKUDMUIXMMOUVIF': 'HIDETHEGOLDINTHETREXESTUMP'}, |
| 25 | + 'enc(playfair-*-5-XQ-IJ)': {'UDTUTUTGMH': "TESTSTRJNG"}, |
| 26 | + 'enc(playfair_cipher-*-5-XX)': None, |
| 27 | + 'enc(playfair_cipher-*-5-X-JJ)': None, |
| 28 | + 'enc(playfair)': {'String!': None, 'This is a test String': "SIHTHTDQCUUTUSHOHW"}, |
| 29 | + 'enc(playfair-*-5-XQ-IJ)': {'TESTSTRING': "UDTUTUTGMH"}, |
| 30 | + 'enc(playfair-playfairexample)': {'HIDETHEGOLDINTHETREESTUMP': 'BMODZBXDNABEKUDMUIXMMOUVIF'}, |
| 31 | + 'enc-dec(playfair-keyword)': ["TEST"], |
| 32 | + 'enc-dec(playfair-*-5-X-IJ)': ["TEST", "TESTSENTENCE"], |
26 | 33 | } |
27 | 34 | __guess__ = ["playfair"] |
28 | 35 |
|
29 | 36 |
|
30 | 37 | # Standard 5×5 Playfair alphabet (I and J share the same cell) |
31 | | -_DEFAULT_ALPHABET = "ABCDEFGHIKLMNOPQRSTUVWXYZ" |
| 38 | +_DEFAULT_ALPHABET = { |
| 39 | + 5: UC.replace("J", ""), |
| 40 | + 6: UC + DG, |
| 41 | +} |
32 | 42 |
|
33 | 43 |
|
34 | | -def _build_grid(key=None): |
35 | | - """Build the 5×5 Playfair grid from an optional keyword.""" |
| 44 | +def __build_grid(key=None, grid_size=5, replace_char=('J', 'I')): |
| 45 | + """Build the Playfair grid from an optional keyword. """ |
36 | 46 | seen, grid = set(), [] |
37 | | - if key: |
38 | | - for c in key.upper(): |
39 | | - if c == 'J': |
40 | | - c = 'I' |
41 | | - if c.isalpha() and c not in seen: |
42 | | - seen.add(c) |
43 | | - grid.append(c) |
44 | | - for c in _DEFAULT_ALPHABET: |
| 47 | + for c in key.upper(): |
| 48 | + if c == replace_char[0]: |
| 49 | + c = replace_char[1] |
| 50 | + if c.isalpha() and c not in seen: |
| 51 | + seen.add(c) |
| 52 | + grid.append(c) |
| 53 | + for c in _DEFAULT_ALPHABET[grid_size]: |
45 | 54 | if c not in seen: |
46 | 55 | seen.add(c) |
47 | 56 | grid.append(c) |
48 | | - pos = {grid[i]: (i // 5, i % 5) for i in range(25)} |
49 | | - return grid, pos |
50 | | - |
51 | | - |
52 | | -def _filler(c): |
53 | | - """Return the filler character for a given letter (X, or Q when the letter is X).""" |
54 | | - return 'Q' if c == 'X' else 'X' |
55 | | - |
56 | | - |
57 | | -def _make_bigrams(text): |
58 | | - """Convert plaintext to bigrams, inserting fillers for repeated-letter pairs.""" |
59 | | - chars = [] |
60 | | - for c in ensure_str(text).upper(): |
61 | | - if c == 'J': |
62 | | - chars.append('I') |
63 | | - elif c.isalpha(): |
64 | | - chars.append(c) |
65 | | - bigrams = [] |
66 | | - i = 0 |
67 | | - while i < len(chars): |
68 | | - a = chars[i] |
69 | | - if i + 1 < len(chars): |
70 | | - b = chars[i + 1] |
71 | | - if a == b: |
72 | | - bigrams.append((a, _filler(a))) |
73 | | - i += 1 |
74 | | - else: |
75 | | - bigrams.append((a, b)) |
76 | | - i += 2 |
77 | | - else: |
78 | | - bigrams.append((a, _filler(a))) |
79 | | - i += 1 |
80 | | - return bigrams |
81 | | - |
| 57 | + return grid, {grid[i]: (i // grid_size, i % grid_size) for i in range(grid_size ** 2)} |
82 | 58 |
|
83 | | -def _encode_bigram(grid, pos, a, b): |
84 | | - r_a, c_a = pos[a] |
85 | | - r_b, c_b = pos[b] |
86 | | - if r_a == r_b: |
87 | | - return grid[r_a * 5 + (c_a + 1) % 5], grid[r_b * 5 + (c_b + 1) % 5] |
88 | | - elif c_a == c_b: |
89 | | - return grid[((r_a + 1) % 5) * 5 + c_a], grid[((r_b + 1) % 5) * 5 + c_b] |
90 | | - else: |
91 | | - return grid[r_a * 5 + c_b], grid[r_b * 5 + c_a] |
92 | 59 |
|
| 60 | +def __set_params(key, grid_size, fill_chars, replace_char): |
| 61 | + if len(fc := str(fill_chars or "XQ").upper()) == 2 and fc[0] == fc[1]: |
| 62 | + raise LookupError("Bad parameter for encoding 'playfair': filling char must be a single letter or a pair of " |
| 63 | + "distinct letters") |
| 64 | + if len(rc := str(replace_char or "JI").upper()) == 2 and rc[0] == rc[1]: |
| 65 | + raise LookupError("Bad parameter for encoding 'playfair': replaced char must be a pair of distinct letters") |
| 66 | + gs = int(grid_size or 5) |
| 67 | + a = _DEFAULT_ALPHABET[gs] |
| 68 | + if len(fc) == 1: |
| 69 | + fc += a[(a.index(fc[0])+1) % len(a)] |
| 70 | + return (a if key == "*" else a[::-1] if key == "^" else key).upper(), gs, tuple(fc), tuple(rc) |
93 | 71 |
|
94 | | -def _decode_bigram(grid, pos, a, b): |
95 | | - r_a, c_a = pos[a] |
96 | | - r_b, c_b = pos[b] |
97 | | - if r_a == r_b: |
98 | | - return grid[r_a * 5 + (c_a - 1) % 5], grid[r_b * 5 + (c_b - 1) % 5] |
99 | | - elif c_a == c_b: |
100 | | - return grid[((r_a - 1) % 5) * 5 + c_a], grid[((r_b - 1) % 5) * 5 + c_b] |
101 | | - else: |
102 | | - return grid[r_a * 5 + c_b], grid[r_b * 5 + c_a] |
103 | 72 |
|
104 | | - |
105 | | -def playfair_encode(key=None): |
106 | | - grid, pos = _build_grid(key) |
| 73 | +def playfair_encode(key=None, grid_size=None, fill_chars=None, replace_char=None): |
| 74 | + key, gsize, fchars, rchar = __set_params(key, grid_size, fill_chars, replace_char) |
| 75 | + grid, pos = __build_grid(key, gsize, rchar) |
107 | 76 | def encode(text, errors="strict"): |
108 | | - t = ensure_str(text) |
109 | | - result = [] |
110 | | - for a, b in _make_bigrams(t): |
111 | | - ea, eb = _encode_bigram(grid, pos, a, b) |
| 77 | + _filler = lambda c: fchars[1] if c == fchars[0] else fchars[0] |
| 78 | + chars = [rchar[1] if c == rchar[0] else c for c in ensure_str(text).upper().replace(" ", "")] |
| 79 | + result, i, _h = [], 0, handle_error("playfair", errors) |
| 80 | + while i < len(chars): |
| 81 | + a = chars[i] |
| 82 | + if a not in grid: |
| 83 | + result.append(_h(a, i)) |
| 84 | + i += 1 |
| 85 | + continue |
| 86 | + if i + 1 < len(chars): |
| 87 | + b = chars[i+1] |
| 88 | + if a == b: |
| 89 | + b = _filler(a) |
| 90 | + else: |
| 91 | + i += 1 |
| 92 | + else: |
| 93 | + b = _filler(a) |
| 94 | + r_a, c_a = pos[a] |
| 95 | + r_b, c_b = pos[b] |
| 96 | + if r_a == r_b: |
| 97 | + ea, eb = grid[r_a * 5 + (c_a + 1) % 5], grid[r_b * 5 + (c_b + 1) % 5] |
| 98 | + elif c_a == c_b: |
| 99 | + ea, eb = grid[((r_a + 1) % 5) * 5 + c_a], grid[((r_b + 1) % 5) * 5 + c_b] |
| 100 | + else: |
| 101 | + ea, eb = grid[r_a * 5 + c_b], grid[r_b * 5 + c_a] |
112 | 102 | result.extend([ea, eb]) |
113 | | - r = "".join(result) |
114 | | - return r, len(t) |
| 103 | + i += 1 |
| 104 | + return (r := "".join(result)), len(r) |
115 | 105 | return encode |
116 | 106 |
|
117 | 107 |
|
118 | | -def playfair_decode(key=None): |
119 | | - grid, pos = _build_grid(key) |
| 108 | +def playfair_decode(key=None, grid_size=None, fill_chars=None, replace_char=None): |
| 109 | + key, gsize, _, rchar = __set_params(key, grid_size, fill_chars, replace_char) |
| 110 | + grid, pos = __build_grid(key, gsize, rchar) |
120 | 111 | def decode(text, errors="strict"): |
121 | | - t = ensure_str(text) |
122 | | - chars = [] |
123 | | - for c in t.upper(): |
124 | | - if c == 'J': |
125 | | - chars.append('I') |
126 | | - elif c.isalpha(): |
127 | | - chars.append(c) |
128 | | - result = [] |
129 | | - for i in range(0, len(chars) - 1, 2): |
130 | | - da, db = _decode_bigram(grid, pos, chars[i], chars[i + 1]) |
| 112 | + chars = [rchar[1] if c == rchar[0] else c for c in ensure_str(text)] |
| 113 | + result, i, _h = [], 0, handle_error("playfair", errors, decode=True) |
| 114 | + while i < len(chars): |
| 115 | + try: |
| 116 | + r_a, c_a = pos[chars[i]] |
| 117 | + except KeyError: |
| 118 | + result.append(_h(chars[i], i, "".join(result))) |
| 119 | + i += 1 |
| 120 | + continue |
| 121 | + try: |
| 122 | + r_b, c_b = pos[chars[i+1]] |
| 123 | + except KeyError: |
| 124 | + result.append(chars[i]) |
| 125 | + result.append(_h(chars[i+1], i+1, "".join(result))) |
| 126 | + i += 2 |
| 127 | + continue |
| 128 | + if r_a == r_b: |
| 129 | + da, db = grid[r_a * 5 + (c_a - 1) % 5], grid[r_b * 5 + (c_b - 1) % 5] |
| 130 | + elif c_a == c_b: |
| 131 | + da, db = grid[((r_a - 1) % 5) * 5 + c_a], grid[((r_b - 1) % 5) * 5 + c_b] |
| 132 | + else: |
| 133 | + da, db = grid[r_a * 5 + c_b], grid[r_b * 5 + c_a] |
131 | 134 | result.extend([da, db]) |
132 | | - r = "".join(result) |
133 | | - return r, len(t) |
| 135 | + i += 2 |
| 136 | + return (r := "".join(result)), len(r) |
134 | 137 | return decode |
135 | 138 |
|
136 | 139 |
|
137 | | -add("playfair", playfair_encode, playfair_decode, r"^playfair(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?$", |
138 | | - printables_rate=1.) |
| 140 | +add("playfair", playfair_encode, playfair_decode, |
| 141 | + r"^playfair(?:[-_]cipher)?(?:[-_]([\^*]|[a-zA-Z]+))?(?:[-_]([56]))?(?:[-_]([A-Z]{1,2}))?(?:[-_]([A-Z]{2}))?$", |
| 142 | + printables_rate=1., penalty=.1) |
| 143 | + |
0 commit comments