Skip to content

Commit 8d0d777

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

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

ciphers/adfgvx.py

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

0 commit comments

Comments
 (0)