Skip to content

Commit 9b3919e

Browse files
authored
Merge pull request #38 from dhondta/copilot/add-playfair-encoding
Add Playfair cipher codec
2 parents 3ffc107 + f5bf1fd commit 9b3919e

4 files changed

Lines changed: 159 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
336336
- [X] `barbie-N`: aka Barbie Typewriter (*N* belongs to [1, 4])
337337
- [X] `beaufort`: aka Beaufort Cipher (variant of Vigenere Cipher)
338338
- [X] `citrix`: aka Citrix CTX1 password encoding
339+
- [X] `playfair`: aka Playfair Cipher
339340
- [X] `phillips`: aka Phillips Cipher (polyalphabetic block cipher with 8 key squares)
340341
- [X] `polybius`: aka Polybius Square Cipher
341342
- [X] `railfence`: aka Rail Fence Cipher

docs/pages/enc/crypto.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,25 @@ This is a dynamic encoding, that is, it can be called with an integer to define
257257

258258
-----
259259

260+
### Playfair Cipher
261+
262+
The Playfair cipher is a symmetric encryption method using polygram substitution with bigrams (pairs of letters), invented in 1854 by Charles Wheatstone, but popularized by Lord Playfair.
263+
264+
**Codec** | **Conversions** | **Aliases** | **Comment**
265+
:---: | :---: | --- | ---
266+
`playfair` | text <-> Playfair ciphertext | `playfair`, `playfair-playfairexample`, `playfair-keyword` | Dynamic key parameter ; letters only ; `J` is normalized to `I`
267+
268+
```python
269+
>>> codext.encode("HIDETHEGOLDINTHETREESTUMP", "playfair-playfairexample")
270+
'BMODZBXDNABEKUDMUIXMMOUVIF'
271+
>>> codext.decode("BMODZBXDNABEKUDMUIXMMOUVIF", "playfair-playfairexample")
272+
'HIDETHEGOLDINTHETREXESTUMP'
273+
>>> codext.decode(codext.encode("INSTRUMENT", "playfair-keyword"), "playfair-keyword")
274+
'INSTRUMENT'
275+
```
276+
277+
-----
278+
260279
### Trithemius Cipher
261280

262281
This is a variant of the [Vigenere Cipher](#vigenere-cipher) with key `"ABCDEFGHIJKLMNOPQRSTUVWXYZ"`.
@@ -335,4 +354,3 @@ This is a dynamic encoding, that is, it can be called with an integer to define
335354
>>> codext.encode("~bcy*cy*k*~oy~", "xor-10")
336355
'this is a test'
337356
```
338-

src/codext/crypto/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .bacon import *
55
from .barbie import *
66
from .citrix import *
7+
from .playfair import *
78
from .phillips import *
89
from .polybius import *
910
from .railfence import *
@@ -13,4 +14,3 @@
1314
from .vic import *
1415
from .vigenere import *
1516
from .xor import *
16-

src/codext/crypto/playfair.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Playfair Cipher Codec - playfair content encoding.
3+
4+
The Playfair cipher is a symmetric encryption method using polygram substitution
5+
with bigrams (pairs of letters), invented in 1854 by Charles Wheatstone, but
6+
popularized by his friend Lord Playfair.
7+
8+
This codec:
9+
- en/decodes strings from str to str
10+
- en/decodes strings from bytes to bytes
11+
- decodes file content to str (read)
12+
- encodes file content from str to bytes (write)
13+
14+
Reference: https://www.dcode.fr/playfair-cipher
15+
"""
16+
from ..__common__ import *
17+
18+
19+
__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'],
26+
}
27+
__guess__ = ["playfair"]
28+
29+
30+
# Standard 5×5 Playfair alphabet (I and J share the same cell)
31+
_DEFAULT_ALPHABET = "ABCDEFGHIKLMNOPQRSTUVWXYZ"
32+
33+
34+
def _build_grid(key=None):
35+
"""Build the 5×5 Playfair grid from an optional keyword."""
36+
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:
45+
if c not in seen:
46+
seen.add(c)
47+
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+
82+
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+
93+
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+
104+
105+
def playfair_encode(key=None):
106+
grid, pos = _build_grid(key)
107+
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)
112+
result.extend([ea, eb])
113+
r = "".join(result)
114+
return r, len(t)
115+
return encode
116+
117+
118+
def playfair_decode(key=None):
119+
grid, pos = _build_grid(key)
120+
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])
131+
result.extend([da, db])
132+
r = "".join(result)
133+
return r, len(t)
134+
return decode
135+
136+
137+
add("playfair", playfair_encode, playfair_decode, r"^playfair(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?$",
138+
printables_rate=1.)

0 commit comments

Comments
 (0)