Привет всем!
Я решил написать эту статью, потому что сам разобрался со всеми деталями работы этой версии шифровальной машины, и убедился, что написанная мною программа работает идентично эмуляторам этой машины. Это было сделать непросто, так как все описания в интернете, что мне удавалось найти, в лучшем случае упускали часть важных деталей работы этого механизма, а в худшем - содержали смесь из описаний работы разных машин.
Немало кода с объяснениями, что есть в учебниках и научно-популярных статьях моделируют не саму Enigma, а какую-то более упрощенную модель. которой в истории не существовало - и результат их работы не сходился с тем, как работают точные эмуляторы, а криптостойкость ниже, чем у реальной машины. Кроме того, многие описания показались не слишком понятными - я постараюсь здесь всё описать как можно проще и яснее.
В этой статье я описал всё подробно, не опуская детали, и записывая куски кода на Python в целях иллюстрации. В конце статьи полный код, реализующий алгоритм Enigma , и инструкция, как проще освоить весь этот материал на практике начинающему (последовательность задач от частного к общему, от простого к сложному). В самом конце статьи есть список полезных по теме ссылок и очень интересное видео в русском дубляже.
Кроме того, все найденные мною в интернете эмуляторы глючат (почему-то иногда при одних и тех же настройках выдают что-то другое, при этом проблема исчезает при перезапуске). Судя по всему, у них встречаются какие-то сбои при обновлении настроек. Поэтому код на Python в этой статье может быть полезен, чтобы изучать работу шифровальной машины без глюков и, например, сделать свой собственный эмулятор.
Содержание:
1. Историческая справка
2. Общие сведения о шифре
3. Общая схема прохождения сигнала
4. Коммутационная панель
5. Устройство рефлектора
6. Устройство одного ротора как шифрующей машины
7. Взаимодействие и движение системы роторов
8. Работа машины в целом и полный код с тестами
9. Инструкция, как освоить материал этой статьи
10. Полезные ссылки по теме и видео

1. Историческая справка
Само слово Enigma в переводе на русский язык означает "Загадка".
Enigma M3 использовалась во время Второй мировой войны немецким военно-морским флотом (Kriegsmarine). Машина была совместима с машиной Enigma I, использовавшейся армией (Heer) и военно-воздушными силами (Luftwaffe). Единственное очевидное различие между армейской и военно-морской версиями заключается в том, что на роторах последней вместо цифр указаны буквы, остальные различия были незначительные и чисто технические, среди которых самый существенный, наверное, в том, что на Enigma M3 сменный рефлектор (можно использовать разные), а на Enigma I фиксированный несменяемый рефлектор.

На упрощенной принципиальной схеме различий между этими машинами нет

В то время как машины Enigma немецкого Вермахта (сухопутных войск и Luftwaffe) оснащались 5 шифровальными роторами, все военно-морские машины имели 8 роторов на выбор. Первые пять роторов (I-V) были идентичны пяти роторам, которые использовались в остальных подразделениях немецких войск, что обеспечивало некоторую совместимость, но дополнительные три ротора (VI-VIII) использовались исключительно Kriegsmarine.
Дополнительные роторы обладали чуть более сложным устройством (об этом ниже), и поэтому повышали криптостойкость машины.

Дополнительные роторы были внедрены в машины флота в 1939-м году, а 2 февраля 1942 года командование Kriegsmarine внедрило более сложную машину Enigma M4 для использования на подлодках и линкорах, остальные подразделения Kriegsmarine продолжали использовать шифровальную машину Enigma M3.
Машине M3 предшествовали машины M1 и M2, их устройство было во многом аналогичным.

Машины M1, M2 и M3 отличались в основном внешним видом и техническими деталями.



Машины Enigma M4 были в основном уничтожены немцами в конце войны, чтобы они не достались врагам. Чуть позже уничтожением шифровальных машин стали заниматься англичане, чтобы они не попали к конкурентам. Поэтому они стали музейной редкостью. Например, одна из таких машин (на следующем фото) была продана на аукционе в 2019-м году за 800 тысяч долларов. Это была одна из 15 машин, найденных в бункере на ключевой военно-морской базе Германии в норвежском Тронхейме.

В конце 2017 года в Имперском военном музее в Лондоне разработчики применили современные методы искусственного интеллекта (ИИ), чтобы построить нейросеть, способную взламывать Enigma. Используя процессы ИИ на 2000 серверах DigitalOcean, инженеры Enigma Pattern за 13 минут сделали то, на что Алан Тьюринг потратил годы. Команде исследователей потребовалось две недели на обучение машин и создание кода на Python, и ещё две недели на первую успешную попытку расшифровки сообщения.

Казалось бы, использование Enigma должно было уйти в историю, но неожиданно выяснилось, что у нее есть продолжение.
<Отредактировано позже, возможно изначально глупость написал про поставки в ВСУ механических шифраторов>
Есть сообщения, вероятно ложные, будто бы на фронте современную версию Enigma активно используют ВСУ. Аналогично есть новости из марта 2022-го, будто бы русские шифраторы довольно устаревших моделей были захвачены в российской армии возле Киева, об этом даже на сайте криптомузея написано:
"Такие устройства, как M-427, могут показаться устаревшими в эпоху интернета и персональных компьютеров (ПК), но у них есть много преимуществ перед коммерческим готовым к использованию (ГКИ) оборудованием. Их невозможно взломать, и они более устойчивы к электромагнитным импульсным (ЭМИ) атакам."
Главное препятствие к использованию на фронте механических шифраторов - это медленная скорость создания шифротекстов и наличие более эффективных цифровых шифраторов. В частности, в армиях мира используют для секретности сообщений обычно либо проводные телефоны, сообщения которых нельзя также просто перехватить, как радиосообщения, либо протокол шифрования AES256. Однако, поскольку в армиях по-прежнему существуют телеграфные станции, которые медленно передают сообщения, механические шифраторы вполне могут быть использованы для них на практике.

