Skip to content

Commit 507a6e7

Browse files
committed
Added codec: bifid
1 parent c7ed2c9 commit 507a6e7

4 files changed

Lines changed: 115 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
337337
- [X] `barbie-N`: aka Barbie Typewriter (*N* belongs to [1, 4])
338338
- [X] `bazeries`: aka Bazeries Cipher (two Polybius grids and a transposition keyed by a number)
339339
- [X] `beaufort`: aka Beaufort Cipher (variant of Vigenere Cipher)
340+
- [X] `bifid`: aka Bifid Cipher (Delastelle's fractionation with a 5x5 Polybius square) ; `trifid` for its 3x3x3 cube variant
340341
- [X] `citrix`: aka Citrix CTX1 password encoding
341342
- [X] `playfair`: aka Playfair Cipher
342343
- [X] `phillips`: aka Phillips Cipher (polyalphabetic block cipher with 8 key squares)

‎docs/pages/enc/crypto.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,32 @@ This is a variant of the [Vigenere Cipher](#vigenere-cipher). There is no defaul
194194

195195
-----
196196

197+
### Bifid/Trifid Cipher
198+
199+
These ciphers were invented by Felix Delastelle. Each letter is replaced by its coordinates in a 5x5 Polybius square (Bifid) or in a 3x3x3 cube (Trifid). Within each block of letters, whose size is the period (the whole text by default), the coordinates are written in rows (all the rows, then all the columns, ...), then read again as groups of coordinates that give the new letters. The square or cube is filled with an optional keyword (without duplicates), then with the remaining characters of the alphabet.
200+
201+
**Codec** | **Conversions** | **Aliases** | **Comment**
202+
:---: | :---: | --- | ---
203+
`bifid` | text <-> Bifid ciphertext | `bifid-keyword`, `bifid_cipher-keyword-5`, `bifid-7`, ... | 5x5 square ; `J` is normalized to `I` ; other characters and the case are kept
204+
`trifid` | text <-> Trifid ciphertext | `trifid-keyword`, `trifid_cipher-keyword-5`, `trifid-7`, ... | 3x3x3 cube with the 26 letters and `+` ; other characters are kept ; uppercase output (a letter can be substituted by `+`)
205+
206+
The codec name holds the optional keyword, then the optional period. The period only counts the letters of the alphabet.
207+
208+
```python
209+
>>> codext.encode("FLEEATONCE", "bifid-bgwkzqpndsioaxefclumthyvr")
210+
'UAEOLWRINS'
211+
>>> codext.encode("Flee at once!", "bifid-bgwkzqpndsioaxefclumthyvr")
212+
'Uaeo lw rins!'
213+
>>> codext.decode("UAEOLWRINS", "bifid-bgwkzqpndsioaxefclumthyvr")
214+
'FLEEATONCE'
215+
>>> codext.encode("aide-toi, le ciel t'aidera", "trifid-felixmariedelastelle-5")
216+
"FMJF-VOI, SS UFTF P'UFEQQC"
217+
>>> codext.decode("FMJFVOISSUFTFPUFEQQC", "trifid-felixmariedelastelle-5")
218+
'AIDETOILECIELTAIDERA'
219+
```
220+
221+
-----
222+
197223
### Citrix CTX1
198224

199225
This implements the Citrix CTX1 password encoding algorithm.

‎src/codext/crypto/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .bacon import *
66
from .barbie import *
77
from .bazeries import *
8+
from .bifid import *
89
from .citrix import *
910
from .playfair import *
1011
from .phillips import *

‎src/codext/crypto/bifid.py‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Bifid/Trifid Cipher Codec - bifid/trifid content encoding.
3+
4+
The Bifid and Trifid ciphers were invented by Felix Delastelle. Each letter is replaced by its coordinates in a 5x5
5+
Polybius square (Bifid ; I and J sharing a cell) or in a 3x3x3 cube (Trifid ; 26 letters and "+"). Within each block of
6+
letters (whose size is the period, the whole text by default), the coordinates are written in rows (all the rows, then
7+
all the columns, ...), then read again as groups of coordinates that give the new letters. The square or cube is
8+
filled with an optional keyword (without duplicates), then with the remaining characters of the alphabet.
9+
10+
This codec:
11+
- en/decodes strings from str to str
12+
- en/decodes strings from bytes to bytes
13+
- decodes file content to str (read)
14+
- encodes file content from str to bytes (write)
15+
16+
Reference: https://en.wikipedia.org/wiki/Bifid_cipher
17+
https://en.wikipedia.org/wiki/Trifid_cipher
18+
"""
19+
from string import ascii_uppercase as UC
20+
21+
from ..__common__ import *
22+
23+
24+
__examples1__ = {
25+
'enc(bifid-bgwkzqpndsioaxefclumthyvr)': {'FLEEATONCE': "UAEOLWRINS", 'Flee at once!': "Uaeo lw rins!"},
26+
'dec(bifid_cipher-bgwkzqpndsioaxefclumthyvr)': {'UAEOLWRINS': "FLEEATONCE"},
27+
'enc(bifid)': {'TEST': "QTUO", 'Jump': "Inuk"},
28+
'enc-dec(bifid|bifid-keyword|bifid-keyword-5|bifid-7|bifid-0)': ["", "Hello, World!", "Test 1234!",
29+
"Delastelle's ciphers"],
30+
}
31+
__examples2__ = {
32+
'enc(trifid-felixmariedelastelle-5)': {'AIDETOILECIELTAIDERA': "FMJFVOISSUFTFPUFEQQC",
33+
"aide-toi, le ciel t'aidera": "FMJF-VOI, SS UFTF P'UFEQQC"},
34+
'dec(trifid_cipher-felixmariedelastelle-5)': {'FMJFVOISSUFTFPUFEQQC': "AIDETOILECIELTAIDERA"},
35+
'enc(trifid)': {'TEST': "UTBK"},
36+
'enc-dec(trifid|trifid-keyword|trifid-keyword-5|trifid-7|trifid-0)': ["", "HELLO, WORLD!", "JUMP + 42"],
37+
}
38+
__guess1__, __guess2__ = ["bifid"], ["trifid"]
39+
40+
41+
# cipher: (alphabet, base of the coordinates, number of coordinates per letter)
42+
_PARAMS = {'bifid': (UC.replace("J", ""), 5, 2), 'trifid': (UC + "+", 3, 3)}
43+
44+
45+
def __fractionation(name, decode=False):
46+
alphabet, base, n = _PARAMS[name]
47+
def _wrapper(key=None, period=None):
48+
key, square = str(key or "").upper(), []
49+
for c in (key.replace("J", "I") if name == "bifid" else key) + alphabet:
50+
if c in alphabet and c not in square:
51+
square.append(c)
52+
coords = {c: tuple(i // base ** (n - 1 - k) % base for k in range(n)) for i, c in enumerate(square)}
53+
letters = {v: c for c, v in coords.items()}
54+
period = int(str(period or 0))
55+
def _code(text, errors="strict"):
56+
text = ensure_str(text)
57+
# only letters of the alphabet are fractionated ; other characters are kept in place
58+
slots, chars = [], []
59+
for i, c in enumerate(text):
60+
if (u := c.upper()) == "J" and name == "bifid":
61+
u = "I"
62+
if u in coords:
63+
slots.append(i)
64+
chars.append(u)
65+
result, size = [], period or len(chars) or 1
66+
for b in range(0, len(chars), size):
67+
block = chars[b:b+size]
68+
if decode:
69+
# the coordinates of the block, written in a row, hold the rows of the encoding matrix one by one
70+
digits, l = [d for c in block for d in coords[c]], len(block)
71+
result += [letters[tuple(digits[k*l+j] for k in range(n))] for j in range(l)]
72+
else:
73+
digits = [coords[c][k] for k in range(n) for c in block]
74+
result += [letters[tuple(digits[j:j+n])] for j in range(0, len(digits), n)]
75+
r = list(text)
76+
for pos, c in zip(slots, result):
77+
# with Trifid, a letter may be substituted by "+", which cannot hold its case ; hence, uppercase
78+
r[pos] = c.lower() if name == "bifid" and text[pos].islower() else c
79+
return (s := "".join(r)), len(s)
80+
return _code
81+
return _wrapper
82+
83+
84+
for _name, _ex, _gs in [("bifid", __examples1__, __guess1__), ("trifid", __examples2__, __guess2__)]:
85+
add(_name, __fractionation(_name), __fractionation(_name, True),
86+
rf"^{_name}(?:[-_]cipher)?(?:[-_]([a-zA-Z]+))?(?:[-_](\d+))?$",
87+
examples=_ex, guess=_gs, printables_rate=1., penalty=.1)

0 commit comments

Comments
 (0)