Skip to content

Commit 06d84db

Browse files
committed
Improved codec: playfair
1 parent 4ba756f commit 06d84db

3 files changed

Lines changed: 101 additions & 96 deletions

File tree

src/codext/VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.16.3
1+
1.16.4

src/codext/crypto/playfair.py

Lines changed: 99 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -13,126 +13,131 @@
1313
1414
Reference: https://www.dcode.fr/playfair-cipher
1515
"""
16+
from string import ascii_uppercase as UC, digits as DG
17+
1618
from ..__common__ import *
1719

1820

1921
__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"],
2633
}
2734
__guess__ = ["playfair"]
2835

2936

3037
# 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+
}
3242

3343

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. """
3646
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]:
4554
if c not in seen:
4655
seen.add(c)
4756
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)}
8258

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]
9259

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)
9371

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]
10372

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)
10776
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]
112102
result.extend([ea, eb])
113-
r = "".join(result)
114-
return r, len(t)
103+
i += 1
104+
return (r := "".join(result)), len(r)
115105
return encode
116106

117107

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)
120111
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]
131134
result.extend([da, db])
132-
r = "".join(result)
133-
return r, len(t)
135+
i += 2
136+
return (r := "".join(result)), len(r)
134137
return decode
135138

136139

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+

src/codext/crypto/vic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,6 @@ def _decode(text, errors="strict"):
137137
return _decode
138138

139139

140-
add("vic", vic_encode, vic_decode, r"vic(?:[-_]([\^*./a-zA-Z]+))?(?:[-_]([0-9]{2}))?(?:[-_]([0-9]+T?))?$",
140+
add("vic", vic_encode, vic_decode, r"vic(?:[-_]([\^*]|[./a-zA-Z]+))?(?:[-_]([0-9]{2}))?(?:[-_]([0-9]+T?))?$",
141141
printables_rate=1., penalty=.1)
142142

0 commit comments

Comments
 (0)