2. Общие сведения о шифре
Шифр Enigma - это шифр замены, что означает, что каждый шифруемый символ (латинские буквы) заменяется на какой-то другой символ, причем правило замены зависит от ключа (всех начальных настроек машины) и от положения символа в сообщении.
Важной особенностью машины является то, что она же является дешифратором: если вы повторно зашифруете зашифрованный текст с теми же настройками, вы получите открытый текст. Таким образом, чтобы расшифровать сообщение, вам нужно знать ключ (список начальных настроек, при которых производилось шифрование) и иметь такую же машину.
Сигнал проходит через 5 разных шифрующих устройств (рефлектор, 3 ротора и коммутационная панель). Рефлектор нужен для того, чтобы шифратор являлся дешифратором, 3 ротора обеспечивают неуязвимость шифра для стандартных методов криптоанализа, а коммутационная панель создает огромное количество вариантов, чтобы нельзя было расшифровать ключ методом полного перебора всех вариантов ключа. Кроме того, ключ делился еще и на 2 части - одна часть (дневной ключ) обновлялась каждые сутки и была единой, а вторая часть (ключ сообщения) была уникальной для каждого сообщения.
Процесс передачи ключа сообщения был двухэтапным и выглядел так:
Действия отправителя (шифровальщика):
-
Подготовка: выбирались две случайные трёхбуквенные комбинации:
Исходная позиция роторов: ABC.
Ключ сообщения (сеансовый ключ): XYZ.
Шифрование ключа: оператор устанавливал роторы на исходную позицию ABC и печатал на клавиатуре XYZ. На ламповой панели загорался зашифрованный ключ, например DUM.
Шифрование сообщения: роторы переводились в новую позицию, соответствующую сеансовому ключу XYZ. После этого шифровался основной текст.
Отправка: в радиограмму включались исходная позиция ABC, зашифрованный ключ DUM и основной шифротекст.
Действия получателя:
Настройка: роторы устанавливались на первую полученную комбинацию — ABC.
Расшифровка ключа: оператор печатал вторую комбинацию DUM. Машина расшифровывала её, и на панели загорался исходный сеансовый ключ — XYZ.
Расшифровка сообщения: получатель выставлял роторы на XYZ и вводил основной шифротекст, получая исходное сообщение.
3. Общая схема прохождения сигнала
Начнем со скриншота эмулятора.

В нижней части нарисована коммутационная панель, выше клавиатура, а еще выше - настройки элементов механизма (роторов и рефлектора). Менее красивый, но более удобный эмулятор есть здесь.
Давайте разберемся по порядку.
Пользователь нажимает кнопку на клавиатуре, возникает электрический сигнал, который сначала идет через коммутационную панель, затем через третий ротор (его еще называют правым), затем через второй (средний) и первый (левый). Пройдя через 3 ротора, сигнал проходит через рефлектор, затем обратно проходит роторы уже в противоположном порядке - первый, второй и третий, а потом снова проходит через коммутационную панель.
Пример реального процесса шифрования одной буквы (Q шифруется как O):
1. Вход: 'Q' -> Коммутационная панель -> 'Z'
2. Путь вперед: 'Z' -> Правый ротор -> 'J'
3. Путь вперед: 'J' -> Средний ротор -> 'N'
4. Путь вперед: 'N' -> Левый ротор -> 'N'
5. Рефлектор: 'N' -> 'K'
6. Путь назад: 'K' -> Левый ротор -> 'O'
7. Путь назад: 'O' -> Средний ротор -> 'W'
8. Путь назад: 'W' -> Правый ротор -> 'O'
9. Выход: 'O' -> Коммутационная панель -> 'O'
------------------------------
Результат: O
В моем коде этот процесс реализован следующим образом:
pos0, pos1, pos2 = self.rotor_positions[0], self.rotor_positions[1], self.rotor_positions[2]
processed_char = self._pass_plugboard(char)
processed_char = self._pass_rotor(processed_char, 2, pos2)
processed_char = self._pass_rotor(processed_char, 1, pos1)
processed_char = self._pass_rotor(processed_char, 0, pos0)
processed_char = self._pass_reflector(processed_char)
processed_char = self._pass_rotor(processed_char, 0, pos0, reverse=True)
processed_char = self._pass_rotor(processed_char, 1, pos1, reverse=True)
processed_char = self._pass_rotor(processed_char, 2, pos2, reverse=True)
encrypted_text += self._pass_plugboard(processed_char)
Здесь pos0, pos1, pos2 - это положения роторов перед прохождением сигнала.
Теперь разберемся с работой каждого элемента механизма по отдельности.
4. Коммутационная панель
Это простой шифр замены. Часть букв остаются без изменений, часть букв заменяются на какие-то другие взаимно-однозначно. В машине это было реализовано с помощью кабелей, которыми попарно соединяли буквы, которые нужно обменять при прохождении сигнала. Проще всего это реализовать с помощью словаря на Python, запишу тут словарь, описывающий соединение на скриншоте (две буквы в одной паре, если они одного цвета), и функции класса, моделирующие работу коммутационной панели:
plugboard_pairs = {
'Q': 'Z', 'Z': 'Q', 'W': 'X', 'X': 'W', 'E': 'C', 'C': 'E',
'R': 'V', 'V': 'R', 'T': 'B', 'B': 'T', 'Y': 'N', 'N': 'Y'
}
plugboard_pairs_str="QZ WX EC RV TB YN"
def _create_plugboard(self, pairs_str):
pairs = {}
if pairs_str:
for pair in pairs_str.upper().split():
if len(pair) == 2:
p1, p2 = pair[0], pair[1]
pairs[p1] = p2; pairs[p2] = p1
return pairs
def _pass_plugboard(self, char):
return self.plugboard.get(char, char)
В этом коде моделируется использование 6 кабелей. После словаря написано представление в его строковой форме, чтобы было удобно подавать на вход в класс.
Немецкие военные использовали обычно 10 кабелей. Это дает 150 738 274 937 250 (
) различных вариантов соединения, поэтому именно настройка
коммутационной панели является самой криптостойкой частью ключа. Впрочем, если для шифрования использовать только коммутационную панель, то такой шифр будет слишком легко взламываемым с помощью частотного анализа.

