|
| 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