Skip to content

Commit d595668

Browse files
Copilotdhondta
andcommitted
Add Phillips cipher codec
Co-authored-by: dhondta <9108102+dhondta@users.noreply.github.com> Agent-Logs-Url: https://github.com/dhondta/python-codext/sessions/01837063-2248-4979-b695-b34e3bf223bf
1 parent 0256590 commit d595668

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
286286
- [X] `barbie-N`: aka Barbie Typewriter (*N* belongs to [1, 4])
287287
- [X] `beaufort`: aka Beaufort Cipher (variant of Vigenere Cipher)
288288
- [X] `citrix`: aka Citrix CTX1 password encoding
289+
- [X] `phillips`: aka Phillips Cipher (polyalphabetic block cipher with 8 key squares)
289290
- [X] `polybius`: aka Polybius Square Cipher
290291
- [X] `railfence`: aka Rail Fence Cipher
291292
- [X] `rotN`: aka Caesar cipher (*N* belongs to [1,25])

src/codext/crypto/__init__.py

Lines changed: 1 addition & 0 deletions
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 .phillips import *
78
from .polybius import *
89
from .railfence import *
910
from .rot import *

src/codext/crypto/phillips.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Phillips Cipher Codec - phillips content encoding.
3+
4+
The Phillips cipher is a polyalphabetic substitution cipher using 8 key
5+
squares. The first square is a 5×5 grid built from a keyword (I and J share
6+
one cell). Seven additional squares are derived by rotating every row of the
7+
previous square one step to the left. Plaintext is enciphered in bigrams,
8+
each pair using the next square in a cycle of 8. Non-alphabetic characters
9+
are passed through unchanged; J is treated as I.
10+
11+
This codec:
12+
- en/decodes strings from str to str
13+
- en/decodes strings from bytes to bytes
14+
- decodes file content to str (read)
15+
- encodes file content from str to bytes (write)
16+
17+
Reference: https://www.dcode.fr/phillips-cipher
18+
"""
19+
from ..__common__ import *
20+
21+
_ALPHA = "ABCDEFGHIKLMNOPQRSTUVWXYZ" # 25 letters; I and J share one cell
22+
_ALPHA_SET = set(_ALPHA)
23+
24+
25+
__examples__ = {
26+
'enc(phillips)': None,
27+
'enc(phillips-key)': {'ATTACK': 'BSSBIC', 'TESTME': 'QBTPLY', 'ABCDEF': 'BKDFYD'},
28+
'enc-dec(phillips-key)': ['ATTACK', 'TESTME', 'ABCDEF'],
29+
'enc-dec(phillips-secret)': ['HELLOWORLD', 'ATTACKATDAWN'],
30+
}
31+
__guess__ = ["phillips-key", "phillips-secret", "phillips-password"]
32+
33+
34+
def _build_grid(key):
35+
"""Build the initial 5×5 grid from a keyword (J treated as I)."""
36+
seen, letters = set(), []
37+
for c in key.upper().replace("J", "I") + _ALPHA:
38+
if c in _ALPHA_SET and c not in seen:
39+
letters.append(c)
40+
seen.add(c)
41+
return [letters[i * 5:(i + 1) * 5] for i in range(5)]
42+
43+
44+
def _make_grids(key):
45+
"""Return all 8 grids: the initial grid plus 7 row-rotated variants."""
46+
grid = _build_grid(key)
47+
grids = [grid]
48+
for _ in range(7):
49+
grid = [row[1:] + [row[0]] for row in grid]
50+
grids.append(grid)
51+
return grids
52+
53+
54+
def _grid_positions(grid):
55+
"""Return a mapping from letter to (row, col) for the given grid."""
56+
return {ch: (r, c) for r, row in enumerate(grid) for c, ch in enumerate(row)}
57+
58+
59+
def _process_pair(a, b, grid, decode=False):
60+
"""Encode or decode a letter pair using Playfair substitution rules.
61+
62+
Same row → each letter shifts one step right (encode) / left (decode).
63+
Same col → each letter shifts one step down (encode) / up (decode).
64+
Rectangle → each letter moves to the other's column (self-inverse).
65+
"""
66+
pos = _grid_positions(grid)
67+
r1, c1 = pos[a]
68+
r2, c2 = pos[b]
69+
d = -1 if decode else 1
70+
if r1 == r2:
71+
return grid[r1][(c1 + d) % 5], grid[r2][(c2 + d) % 5]
72+
if c1 == c2:
73+
return grid[(r1 + d) % 5][c1], grid[(r2 + d) % 5][c2]
74+
return grid[r1][c2], grid[r2][c1] # rectangle rule is its own inverse
75+
76+
77+
def phillips_encode(key):
78+
_key = (key or "").strip()
79+
# Compute grids eagerly if key is valid; otherwise defer error to call time
80+
_grids = _make_grids(_key) if _key and _key.isalpha() else None
81+
82+
def encode(text, errors="strict"):
83+
if _grids is None:
84+
raise LookupError("Bad parameter for encoding 'phillips': "
85+
"key must be a non-empty alphabetic string")
86+
t = ensure_str(text).upper().replace("J", "I")
87+
alpha = [(i, c) for i, c in enumerate(t) if c in _ALPHA_SET]
88+
# Pad to an even count with a trailing X
89+
padding_char = None
90+
if len(alpha) % 2 == 1:
91+
alpha.append((-1, "X"))
92+
enc_map = {}
93+
for pair_num, k in enumerate(range(0, len(alpha), 2)):
94+
pos1, a = alpha[k]
95+
pos2, b = alpha[k + 1]
96+
e1, e2 = _process_pair(a, b, _grids[pair_num % 8])
97+
enc_map[pos1] = e1
98+
if pos2 >= 0:
99+
enc_map[pos2] = e2
100+
else:
101+
padding_char = e2
102+
result = [enc_map.get(i, c) for i, c in enumerate(t)]
103+
if padding_char is not None:
104+
result.append(padding_char)
105+
r = "".join(result)
106+
return r, len(text)
107+
108+
return encode
109+
110+
111+
def phillips_decode(key):
112+
_key = (key or "").strip()
113+
# Compute grids eagerly if key is valid; otherwise defer error to call time
114+
_grids = _make_grids(_key) if _key and _key.isalpha() else None
115+
116+
def decode(text, errors="strict"):
117+
if _grids is None:
118+
raise LookupError("Bad parameter for decoding 'phillips': "
119+
"key must be a non-empty alphabetic string")
120+
t = ensure_str(text).upper().replace("J", "I")
121+
alpha = [(i, c) for i, c in enumerate(t) if c in _ALPHA_SET]
122+
if len(alpha) % 2 == 1:
123+
if errors == "strict":
124+
raise ValueError("phillips: encoded text must contain an even "
125+
"number of alphabetic characters")
126+
alpha = alpha[:-1]
127+
dec_map = {}
128+
for pair_num, k in enumerate(range(0, len(alpha), 2)):
129+
pos1, a = alpha[k]
130+
pos2, b = alpha[k + 1]
131+
d1, d2 = _process_pair(a, b, _grids[pair_num % 8], decode=True)
132+
dec_map[pos1] = d1
133+
dec_map[pos2] = d2
134+
result = [dec_map.get(i, c) for i, c in enumerate(t)]
135+
r = "".join(result)
136+
return r, len(text)
137+
138+
return decode
139+
140+
141+
add("phillips", phillips_encode, phillips_decode,
142+
r"^phillips(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?$", penalty=.1)

0 commit comments

Comments
 (0)