5. Устройство рефлектора
Это очень простая часть механизма. Рефлектор разбивает все буквы на пары и действует аналогично коммутационной панели, принимая на вход одну букву и отправляя назад другую. Рефлектор кодируется одной строкой, в которой первая буква показывает, какой букве соответствует буква 'A', вторая буква показывает, какой соответствует 'B' и так далее.
Enigma M3 использовала 2 вида рефлекторов и выбор рефлектора тоже был частью ключа.
UKW-B YRUHQSLDPXNGOKMIEBFZCWVJAT
UKW-C FVPJIAOYEDRZXWGCTKUQSBNMHL
Обратите внимание на то, что эти строки составлены таким образом, чтобы кодирование символов в каждой паре было взаимно-однозначным. Например, в строке UKW-B первым символом стоит 'Y', значит он соответствует букве 'A'. А предпоследним стоит символ 'А', предпоследняя позиция в алфавите соответствует букве 'Y'. Кодом можно описать это так:
REFLECTORS = {
"B": "YRUHQSLDPXNGOKMIEBFZCWVJAT",
"C": "FVPJIAOYEDRZXWGCTKUQSBNMHL"
}
self.reflector_wiring = self._parse_reflector(reflector_type)
def _parse_reflector(self, reflector_id):
ref_name = reflector_id.split('-')[-1].upper()
return self.REFLECTORS.get(ref_name, reflector_id)
def _pass_reflector(self, char):
return self.reflector_wiring[self.ALPHABET.find(char)]
6. Устройство одного ротора как шифрующей машины
Каждый ротор состоит из двух основных частей: внешнего алфавитного кольца и внутреннего диска с проводкой.
Внешнее алфавитное кольцо предназначено для оператора, чтобы тот выставлял начальную позицию. Кроме того, внешние кольца зацепляются друг с другом.
Внутри каждого ротора находится диск из изоляционного материала. С правой стороны диска есть 26 подпружиненных электрических контактов. С левой стороны — 26 плоских контактов. Эта сеть проводов и реализует строку ротора.

Например, строка ротора "EKMFLGDQVZNTOWYHXUSPAIBRCJ" означает:
Правый контакт 'A' (индекс 0) соединен с левым контактом 'E' (индекс 4).
Правый контакт 'B' (индекс 1) соединен с левым контактом 'K' (индекс 10).
... и так далее.
Когда сигнал идет в прямом направлении (справа налево), буквы из алфавитного кольца шифруются соответствующими буквами из строки ротора. Когда сигнал идет в обратном направлении (слева направо), происходит обратная замена.
Реализовать функцию одного ротора можно следующим образом:
def _pass_rotor(self, char, rotor_idx, position_char, reverse=False):
wiring = self.rotors_wiring[rotor_idx]
ring_offset = self.ring_offsets[rotor_idx]
position_offset = self.ALPHABET.find(position_char)
total_offset = (position_offset - ring_offset + 26) % 26
if not reverse:
entry_index = (self.ALPHABET.find(char) + total_offset) % 26
index_after_wiring = self.ALPHABET.find(wiring[entry_index])
final_index = (index_after_wiring - total_offset + 26) % 26
else:
entry_index = (self.ALPHABET.find(char) + total_offset) % 26
index_after_wiring = wiring.find(self.ALPHABET[entry_index])
final_index = (index_after_wiring - total_offset + 26) % 26
return self.ALPHABET[final_index]
Здесь wiring - это и есть внутренняя строка ротора.
Довольно интересны настройки ring_offset и position_offset.
Установочное кольцо позволяет повернуть внутреннюю проводку относительно внешней буквенной насечки на роторе. Например, если мы ставим кольцо на 'B', то вся проводка сдвигается на одну позицию, если на 'C', то на две позиции, и так далее. Это заводская настройка, которая не меняется во время работы. В эмуляторе она обозначена как Ring Setting и эта настройка является частью ключа.
position_offset — это та буква, которую оператор видит в окошке машины. Она показывает, насколько весь ротор повёрнут относительно корпуса машины. Это значение меняется с каждым нажатием клавиши. В эмуляторе обозначена как Initial Position. Начальное значение (перед первым нажатием клавиши) position_offset тоже является частью ключа.

