Skip to content

Commit 190fb9d

Browse files
committed
Added codec: enigma
1 parent 77f9813 commit 190fb9d

4 files changed

Lines changed: 181 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
353353
- [X] `chaocipher`: aka Chaocipher (two alphabets permuted after each letter)
354354
- [X] `citrix`: aka Citrix CTX1 password encoding
355355
- [X] `columnar`: aka Columnar Transposition Cipher (keyword-ordered columns ; a second keyword gives the double transposition)
356+
- [X] `enigma`: aka Enigma machine (Enigma I, M3 and M4, with reflector, rotors, rings, positions and plugboard)
356357
- [X] `foursquare`: aka Four-square Cipher (bigrams substituted with two keyed squares and two plain ones)
357358
- [X] `gronsfeld`: aka Gronsfeld Cipher (variant of Vigenere Cipher with a numeric key)
358359
- [X] `hill`: aka Hill Cipher (blocks of letters multiplied by a key matrix modulo 26)

‎docs/pages/enc/crypto.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,35 @@ The text is written in rows under a keyword, then read column by column, in the
275275

276276
-----
277277

278+
### Enigma
279+
280+
The Enigma machine is the rotor cipher machine used by the German armed forces during World War II. Each letter goes through the plugboard, the rotors (from right to left), the reflector, then back through the rotors and the plugboard. The rotors step before each letter, including the double stepping of the middle rotor ; as the reflector is an involution, the same settings decrypt the ciphertext. This codec emulates the Enigma I and M3 (3 rotors among I to VIII, reflector A, B or C) and the M4 (Beta or Gamma as the fixed fourth rotor, thin reflector B or C).
281+
282+
The settings are given with the codec name as `enigma-[reflector]-[rotors]-[rings]-[positions]-[plugboard]`, from the left to the right of the machine, each part being optional from the right:
283+
284+
- *reflector*: `a`, `b`, `c`, `bthin` or `cthin` (default: `b`)
285+
- *rotors*: their numbers (`1` to `8`), prefixed with `b` (Beta) or `g` (Gamma) for the M4 (default: `123`)
286+
- *rings*: the ring settings, as letters (`a` for 01, `b` for 02, ... ; default: `a` for each rotor)
287+
- *positions*: the initial positions, as letters (default: `a` for each rotor)
288+
- *plugboard*: the pairs of swapped letters, concatenated (e.g. `avbscg` for A-V, B-S and C-G ; default: none)
289+
290+
**Codec** | **Conversions** | **Aliases** | **Comment**
291+
:---: | :---: | --- | ---
292+
`enigma` | text <-> Enigma ciphertext | `enigma`, `enigma_cipher-c-678`, `enigma-b-245-bul-bla-avbscgdlfuhzinkmowrx`, `enigma-cthin-b568-epel-cdsz-aebfcmdqhujnlxprszvw` | Only ASCII letters are encrypted (their case is kept) ; other characters are left unchanged and do not step the rotors
293+
294+
```python
295+
>>> codext.encode("Attack at dawn", "enigma")
296+
'Bzhgno cr rtcm'
297+
>>> codext.decode("Bzhgno cr rtcm", "enigma")
298+
'Attack at dawn'
299+
>>> codext.encode("Attack at dawn", "enigma-b-245-bul-bla-avbscgdlfuhzinkmowrx")
300+
'Evhqzv rr blri'
301+
>>> codext.encode("Attack at dawn", "enigma-cthin-b568-epel-cdsz-aebfcmdqhujnlxprszvw")
302+
'Svbyeg oi nxcm'
303+
```
304+
305+
-----
306+
278307
### Four-square Cipher
279308

280309
The Four-square cipher, invented by Felix Delastelle, substitutes pairs of letters using four squares arranged in a 2x2 matrix: the upper-left and lower-right ones hold the alphabet in order, while the upper-right and lower-left ones are filled with two keywords. The first letter of each pair is located in the upper-left square and the second one in the lower-right square ; they are replaced by the letters at the other corners of the rectangle they form (in the upper-right square, then in the lower-left one).

‎src/codext/crypto/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .chaocipher import *
1010
from .citrix import *
1111
from .columnar import *
12+
from .enigma import *
1213
from .foursquare import *
1314
from .hill import *
1415
from .nihilist import *

