Skip to content

Commit 8ec6295

Browse files
author
Saiprashanth Pulisetti
committed
feat(ciphers): add ADFGX cipher with CLI
1 parent efcef14 commit 8ec6295

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

ciphers/adfgx.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python3
2+
"""ADFGX cipher implementation with CLI encode/decode.
3+
4+
Usage examples:
5+
python -m ciphers.adfgx encode --key squarekeyword --transposition SECRET --text "attack at once"
6+
python -m ciphers.adfgx decode --key squarekeyword --transposition SECRET --text <ciphertext>
7+
8+
The ADFGX cipher uses a 5x5 Polybius square (I/J merged) and a columnar
9+
transposition with a keyword.
10+
"""
11+
from __future__ import annotations
12+
13+
import argparse
14+
import math
15+
import sys
16+
from typing import List
17+
18+
ALPHABET = "ABCDEFGHIKLMNOPQRSTUVWXYZ" # J merged with I
19+
HEADERS = "ADFGX"
20+
21+
22+
def build_square(keyword: str) -> str:
23+
key = "".join(ch for ch in keyword.upper() if ch.isalpha())
24+
key = key.replace("J", "I")
25+
seen = set()
26+
square = []
27+
for ch in key + ALPHABET:
28+
if ch not in seen:
29+
seen.add(ch)
30+
square.append(ch)
31+
return "".join(square[:25])
32+
33+
34+
def to_pairs(text: str, square: str) -> str:
35+
out = []
36+
for ch in text.upper():
37+
if not ch.isalpha():
38+
continue
39+
ch = "I" if ch == "J" else ch
40+
idx = square.find(ch)
41+
if idx == -1:
42+
continue
43+
r, c = divmod(idx, 5)
44+
out.append(HEADERS[r])
45+
out.append(HEADERS[c])
46+
return "".join(out)
47+
48+
49+
def from_pairs(pairs: str, square: str) -> str:
50+
assert len(pairs) % 2 == 0
51+
out = []
52+
for i in range(0, len(pairs), 2):
53+
r = HEADERS.index(pairs[i])
54+
c = HEADERS.index(pairs[i + 1])
55+
out.append(square[r * 5 + c])
56+
return "".join(out)
57+
58+
59+
def columnar_transpose_encrypt(text: str, key: str) -> str:
60+
key = "".join(ch for ch in key.upper() if ch.isalpha())
61+
order = sorted(range(len(key)), key=lambda i: (key[i], i))
62+
rows = math.ceil(len(text) / len(key))
63+
grid = [list(text[i * len(key):(i + 1) * len(key)]) for i in range(rows)]
64+
# Pad with X
65+
if grid:
66+
last = grid[-1]
67+
while len(last) < len(key):
68+
last.append("X")
69+
out = []
70+
for col in order:
71+
for r in range(rows):
72+
if col < len(grid[r]):
73+
out.append(grid[r][col])
74+
return "".join(out)
75+
76+
77+
def columnar_transpose_decrypt(cipher: str, key: str) -> str:
78+
key = "".join(ch for ch in key.upper() if ch.isalpha())
79+
order = sorted(range(len(key)), key=lambda i: (key[i], i))
80+
rows = math.ceil(len(cipher) / len(key))
81+
cols = len(key)
82+
# Determine column lengths
83+
base_len = rows
84+
grid = [[None] * cols for _ in range(rows)]
85+
idx = 0
86+
for col in order:
87+
for r in range(rows):
88+
if idx < len(cipher):
89+
grid[r][col] = cipher[idx]
90+
idx += 1
91+
# Read row-wise
92+
out = []
93+
for r in range(rows):
94+
for c in range(cols):
95+
ch = grid[r][c]
96+
if ch is not None:
97+
out.append(ch)
98+
return "".join(out)
99+
100+
101+
def encode(key: str, transposition: str, text: str) -> str:
102+
square = build_square(key)
103+
pairs = to_pairs(text, square)
104+
return columnar_transpose_encrypt(pairs, transposition)
105+
106+
107+
def decode(key: str, transposition: str, text: str) -> str:
108+
square = build_square(key)
109+
pairs = columnar_transpose_decrypt(text, transposition)
110+
# Remove any padding X if odd length occurs; pairs must be even
111+
if len(pairs) % 2 == 1:
112+
pairs = pairs[:-1]
113+
return from_pairs(pairs, square)
114+
115+
116+
def parse_args(argv: List[str]) -> argparse.Namespace:
117+
parser = argparse.ArgumentParser(description="ADFGX cipher encode/decode")
118+
sub = parser.add_subparsers(dest="cmd", required=True)
119+
120+
p_enc = sub.add_parser("encode")
121+
p_enc.add_argument("--key", required=True)
122+
p_enc.add_argument("--transposition", required=True)
123+
p_enc.add_argument("--text", required=True)
124+
125+
p_dec = sub.add_parser("decode")
126+
p_dec.add_argument("--key", required=True)
127+
p_dec.add_argument("--transposition", required=True)
128+
p_dec.add_argument("--text", required=True)
129+
130+
return parser.parse_args(argv)
131+
132+
133+
def main(argv: List[str] | None = None) -> int:
134+
args = parse_args(sys.argv[1:] if argv is None else argv)
135+
if args.cmd == "encode":
136+
print(encode(args.key, args.transposition, args.text))
137+
return 0
138+
if args.cmd == "decode":
139+
print(decode(args.key, args.transposition, args.text))
140+
return 0
141+
return 2
142+
143+
144+
if __name__ == "__main__": # pragma: no cover
145+
raise SystemExit(main())

0 commit comments

Comments
 (0)