Таким образом, весь ротор повернут относительно машины, и его внутренняя проводка повернута относительно внешней буквенной насечки.
Итоговое (чистое) смещение (total_offset) — это смещение внутренней проводки ротора относительно неподвижных контактов самой машины (статора). Оно здесь используется, чтобы найти, на какой из 26 физических контактов ротора попал сигнал.
Если на этом месте вы немного запутались, вам может помочь следующее объяснение.
Представьте себе три вложенных друг в друга диска:
Неподвижный Корпус (статор): это внешний, неподвижный диск с 26 контактами, на которые подаётся сигнал. Обозначим их A, B, C... Z. Это абсолютная система отсчёта.
Внешнее Кольцо Ротора (с буквами): это сам ротор, который мы можем вращать. На его ободе нанесены буквы A, B, C... Z. Когда мы говорим "ротор в позиции 'C'", мы имеем в виду, что буква 'C' на этом кольце видна в окошке машины. Это position_offset.
Внутренняя Проводка (сердцевина): это "мозг" ротора, сама схема перепутанных проводов. Она находится внутри ротора. У неё тоже есть 26 условных входных контактов. Это wiring.
Рассмотрим, как это работает, постепенно: посмотрим, как эти части работают вместе.
Сценарий 1: всё по нулям (базовое положение).
Позиция ротора: 'A' (position_offset = 0).
Настройка кольца (Ring Setting): 'A' (ring_offset = 0).
В этом случае все три диска идеально совмещены. Сигнал с контакта 'A' на Корпусе попадает на контакт 'A' Внешнего Кольца и дальше на вход 'A' Внутренней Проводки.
total_offset = (0 - 0) = 0. Смещения нет.
Сценарий 2: поворачиваем ротор.
Позиция ротора: 'C' (position_offset = 2).
Настройка кольца: 'A' (ring_offset = 0).
Мы повернули весь ротор (Внешнее Кольцо + Внутренняя Проводка) на две позиции вперёд.
Теперь сигнал с контакта 'A' на Корпусе попадает на контакт 'C' на Внешнем Кольце Ротора.
Поскольку Внутренняя Проводка повернулась вместе с ротором, сигнал попадает на вход 'C' (индекс 2) Внутренней Проводки.
total_offset = (2 - 0) = 2. Смещение проводки относительно корпуса равно 2.
Сценарий 3: добавляем настройку кольца (Ring Setting)
Настройка кольца смещает Внутреннюю Проводку относительно Внешнего Кольца Ротора.
Позиция ротора: 'A' (position_offset = 0).
Настройка кольца: 'B' (ring_offset = 1).
Что это значит физически? Мы говорим машине: "Сделай так, чтобы буква 'B' на Внешнем Кольце соответствовала 'нулевому' положению Внутренней Проводки". Это сдвигает проводку назад на одну позицию.
Сигнал с контакта 'A' на Корпусе попадает на контакт 'A' на Внешнем Кольце.
Но Внутренняя Проводка сдвинута назад! Поэтому сигнал попадает на вход 'Z' (или -1) Внутренней Проводки.
total_offset = (0 - 1) = -1 (или 25 по модулю 26). Смещение проводки относительно корпуса равно -1.
Общая формула: total_offset = position_offset - ring_offset
Рассмотрим пример: ротор в позиции 'D' (pos = 3), кольцо на 'B' (ring = 1).
position_offset = 3: Мы поворачиваем весь ротор (Внешнее Кольцо + Внутренняя Проводка) на 3 позиции вперёд относительно Корпуса.
ring_offset = 1: Из-за настройки кольца Внутренняя Проводка уже была смещена на 1 позицию назад относительно Внешнего Кольца.
Внутренняя проводка сместилась на 3 шага вперёд вместе с ротором, но при этом на 1 шаг назад из-за кольца. Её чистое смещение относительно неподвижного Корпуса машины составляет 3 - 1 = 2.
7. Взаимодействие и движение системы роторов
Выше уже было упомянуто, что при каждом нажатии клавиши ротор поворачивается целиком относительно корпуса машины. Это обеспечивает неприступность шифра для частотного анализа: правило замены буквы зависит от его позиции относительно начала сообщения.
Система, состоящая из трех роторов, работает еще более сложным образом.

