Skip to content

Commit 67a7869

Browse files
committed
Added new codec: vic
1 parent aeaf39e commit 67a7869

5 files changed

Lines changed: 169 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
343343
- [X] `scytaleN`: encrypts using the number of letters on the rod (*N* belongs to [1,[)
344344
- [X] `shiftN`: shift ordinals (*N* belongs to [1,255])
345345
- [X] `trithemius`: aka Trithemius Cipher (variant of Vigenere Cipher)
346+
- [X] `vic`: aka VIC Cipher
346347
- [X] `vigenere`: aka Vigenere Cipher
347348
- [X] `xorN`: XOR with a single byte (*N* belongs to [1,255])
348349

‎docs/pages/enc/crypto.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,30 @@ This is a variant of the [Vigenere Cipher](#vigenere-cipher) with key `"ABCDEFGH
274274

275275
-----
276276

277+
### VIC Cipher
278+
279+
The VIC cipher combines a straddling checkerboard substitution (converting letters to a stream of digits) and an over-encryption based on a modulo-10 sum with eventually a conversion from digits to text.
280+
281+
**Codec** | **Conversions** | **Aliases** | **Comment**
282+
:---: | :---: | --- | ---
283+
`vic` | text <-> VIC digit/text ciphertext | `vic-keyword`, `vic-key-12`, `vic-^-26-0248`, `vic-*-73-534T` | optional checkerboard keyword or alphabet marker, blank positions (exactly 2 distinct digits), over-encryption key (digits), "`T`" marker for text conversion
284+
285+
Alphabet markers:
286+
- `*`: uses "`ABC[...]XYZ./`" (uppercase letters + "`./`")
287+
- `^`: uses "`./ZYX[...]CBA`" ("`./`" + reversed uppercase letters)
288+
289+
```python
290+
>>> import codext
291+
>>> codext.encode("VICTOR", "vic-^-26-0248T")
292+
'VVXYWYY.XYJ'
293+
>>> codext.decode("VVXYWYY.XYJ", "vic-^-26-0248T")
294+
'VICTOR2' # trailing superfluous letter because of text conversion
295+
>>> codext.encode("LONGTESTSTRING", "vic-^-85-72564")
296+
'5031237844273727064454072378'
297+
```
298+
299+
-----
300+
277301
### Vigenere Cipher
278302

279303
This is a dynamic encoding, that is, it holds the key. There is no default key, meaning that `vigenere` as the encoding scheme throws a `LookupError` indicating that the _key must be a non-empty alphabetic string_.

‎src/codext/VERSION.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.16.2
1+
1.16.3

‎src/codext/crypto/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .rot import *
1111
from .scytale import *
1212
from .shift import *
13+
from .vic import *
1314
from .vigenere import *
1415
from .xor import *
1516

‎src/codext/crypto/vic.py‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Vic Cipher Codec - vic content encoding.
3+
4+
This codec:
5+
- en/decodes strings from str to str
6+
- en/decodes strings from bytes to bytes
7+
- decodes file content to str (read)
8+
- encodes file content from str to bytes (write)
9+
"""
10+
from itertools import cycle
11+
from string import ascii_uppercase as UC
12+
13+
from ..__common__ import *
14+
15+
16+
__examples__ = {
17+
'dec(vic-test-33)': None,
18+
'dec(vic-*-12-1234T': {'BBXMBDMBCBBHEMC': "THISISATEST"},
19+
'dec(vic-^-26-0248T)': {'VVXYWYY.XYJ': "VICTOR2"}, # '2' because of superfluous extra letter from substitution
20+
'enc(vic-test-12-ABC)': None,
21+
'enc(vic-*-12-1234T': {'This is a Test': "BBXMBDMBCBBHEMC"},
22+
'enc(vic-^-26-0248)': {'VICTOR': "88547440546"},
23+
'enc(vic-^-26-0248T)': {'VICTOR': "VVXYWYY.XYJ"},
24+
'enc-dec(vic-^-26)': ["TEST", "LONGTESTSTRING", "VICTOR"],
25+
'enc-dec(vic-^-85-72564)': ["TEST", "LONGTESTSTRING", "VICTOR"],
26+
'enc-dec(vic-^-85-54321T)': ["TEST", "LONGTESTSTRING", "VICTOR"],
27+
}
28+
__guess__ = ["vic"]
29+
30+
31+
def __build_alphabet(key, alphabet=UC, reverse=False):
32+
"""Return the 28-letter alphabet derived from the input key."""
33+
seen, result = set(), ""
34+
for c in (key or "").upper():
35+
if c.isalpha() and c not in seen:
36+
result += c
37+
seen.add(c)
38+
for c in (alphabet[::-1] if reverse else alphabet):
39+
if c not in seen:
40+
result += c
41+
return "./" + result if reverse else result + "./"
42+
43+
44+
def __build_checkerboard(alphabet, blank1=1, blank2=2):
45+
"""Build encode/decode lookup tables for the straddling checkerboard.
46+
47+
Layout with digit1=2 and digit2=6:
48+
0 1 [2] 3 4 5 [6] 7 8 9
49+
0: * * * * * * * *
50+
2: * * * * * * * * * *
51+
6: * * * * * * * * * *
52+
"""
53+
if blank1 == blank2:
54+
raise LookupError(f"Bad parameter for encoding 'vic': blank1 and blank2 cannot be identical")
55+
enc, dec, i = {}, {}, 0
56+
# top row
57+
for col in range(10):
58+
if col not in (blank1, blank2):
59+
enc[alphabet[i]] = str(col)
60+
i += 1
61+
# second row ; header digit is 'blank1'
62+
for col in range(10):
63+
enc[alphabet[i]] = str(blank1) + str(col)
64+
i += 1
65+
# third row ; header digit is 'blank2'
66+
for col in range(10):
67+
enc[alphabet[i]] = str(blank2) + str(col)
68+
i += 1
69+
return enc, {v: k for k, v in enc.items()}
70+
71+
72+
def __set_params(key, blanks, numeric_key):
73+
return (UC if key == "*" else UC[::-1] if key == "^" else key).upper(), \
74+
tuple(map(int, str(blanks) or "12")), \
75+
str(numeric_key or "").rstrip("T"), \
76+
key == "^", \
77+
str(numeric_key or " ")[-1] == "T"
78+
79+
80+
def vic_encode(key=None, blanks=None, numeric_key=None):
81+
key, blanks, numeric_key, rev, txt = __set_params(key, blanks, numeric_key)
82+
enc_map, dec_map = __build_checkerboard(__build_alphabet(key, reverse=rev), blanks[0], blanks[1])
83+
def _encode(text, errors="strict"):
84+
_h = handle_error("vic", errors)
85+
digits, nk_i, nk_l = [], 0, len(numeric_key or "")
86+
for pos, c in enumerate(ensure_str(text).upper().replace(" ", "")):
87+
# 1) encode with the straddling checkerboard
88+
c = enc_map[c] if c in enc_map else _h(c, pos, "".join(digits))
89+
# 2) if numeric_key is defined, over-encrypt digits
90+
if numeric_key and c.isdigit():
91+
for ci in c:
92+
digits.append(str((int(ci) + int(numeric_key[nk_i % nk_l])) % 10))
93+
nk_i += 1
94+
else:
95+
digits.append(c)
96+
r = "".join(d for d in digits if d)
97+
# 3) if text mode, convert digits to text
98+
if txt:
99+
i, r0, l, r = 0, r, len(r), ""
100+
while i < len(r0):
101+
if int(r0[i]) in blanks:
102+
r += dec_map[r0[i] + ("0" if i == l - 1 else r0[i+1])]
103+
i += 1
104+
else:
105+
r += dec_map[r0[i]]
106+
i += 1
107+
return r, len(r)
108+
return _encode
109+
110+
111+
def vic_decode(key=None, blanks=None, numeric_key=None):
112+
key, blanks, numeric_key, rev, txt = __set_params(key, blanks, numeric_key)
113+
enc_map, dec_map = __build_checkerboard(__build_alphabet(key, reverse=rev), blanks[0], blanks[1])
114+
def _decode(text, errors="strict"):
115+
_h = handle_error("vic", errors, decode=True)
116+
# 1) if text mode, convert text to digits
117+
text = "".join(enc_map[c] for c in ensure_str(text)) if txt else ensure_str(text)
118+
# 2) if numeric_key is defined, over-decrypt
119+
digits, nk_i, nk_l = [], 0, len(numeric_key or "")
120+
for pos, c in enumerate(text):
121+
if numeric_key and c.isdigit():
122+
for ci in c:
123+
digits.append(str((int(c) - int(numeric_key[nk_i % nk_l])) % 10))
124+
nk_i += 1
125+
else:
126+
digits.append(c if c.isdigit() else _h(c, pos, "".join(digits)))
127+
# 3) decode with the straddling checkerboard
128+
i, r, r0 = 0, "", "".join(d for d in digits if d)
129+
while i < (l := len(r0)):
130+
if int(r0[i]) in blanks:
131+
r += r0[i] if i == l - 1 else dec_map[r0[i] + r0[i+1]]
132+
i += 1
133+
else:
134+
r += dec_map[r0[i]] if r0[i] in dec_map else _h(r0[i], i, r)
135+
i += 1
136+
return r, len(r)
137+
return _decode
138+
139+
140+
add("vic", vic_encode, vic_decode, r"vic(?:[-_]([\^*./a-zA-Z]+))?(?:[-_]([0-9]{2}))?(?:[-_]([0-9]+T?))?$",
141+
printables_rate=1., penalty=.1)
142+

0 commit comments

Comments
 (0)