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