Skip to content

Commit f4a4551

Browse files
authored
Merge pull request #40 from dhondta/copilot/add-phillips-encoding
Add Phillips cipher codec
2 parents 8b627c4 + 88d0813 commit f4a4551

3 files changed

Lines changed: 129 additions & 0 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] `phillips`: aka Phillips Cipher (polyalphabetic block cipher with 8 key squares)
339340
- [X] `polybius`: aka Polybius Square Cipher
340341
- [X] `railfence`: aka Rail Fence Cipher
341342
- [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: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
22+
__examples__ = {
23+
'enc(phillips)': None,
24+
'enc(phillips-key)': {'ATTACK': 'BSSBIC', 'TESTME': 'QBTPLY', 'ABCDEF': 'BKDFYD'},
25+
'enc-dec(phillips-key)': ['ATTACK', 'TESTME', 'ABCDEF'],
26+
'enc-dec(phillips-secret)': ['HELLOWORLD', 'ATTACKATDAWN'],
27+
}
28+
__guess__ = ["phillips-key", "phillips-secret", "phillips-password"]
29+
30+
31+
_ALPHABET = "ABCDEFGHIKLMNOPQRSTUVWXYZ"
32+
33+
34+
def __make_grids(key):
35+
"""Return all 8 grids: the initial grid plus 7 row-rotated variants."""
36+
# build the initial 5×5 grid from a keyword (J treated as I)
37+
seen, letters = set(), []
38+
for c in key.upper().replace("J", "I") + _ALPHABET:
39+
if c in set(_ALPHABET) and c not in seen:
40+
letters.append(c)
41+
seen.add(c)
42+
grid = [letters[i * 5:(i + 1) * 5] for i in range(5)]
43+
# now build the other 7 row-rotated variant grids
44+
grids = [grid]
45+
for _ in range(7):
46+
grid = [row[1:] + [row[0]] for row in grid]
47+
grids.append(grid)
48+
return grids
49+
50+
51+
def __process_pair(a, b, grid, decode=False):
52+
"""Encode or decode a letter pair using Playfair substitution rules.
53+
54+
Same row → each letter shifts one step right (encode) / left (decode).
55+
Same col → each letter shifts one step down (encode) / up (decode).
56+
Rectangle → each letter moves to the other's column (self-inverse).
57+
"""
58+
pos = {ch: (r, c) for r, row in enumerate(grid) for c, ch in enumerate(row)}
59+
r1, c1 = pos[a]
60+
r2, c2 = pos[b]
61+
d = -1 if decode else 1
62+
if r1 == r2:
63+
return grid[r1][(c1 + d) % 5], grid[r2][(c2 + d) % 5]
64+
if c1 == c2:
65+
return grid[(r1 + d) % 5][c1], grid[(r2 + d) % 5][c2]
66+
return grid[r1][c2], grid[r2][c1] # rectangle rule is its own inverse
67+
68+
69+
def phillips_encode(key):
70+
_key = (key or "").strip()
71+
# Compute grids eagerly if key is valid; otherwise defer error to call time
72+
_grids = __make_grids(_key) if _key and _key.isalpha() else None
73+
def encode(text, errors="strict"):
74+
if _grids is None:
75+
raise LookupError("Bad parameter for encoding 'phillips': "
76+
"key must be a non-empty alphabetic string")
77+
t = ensure_str(text).upper().replace("J", "I")
78+
alpha = [(i, c) for i, c in enumerate(t) if c in set(_ALPHABET)]
79+
# Pad to an even count with a trailing X
80+
padding_char = None
81+
if len(alpha) % 2 == 1:
82+
alpha.append((-1, "X"))
83+
enc_map = {}
84+
for pair_num, k in enumerate(range(0, len(alpha), 2)):
85+
pos1, a = alpha[k]
86+
pos2, b = alpha[k + 1]
87+
e1, e2 = __process_pair(a, b, _grids[pair_num % 8])
88+
enc_map[pos1] = e1
89+
if pos2 >= 0:
90+
enc_map[pos2] = e2
91+
else:
92+
padding_char = e2
93+
result = [enc_map.get(i, c) for i, c in enumerate(t)]
94+
if padding_char is not None:
95+
result.append(padding_char)
96+
return "".join(result), len(text)
97+
return encode
98+
99+
100+
def phillips_decode(key):
101+
_key = (key or "").strip()
102+
# Compute grids eagerly if key is valid; otherwise defer error to call time
103+
_grids = __make_grids(_key) if _key and _key.isalpha() else None
104+
def decode(text, errors="strict"):
105+
if _grids is None:
106+
raise LookupError("Bad parameter for decoding 'phillips': "
107+
"key must be a non-empty alphabetic string")
108+
t = ensure_str(text).upper().replace("J", "I")
109+
alpha = [(i, c) for i, c in enumerate(t) if c in set(_ALPHABET)]
110+
if len(alpha) % 2 == 1:
111+
if errors == "strict":
112+
raise ValueError("phillips: encoded text must contain an even "
113+
"number of alphabetic characters")
114+
alpha = alpha[:-1]
115+
dec_map = {}
116+
for pair_num, k in enumerate(range(0, len(alpha), 2)):
117+
pos1, a = alpha[k]
118+
pos2, b = alpha[k + 1]
119+
d1, d2 = __process_pair(a, b, _grids[pair_num % 8], decode=True)
120+
dec_map[pos1] = d1
121+
dec_map[pos2] = d2
122+
return "".join(dec_map.get(i, c) for i, c in enumerate(t)), len(text)
123+
return decode
124+
125+
126+
add("phillips", phillips_encode, phillips_decode, r"^phillips(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?$", printables_rate=1.,
127+
penalty=.1)

0 commit comments

Comments
 (0)