‎src/codext/crypto/enigma.py‎

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Enigma Codec - Enigma machine content encoding.
3+
4+
The Enigma machine is a rotor cipher machine used by the German armed forces during World War II. Each letter goes
5+
through the plugboard, the rotors (from right to left), the reflector, then the rotors again (from left to right) and
6+
the plugboard. Before each letter, the rightmost rotor steps, the middle rotor steps when the rightmost one passes its
7+
notch, and the leftmost rotor steps when the middle one passes its notch (the middle one then also steps, which is
8+
known as the double stepping anomaly). As the reflector is an involution, the same settings decrypt the ciphertext.
9+
10+
This codec emulates the Enigma I and M3 (three rotors among I to VIII, with reflector A, B or C) and the M4 (with the
11+
Beta or Gamma rotor as the fourth one, leftmost and fixed, and the thin reflector B or C). The settings are given with
12+
the codec name, as "enigma-[reflector]-[rotors]-[rings]-[positions]-[plugboard]" (from the left to the right of the
13+
machine), each part being optional from the right:
14+
- reflector: "a", "b", "c", "bthin" or "cthin" (default: "b")
15+
- rotors: their numbers (1 to 8), prefixed with "b" (Beta) or "g" (Gamma) for the M4 (default: "123")
16+
- rings: the ring settings, as letters (default: "a" for each rotor)
17+
- positions: the initial positions, as letters (default: "a" for each rotor)
18+
- plugboard: the pairs of swapped letters, concatenated (e.g. "avbscg" for A-V, B-S and C-G ; default: none)
19+
20+
Only the letters are encrypted (keeping their case) ; other characters are left unchanged and do not step the rotors.
21+
22+
This codec:
23+
- en/decodes strings from str to str
24+
- en/decodes strings from bytes to bytes
25+
- decodes file content to str (read)
26+
- encodes file content from str to bytes (write)
27+
28+
Reference: https://en.wikipedia.org/wiki/Enigma_rotor_details
29+
https://www.cryptomuseum.com/crypto/enigma/wiring.htm
30+
"""
31+
from string import ascii_uppercase as UC
32+
33+
from ..__common__ import *
34+
35+
36+
__examples__ = {
37+
'enc(enigma-d)': None,
38+
'enc(enigma-b-122)': None,
39+
'enc(enigma-b-b123)': None,
40+
'enc(enigma-bthin-123)': None,
41+
'enc(enigma-b-123-ab)': None,
42+
'enc(enigma-b-123-aaa-aaaa)': None,
43+
'enc(enigma-b-123-aaa-aaa-abc)': None,
44+
'enc(enigma-b-123-aaa-aaa-abca)': None,
45+
'enc(enigma|enigma_cipher|enigma-b-123|enigma-b-123-aaa-aaa)': {'AAAAA': "BDZGO",
46+
'Hello, world. This is a test.':
47+
"Ilbda, amtaz. Morn zd d iotu."},
48+
'enc(enigma-b-123-aaa-aaz)': {'AAAAAAAAAAAAAAAAAAAAAAAAAA': "UBDZGOWCXLTKSBTMCDLPBMUQOF"},
49+
'enc(enigma-b-123-aaa-adu)': {'AAAAA': "EQIBM"},
50+
'enc(enigma-b-123-qch-adf)': {'AAAAAAAAAAAAAAAAAAAAAAAAAA': "TEVFKUTIIWEDWVIJPMVPGDEZSP"},
51+
'enc(enigma-bthin-b123-aopd-xefq)': {'AAAAAAAAAAAAAAAAAAAAAAAAAA': "GZXGXQUSUWJPWVIGVBTUDQZNZJ"},
52+
'enc(enigma-cthin-g654-aamw-ljgu)': {'AAAAAAAAAAAAAAAAAAAAAAAAAA': "HZJLPIKWBZXNCWFFIHWLEROOZC"},
53+
'enc(enigma-bthin-g748-aiou-ivoz-wnmjlxybfpqdusihcegr)':
54+
{'AAAAAAAAAAAAAAAAAAAAAAAAAA': "GHLIMOJIUWDKLWMJGNJKDYJVDK"},
55+
'enc-dec(enigma-a-678-xyz-qmj-azbycx)': ["ATTACK AT DAWN", "This is a test !", "Café, ñandú & 10€"],
56+
}
57+
__guess__ = []
58+
59+
60+
# rotor wirings and notches (letters in the window when the next rotor steps), the fourth rotors (M4) and reflectors
61+
ROTORS = {
62+
'1': ("EKMFLGDQVZNTOWYHXUSPAIBRCJ", "Q"),
63+
'2': ("AJDKSIRUXBLHWTMCQGZNPYFVOE", "E"),
64+
'3': ("BDFHJLCPRTXVZNYEIWGAKMUSQO", "V"),
65+
'4': ("ESOVPZJAYQUIRHXLNFTGKDCMWB", "J"),
66+
'5': ("VZBRGITYUPSDNHLXAWMJQOFECK", "Z"),
67+
'6': ("JPGVOUMFYQBENHZRDKASXLICTW", "ZM"),
68+
'7': ("NZJHGRCXMYSWBOUFAIVLPEKQDT", "ZM"),
69+
'8': ("FKQHTLXOCBJSPDZRAMEWNIUYGV", "ZM"),
70+
'b': ("LEYJVCNIXWPBQMDRTAKZGFUHOS", ""),
71+
'g': ("FSOKANUERHMBTIYCWLQPZXVGJD", ""),
72+
}
73+
REFLECTORS = {
74+
'a': "EJMZALYXVBWFCRQUONTSPIKHGD",
75+
'b': "YRUHQSLDPXNGOKMIEBFZCWVJAT",
76+
'c': "FVPJIAOYEDRZXWGCTKUQSBNMHL",
77+
'bthin': "ENKQAUYWJICOPBLMDXZVFTHRGS",
78+
'cthin': "RDOBJNTKVEHMLFCWZAXGYIPSUQ",
79+
}
80+
81+
82+
def __set_params(reflector, rotors, rings, positions, plugboard):
83+
# rotors given as digits only are converted to an integer when parsing the codec name
84+
reflector, rotors = (reflector or "b").lower(), str(rotors or "123").lower()
85+
m4 = len(rotors) == 4
86+
if not re.match(r"^[bg]?[1-8]{3}$", rotors) or len(set(rotors)) != len(rotors):
87+
raise LookupError("Bad parameter for encoding 'enigma': rotors must be 3 distinct numbers from 1 to 8, "
88+
"prefixed with 'b' or 'g' for the M4")
89+
if m4 != reflector.endswith("thin"):
90+
raise LookupError("Bad parameter for encoding 'enigma': the M4 (4 rotors) requires a thin reflector, and a "
91+
"thin reflector requires the M4")
92+
rings, positions = (rings or "a" * len(rotors)).upper(), (positions or "a" * len(rotors)).upper()
93+
if len(rings) != len(rotors) or len(positions) != len(rotors):
94+
raise LookupError("Bad parameter for encoding 'enigma': rings and positions must be given for each rotor")
95+
plugboard, plugs = (plugboard or "").upper(), list(range(26))
96+
if len(plugboard) % 2 or len(set(plugboard)) != len(plugboard):
97+
raise LookupError("Bad parameter for encoding 'enigma': plugboard must be pairs of distinct letters")
98+
for a, b in zip(plugboard[::2], plugboard[1::2]):
99+
plugs[UC.index(a)], plugs[UC.index(b)] = UC.index(b), UC.index(a)
100+
return REFLECTORS[reflector], rotors, rings, positions, plugs
101+
102+
103+
def enigma(reflector=None, rotors=None, rings=None, positions=None, plugboard=None):
104+
# bad parameters are only reported when coding, as the lookup of the bare codec name must succeed (e.g. when
105+
# listing the encodings)
106+
try:
107+
refl, rotors, rings, positions, plugs = __set_params(reflector, rotors, rings, positions, plugboard)
108+
except LookupError as e:
109+
err = e
110+
def _code(text, errors="strict"):
111+
raise err
112+
return _code
113+
refl = [UC.index(c) for c in refl]
114+
# wirings (forward and backward), notches and ring settings, from the left to the right of the machine
115+
fwd = [[UC.index(c) for c in ROTORS[r][0]] for r in rotors]
116+
bwd = [[w.index(i) for i in range(26)] for w in fwd]
117+
notches = [{UC.index(c) for c in ROTORS[r][1]} for r in rotors]
118+
rings = [UC.index(c) for c in rings]
119+
def _code(text, errors="strict"):
120+
pos, r = [UC.index(c) for c in positions], []
121+
# the leftmost rotor of the M4 never steps, hence the rotors that step are always the 3 rightmost ones
122+
l, m, n = len(pos) - 3, len(pos) - 2, len(pos) - 1
123+
for c in ensure_str(text):
124+
if not (c.isascii() and c.isalpha()):
125+
r.append(c)
126+
continue
127+
# stepping, with the double stepping of the middle rotor
128+
step_m, step_l = pos[n] in notches[n] or pos[m] in notches[m], pos[m] in notches[m]
129+
pos[n] = (pos[n] + 1) % 26
130+
if step_m:
131+
pos[m] = (pos[m] + 1) % 26
132+
if step_l:
133+
pos[l] = (pos[l] + 1) % 26
134+
# plugboard, rotors from the right to the left, reflector, rotors from the left to the right, plugboard
135+
x = plugs[UC.index(c.upper())]
136+
for i in range(n, -1, -1):
137+
s = pos[i] - rings[i]
138+
x = (fwd[i][(x + s) % 26] - s) % 26
139+
x = refl[x]
140+
for i in range(n + 1):
141+
s = pos[i] - rings[i]
142+
x = (bwd[i][(x + s) % 26] - s) % 26
143+
x = UC[plugs[x]]
144+
r.append(x if c.isupper() else x.lower())
145+
return (r := "".join(r)), len(r)
146+
return _code
147+
148+
149+
add("enigma", enigma, enigma, r"^enigma(?:[-_]cipher)?(?:[-_]([a-cA-C]|[bcBC]thin))?(?:[-_]([bgBG]?\d+))?"
150+
r"(?:[-_]([a-zA-Z]+))?(?:[-_]([a-zA-Z]+))?(?:[-_]([a-zA-Z]+))?$", printables_rate=1., penalty=.1)

0 commit comments

Comments
 (0)