Как работает правый ротор (быстрый)
Правый ротор является основным "двигателем" всей системы. Он устроен проще всего: при каждом нажатии оператором клавиши на клавиатуре, еще до того, как электрический сигнал начнет свой путь, механический рычаг толкает правый ротор, заставляя его провернуться ровно на один шаг вперед. Это происходит всегда, без каких-либо условий. Именно это постоянное и предсказуемое движение обеспечивает смену шифра для каждой следующей буквы в сообщении.
Как работает средний ротор
Средний ротор вращается гораздо реже и его движение подчиняется более сложным правилам. Он делает шаг вперед только в двух случаях.
Во-первых, если правый ротор, вращаясь, доходит до своей специальной позиции — "выемки", он зацепляет механизм среднего ротора, заставляя его провернуться вместе с собой при следующем нажатии.
Во-вторых (и это ключевая особенность, известная как "аномалия двойного шага"), если средний ротор сам находится на своей выемке, он также делает шаг вперед. Эти два условия гарантируют, что средний ротор вращается примерно раз в 26 нажатий, но иногда может сделать два шага подряд, усложняя предсказание его положения.
Как работает левый ротор (медленный)
Левый ротор — самый медленный из трех, и его движение происходит только в одном, строго определенном случае. Он делает шаг вперед только тогда, когда средний ротор, вращаясь, доходит до своей "выемки" и толкает его. Это означает, что для одного полного оборота левого ротора средний ротор должен совершить полный оборот, что в свою очередь требует примерно 26 оборотов правого ротора. Таким образом, левый ротор двигается крайне редко, примерно раз в 676 нажатий, обеспечивая огромный период повторения шифра.
Чтобы было понятнее, как это реализовано, рассмотрим следующий рисунок механизма

