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