Turnover cogwheels: поворотные зубчатые колёса (Механизм переноса)
Cipher wheels: шифровальные колёса (Роторы)
Reflector: рефлектор (Отражатель)
Entry disc: входной диск (Статор)
Driving cogwheel (steps on each keypress): ведущее зубчатое колесо (поворачивается при каждом нажатии)
Spindle: ось (Вал)
Correction: коррекция / Регулировка
Маленькие белые треугольники со штриховкой: выемки (Notches)
Для машины Enigma M3 строки роторов имеют следующий вид
I EKMFLGDQVZNTOWYHXUSPAIBRCJY Начальная позиция выемки Q
II AJDKSIRUXBLHWTMCQGZNPYFVOEM Начальная позиция выемки E
III BDFHJLCPRTXVZNYEIWGAKMUSQOD Начальная позиция выемки V
IV ESOVPZJAYQUIRHXLNFTGKDCMWBR Начальная позиция выемки J
V VZBRGITYUPSDNHLXAWMJQOFECKH Начальная позиция выемки Z
VI JPGVOUMFYQBENHZRDKASXLICTWHU Начальная позиция выемки Z и M
VII NZJHGRCXMYSWBOUFAIVLPEKQDTHU Начальная позиция выемки Z и M
VIII FKQHTLXOCBJSPDZRAMEWNIUYGVHU Начальная позиция выемки Z и M
Роторы VI, VII, VIII имели 2 выемки, что усложняло их движение, так как они толкали соседний ротор чаще. Здесь мы рассмотрим только первые 5 роторов.
Движение системы роторов с одной выемкой можно запрограммировать так. Эта функция вызывается перед шифрованием каждого символа.
def _step_rotors(self):
left_pos, middle_pos, right_pos = self.rotor_positions
# self.rotor_order_names хранит имена роторов, например ["I", "III", "II"]
left_name, middle_name, right_name = self.rotor_order_names
# Проверяем, находятся ли роторы НА выемках (notch) ПЕРЕД началом вращения
middle_is_on_notch = middle_pos == self.TURNOVER_NOTCHES[middle_name]
right_is_on_notch = right_pos == self.TURNOVER_NOTCHES[right_name]
# Шаг 1: Правый ротор вращается всегда
# (Но его поворот для следующего ротора сработает только если он БЫЛ на выемке)
# Шаг 2: Средний ротор вращается, если правый БЫЛ на выемке
if right_is_on_notch:
middle_pos = self.ALPHABET[(self.ALPHABET.find(middle_pos) + 1) % 26]
# Шаг 3: Левый ротор вращается, если средний БЫЛ на выемке.
# В этот же момент из-за механики поворачивается и сам средний ротор (двойной шаг)
if middle_is_on_notch:
middle_pos = self.ALPHABET[(self.ALPHABET.find(middle_pos) + 1) % 26]
left_pos = self.ALPHABET[(self.ALPHABET.find(left_pos) + 1) % 26]
# И только, как все "последствия" положения роторов были учтены
# вращаем правый ротор
right_pos = self.ALPHABET[(self.ALPHABET.find(right_pos) + 1) % 26]
self.rotor_positions = [left_pos, middle_pos, right_pos]
8. Работа машины в целом и полный код с тестами
Итак, у нас теперь есть описание каждого элемента машины. Собираем класс на Питоне, который состоит из функций рефлектора, роторов, коммутационной панели и общего алгоритма прохождения сигнала.
class EnigmaMachine:
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
ROTORS = {
"I": "EKMFLGDQVZNTOWYHXUSPAIBRCJ",
"II": "AJDKSIRUXBLHWTMCQGZNPYFVOE",
"III": "BDFHJLCPRTXVZNYEIWGAKMUSQO",
"IV": "ESOVPZJAYQUIRHXLNFTGKDCMWBR",
"V": "VZBRGITYUPSDNHLXMWMJQQOFECKH"
}
REFLECTORS = {
"B": "YRUHQSLDPXNGOKMIEBFZCWVJAT",
"C": "FVPJIAOYEDRZXWGCTKUQSBNMHL"
}
TURNOVER_NOTCHES = {
"I": "Q", "II": "E", "III": "V", "IV": "J", "V": "Z"
}
def __init__(self, rotor_order, ring_settings, initial_positions, plugboard_pairs_str, reflector_type):
"""Инициализирует машину Enigma с заданными настройками."""
self.rotor_order_names = self._parse_string_setting(rotor_order)
self.rotors_wiring = [self.ROTORS[name] for name in self.rotor_order_names]
self.ring_settings_chars = self._parse_string_setting(ring_settings)
self.ring_offsets = [self.ALPHABET.find(c) for c in self.ring_settings_chars]
self.rotor_positions = self._parse_string_setting(initial_positions)
self.plugboard = self._create_plugboard(plugboard_pairs_str)
self.reflector_wiring = self._parse_reflector(reflector_type)
def _parse_string_setting(self, setting_str):
if isinstance(setting_str, str):
return setting_str.upper().split()
return [s.upper() for s in setting_str]
def _create_plugboard(self, pairs_str):
pairs = {}
if pairs_str:
for pair in pairs_str.upper().split():
if len(pair) == 2:
p1, p2 = pair[0], pair[1]
pairs[p1] = p2; pairs[p2] = p1
return pairs
def _parse_reflector(self, reflector_id):
ref_name = reflector_id.split('-')[-1].upper()
return self.REFLECTORS.get(ref_name, reflector_id)
def _pass_plugboard(self, char):
return self.plugboard.get(char, char)
def _pass_reflector(self, char):
return self.reflector_wiring[self.ALPHABET.find(char)]
def _pass_rotor(self, char, rotor_idx, position_char, reverse=False):
wiring = self.rotors_wiring[rotor_idx]
ring_offset = self.ring_offsets[rotor_idx]
position_offset = self.ALPHABET.find(position_char)
total_offset = (position_offset - ring_offset + 26) % 26
if not reverse:
entry_index = (self.ALPHABET.find(char) + total_offset) % 26
index_after_wiring = self.ALPHABET.find(wiring[entry_index])
final_index = (index_after_wiring - total_offset + 26) % 26
else:
entry_index = (self.ALPHABET.find(char) + total_offset) % 26
index_after_wiring = wiring.find(self.ALPHABET[entry_index])
final_index = (index_after_wiring - total_offset + 26) % 26
return self.ALPHABET[final_index]
# --- ИЗМЕНЕННАЯ ЛОГИКА ВРАЩЕНИЯ ---
def _step_rotors(self):
left_pos, middle_pos, right_pos = self.rotor_positions
# self.rotor_order_names хранит имена роторов, например ["I", "III", "II"]
left_name, middle_name, right_name = self.rotor_order_names
# Проверяем, находятся ли роторы НА выемках (notch) ПЕРЕД началом вращения
middle_is_on_notch = middle_pos == self.TURNOVER_NOTCHES[middle_name]
right_is_on_notch = right_pos == self.TURNOVER_NOTCHES[right_name]
# Шаг 1: Правый ротор вращается всегда
# (Но его поворот для следующего ротора сработает только если он БЫЛ на выемке)
# Шаг 2: Средний ротор вращается, если правый БЫЛ на выемке
if right_is_on_notch:
middle_pos = self.ALPHABET[(self.ALPHABET.find(middle_pos) + 1) % 26]
# Шаг 3: Левый ротор вращается, если средний БЫЛ на выемке.
# В этот же момент из-за механики поворачивается и сам средний ротор (двойной шаг)
if middle_is_on_notch:
middle_pos = self.ALPHABET[(self.ALPHABET.find(middle_pos) + 1) % 26]
left_pos = self.ALPHABET[(self.ALPHABET.find(left_pos) + 1) % 26]
# И только теперь правый ротор делает свой шаг
right_pos = self.ALPHABET[(self.ALPHABET.find(right_pos) + 1) % 26]
self.rotor_positions = [left_pos, middle_pos, right_pos]
def encrypt_message(self, text):
encrypted_text = ""
# Сохраняем начальное состояние, чтобы можно было вызывать метод несколько раз
initial_positions_backup = list(self.rotor_positions)
for char in text.upper():
if char in self.ALPHABET:
# Вращение происходит ПЕРЕД шифрованием каждого символа
self._step_rotors()
# Получаем текущие позиции ПОСЛЕ вращения для передачи в роторы
pos0, pos1, pos2 = self.rotor_positions[0], self.rotor_positions[1], self.rotor_positions[2]
processed_char = self._pass_plugboard(char)
processed_char = self._pass_rotor(processed_char, 2, pos2)
processed_char = self._pass_rotor(processed_char, 1, pos1)
processed_char = self._pass_rotor(processed_char, 0, pos0)
processed_char = self._pass_reflector(processed_char)
processed_char = self._pass_rotor(processed_char, 0, pos0, reverse=True)
processed_char = self._pass_rotor(processed_char, 1, pos1, reverse=True)
processed_char = self._pass_rotor(processed_char, 2, pos2, reverse=True)
encrypted_text += self._pass_plugboard(processed_char)
else:
encrypted_text += char
# Восстанавливаем исходные позиции роторов
self.rotor_positions = initial_positions_backup
return encrypted_text
# --- ПРОВЕРКА РЕШЕНИЯ НА ТЕСТАХ ---
if __name__ == "__main__":
print("--- Тестирование класса EnigmaMachine на тестах ---")
# 1. Адаптируем настройки из скрипта для класса
# Роторы: (ROTOR_I, ROTOR_III, ROTOR_II) -> ["I", "III", "II"]
# Позиции: ('F', 'U', 'N') -> ['F', 'U', 'N']
# Коммутационная панель: словарь -> строка "QZ WX EC RV TB YN"
# Рефлектор: REFLECTOR_B -> "B"
# Установочные кольца (Ring Settings): в этих тестах отсутствуют,
# что эквивалентно их положению "A A A".
machine = EnigmaMachine(
rotor_order=["I", "III", "II"],
ring_settings="A A A",
initial_positions=['F', 'U', 'N'],
plugboard_pairs_str="QZ WX EC RV TB YN",
reflector_type="B"
)
# 2. Берем тесты из скрипта
tests = [
{"name": "Тест 1: Простое слово 'HELLO'", "plaintext": "HELLO", "expected": "CPXXR"},
{"name": "Тест 2: Фраза с пробелом 'ENIGMA MACHINE'", "plaintext": "ENIGMA MACHINE", "expected": "MBPTQF HBDJTGG"},
{"name": "Тест 3: Проверка коммутации 'QUERY'", "plaintext": "QUERY", "expected": "LVNUZ"},
{"name": "Тест 4: Проверка механики на длинной строке", "plaintext": "AAAAAAAAAAAAAAAAAA", "expected": "DWJHCFCBIWCRNRHYRY"}
]
all_passed = True
for test in tests:
print(f"\n--- Запуск: {test['name']} ---")
# 3. Выполняем шифрование
actual_output = machine.encrypt_message(test['plaintext'])
print(f"Входной текст: '{test['plaintext']}'")
print(f"Ожидаемый результат: '{test['expected']}'")
print(f"Полученный результат: '{actual_output}'")
if actual_output == test['expected']:
print("Статус: ПРОЙДЕН!")
else:
print("Статус: ПРОВАЛЕН!")
all_passed = False
print("\n" + "="*30)
if all_passed:
print("ИТОГ: ВСЕ ТЕСТЫ ПРОЙДЕНЫ УСПЕШНО!")
else:
print("ИТОГ: НЕКОТОРЫЕ ТЕСТЫ НЕ ПРОШЛИ.")
print("="*30)
В нашем алгоритме дополнительно следует отметить, что символы, которые не являются буквами, пропускаем без шифрования. В реальных машинах было не так. В них сообщения передавались большими латинскими буквами без знаков препинания и пробелов, при этом машина сама дробила их на "слова", состоящие из 4 или 5 букв. Отличная иллюстрация дана в эмуляторе, скриншоты из которого в этой статье были использованы:

Здесь показано шифрование с теми же настройками, что и в тестах выше. Демонстрируется проверка правильности работы 4-го теста, можете самостоятельно проверить и остальные.
9. Инструкция, как освоить материал этой статьи
Решите последовательно несколько задач
-
Запрограммируйте коммутационную панель и аналогично рефлектор.
def plugboard(char, pairs):
# pairs - это словарь, например {'A': 'B', 'B': 'A'}
# Ваш код здесь
return charТесты
pairs_test = {'A':'B', 'B':'A', 'C':'D', 'D':'C'}
plugboard('A', pairs_test) должен вернуть 'B'plugboard('C', pairs_test) должен вернуть 'D'
plugboard('E', pairs_test) должен вернуть 'E'
-
Запрограммируйте один ротор.
# Английский алфавит
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# Проводка одного из реальных роторов Enigma
ROTOR_I = "EKMFLGDQVZNTOWYHXUSPAIBRCJ"
def rotor_pass(char, wiring, offset):
# char - входной символ
# wiring - строка с проводкой ротора
# offset - текущее смещение ротора (на сколько он повернут)
# Ваш код здесь
# Подсказка: используйте сложение и вычитание индексов по модулю 26
return charТесты
rotor_pass('A', ROTOR_I, 0) должен вернуть 'E'
rotor_pass('A', ROTOR_I, 1) должен вернуть 'K' (сначала смещаем, потом шифруем) rotor_pass('B', ROTOR_I, 2) должен вернуть 'F'
rotor_pass('Z', ROTOR_I, 3) должен вернуть 'M' (сначала смещаем, потом шифруем) -
Запрограммируйте статичную машину (в которой роторы не двигаются).
def encrypt_single_char(char, rotor_order, rotor_positions, plugboard_pairs):
# Ваш код здесь
return char# константы для тестов
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" и
ROTOR_I = "EKMFLGDQVZNTOWYHXUSPAIBRCJ"
ROTOR_II = "AJDKSIRUXBLHWTMCQGZNPYFVOE"
ROTOR_III = "BDFHJLCPRTXVZNYEIWGAKMUSQO"
REFLECTOR_B = "YRUHQSLDPXNGOKMIEBFZCWVJAT"
rotor_order = (ROTOR_I, ROTOR_III, ROTOR_II)
rotor_positions = ('F', 'U', 'N')
plugboard_pairs = {'Q': 'Z', 'Z': 'Q', 'W': 'X', 'X': 'W', 'E': 'C', 'C': 'E', 'R': 'V', 'V': 'R', 'T': 'B', 'B': 'T', 'Y': 'N', 'N': 'Y'}
Входной символ: 'Q' Правильный вывод : 'O'
Входной символ: 'E' Правильный вывод : 'Z'
Входной символ: 'A' Правильный вывод : 'C'
Входной символ: 'Z' Правильный вывод : 'E'
Теперь, наконец, запрограммируйте всю машину, и у вас должен получиться код, аналогичный тому, что приведен в этой статье.
10. Полезные ссылки по теме и видео
В процессе подготовки материала я использовал множество источников. Вот часть из них
Эмулятор различных машин Энигма.
Сайт про шифры Второй мировой войны.
Сайт первого куратора музея Блетчи-Парка.
Подбора разных симуляторов Энигмы.
Шифровальная машина Энигма в FPGA.
Подробное описание "бомбы" Тьюринга.
Интересное описание Энигмы с примерами и историей.
Если же вас интересует не только алгоритм шифрования, но и сами детали технического устройства, рекомендую посмотреть видео на ютубе. Там показана 3D-модель устройства.
Kwisatz
Можно источник?
Каких именно причин? Почему не софт? Это очень интересный вопрос вэпоху сетецентричности. Что потом происходит с выводом механического шифратора? Ну и финальное, а это вообще правда?
PS вот тут неплохой видосик где можно вживую посмотреть как машинка работает, ну или в фильма "игра в имитацию"
master_program Автор
Спасибо за видео, этого не находил. Полезная ссылка !
Роторные механические шифраторы не переставали использоваться с тех пор в армиях мира, они и ВС РФ используются, в частности. Но в основном они проще Энигмы.
Их преимущество как раз в том, что они механические. Смартфону, например, постоянно подзарядка нужна, да и РЭБ его может вывести из строя.
Про ВСУ есть разные новости
Невероятная новая машина "Энигма" помогает побеждать врагов Запада, предсказывая атаки до их начала (The Sun, Великобритания) | 20.05.2025, ИноСМИ
Морпехи РФ захватили командно-штабную машину ВСУ с современной «Энигмой» - KP.RU
eledev
Ученый изнасиловал журналиста? Вот про Энигму это вы серьезно?
master_program Автор
Роторные шифровальные машины в армиях до сих пор используются. А вот что там конкретно за устройства, про которые пишут в СМИ, и насколько они похожи на Энигму, информации почти нет.
Kwisatz
Отвечу на все разом. Вы пишете про механическое шифрование, а дальше то с этим что делать? на голубях? Не будет это звучат как с ЕГЭ "мы месяц на санях несли задания ЕГО в самую дальнюю юрту и затем за день они были слиты в сеть".
master_program Автор
Ровно то же самое, что и во времена ВМВ - передача шифротекста по радиосвязи.
Kwisatz
Во первых, по вашим же ссылкам речь о программных комплексах. Во вторых, вы понимаете что есть сетецентричность? Это совершенно иной уровень войны, если добавить к этому дронов то получается классическая война будущего. Сейчас все разом не уничтожает рой дронов только потому, что у некоторых маленьких стран маловато денег. При такой картине мира, то что вы описываете это даже не прошлый век, это позапрошлый, да еще и не имеющий смысла.
master_program Автор
Ну одно дело война будущего, другое дело реальная война. Я вот сейчас нашел по поводу армейских применений еще.
"Механические шифраторы применяются в низкоскоростных аппаратах (СТА-2М), а электронные - в более скоростных системах."
А такие аппараты в армии до сих пор используют, есть даже профессия такая, военный телеграфист.
Насчет моих ссылок, там не везде явно написано, что это ПО.
Kwisatz
Реальная война? Черчиль сказал: "все генералы готовятся к войнам прошлого", самый запомнившийся мне пример - линкор Ямато. Когда в воздухе висит несколько тысяч дронов/самолетов, рассуждения о механике выглядят слабовато
master_program Автор
Я параллельно на форуме, где есть действующие военные, спросил, какие аппараты они видели. Механических не видели, в подавляющем большинстве случаев используют вот такие. Это проводной телефон, в отличие от радиосвязи сигнал не получится легко перехватить, поэтому даже не шифруют.
Может быть это не похоже на войну будущего, но это реальная война.
Kwisatz
не сомневаюсь, а еще винтовки системы мосина люди видели. Не услышал аргумента.
master_program Автор
Винтовки Мосина используют, потому что других не хватило на всех. А такие аппараты - потому что проводные коммуникации остаются наиболее надежным способом передать информацию «не для чужих ушей».
Kwisatz
Ровно как и AES
master_program Автор
Похоже, нет четких сообщений, что кто-то механические шифраторы поставляет - видимо, если они и используются, то потому что со времен СССР остались и пригодны для телеграфной связи. Я соответствующий кусок текста позже сегодня отредактирую в статье, внесу примечание.
В целом это интересная историческая справка в тему будет, а что с этим сейчас происходит.
master_program Автор
Посмотрите, отредактировал статью
master_program Автор
На сайте крипто-музея есть про вот такую российскую шифровальную машину, например https://www.cryptomuseum.com/crypto/ru/m427/ .
Пишут, что ее используют ВС РФ.
"Такие устройства, как M-427, могут показаться устаревшими в эпоху интернета и персональных компьютеров (ПК), но у них есть много преимуществ перед коммерческим готовым к использованию (ГКИ) оборудованием. Их невозможно взломать, и они более устойчивы к электромагнитным импульсным (ЭМИ) атакам. "
master_program Автор
Я поискал, если тема интересна, для радиосвязи в войсках очень часто используют AES256-шифрование (для полевой связи просто проводные телефоны типа ТА-57).
"По сравнению с асимметричными алгоритмами, такими как RSA, AES (симметричный алгоритм) работает значительно быстрее, что делает его идеальным для шифрования больших объёмов данных, в то время как RSA часто используется для обмена ключами или создания цифровых подписей. "