MathType

неделя, 28 август 2016 г.

Криптоанализът на Енигма (Част I)

Част I. Зъболекари и шифри или няколко думи за скромното начало на модерната криптография

Ако нямате добра система за шифриране, Вие се излагате на съществен риск. Предавана по кабел или безжично, Вашата кореспонденция винаги ще бъде достъпна за всеки шпионин. Вашите писма ще бъдат изложени на риска да бъдат отворени и копирани. Вашите предстоящи и бъдещи договори, Вашите оферти и важни новини ще бъдат достъпни за всяко любопитно око. Предвид настоящото състояние на нещата, почти немислимо е, че хора, засегнати от тези обстоятелства, ще искат да забавят подсигуряването си срещу такива неща.

До момента, шифрирането и дешифрирането бяха инхерентно обвързано с проблеми изкуство. Сега можем да Ви предложим нашата машина "Енигма", която е универсално решение на тези проблеми.

Рекламна брошура на Енигма машина, средата на 20-те години на 20-ти век

Всяка история има своето начало. Наистина добрите истории обикновено имат няколко. Историята на криптоанализа на Енигма, повлиял на изхода на най-ожесточения сблъсък в историята на човечеството, е именно такава. Поради това нейното начало може да бъде отнесено към няколко различни момента от време.

Едно необичайно начало може да бъде събитие от много далечната 1854 година. Тогава зъболекарят от Бристол Джон Холл Брок Твейтс пише до Journal Of Society and Arts за да представи свое откритие. Откритието е нов шифър, който зъболекарят възнамерява да патентова. Това събитие привлича вниманието на един от изтъкнатите английски учени по това време - Чарлз Бабидж. В писмо до списанието, Чарлз Бабидж отбелязва, че патентните претенции на г-н Твейтс са неоснователни, тъй като неговият шифър се оказва вариант на вече добре известен шифър. Това е шифърът, станал известен като "Шифър на Виженер".

Преоткриван много пъти в историята, описан като идея за пръв път през 1553 година от Джован Батиста Беласо и усъвършенстван от Блез де Виженер, шифърът до този момент е обгърнат от аура на непобедимост, тъй като за 3 столетия не е намерен универсален начин за пробиването му. Малкото успехи при разшифроването му до тогавашния момент разчитат на това, че криптоаналитикът има някаква предварителна информация за съдържанието на съобщението, което е шифрирано с помощта на шифъра.

Шифърът на Виженер много скоро изправил Бабидж пред интересен проблем, тъй като доста обиденият зъболекар му отправил публично предизвикателство - да пробие два последователни пъти "неговия" шифър. Оказало се, както се уверили и читателите на списанието, че Бабидж успял. За съжаление той никога не разкрил методите си, оставяйки славата на майора от пруската армия Фридрих Касиски (да, фамилията е полска), който през 1863 година публикува "Тайнопис и изкуството на дешифрирането". В книгата е изложена обща атака срещу шифъра на Виженер. По същото време тече Американската гражданска война и юнионистите редовно четат съобщения на Конфедерацията, които се шифрират с вариант на същия шифър.

Шифърът на Виженер и моментът на неговото пробиване са важна част от нашата история, тъй като от опитите за "поправяне" на този доста елементарен от днешна гледна точка шифър водят до две важни открития, случили се по почти едно и също време.

Първото от тези открития са роторните машини за шифриране, които доминират криптографията до появата и внедряването на цифровата електроника през 70-те и 80-те години на миналия век. Най-популярният, но далеч не единствен, представител на тези машини е именно Енигма.

Второто откритие е алгоритъм за шифриране, за чиято теоретична непробиваемост имаме безспорни доказателства. Всъщност ние знаем, че използван правилно, този шифър би устоял и дори на атака от извънземни, които евентуално биха го атакували с техните безкрайно напреднали компютърни технологии и математически знания. (При условие, разбира се, че тези извънземни не могат някак мистериозно да прекосят времето и пространството назад към момента и мястото, в което даденото съобщение е било шифрирано или дешифрирано) [1].

Блез де Виженер (1523-1596) и Чарлз Бабидж (1791-1871)

Шифърът на Виженер също възниква като доста елементарно, но ефективно, подобрение на Шифъра на Цезар, който е известен от античността. Последният замества всяка буква от азбуката с друга буква, която се намира на определен брой фиксирани позиции напред или назад в естествения ред на буквите. Така например, според историческите източници, Цезар замествал A с D, B с Е, C с F и т.н. Тоест, той замествал всяка буква с третата след нея буква в азбуката. Този подход има фатален недостатък, известен ни поне от средновековието. Шифърът на Виженер възниква именно като опит за премахване на този недостатък. (Изпробвайте Шифъра на Цезар тук).

Въпреки че Шифърът на Цезар замества буквите, всяка буква се замества всеки път с една и съща друга буква. Това позволява извършването на лесна атака - т.нар. "честотен анализ". В думите на реалните езици, някои букви се срещат по-често от останалите. Така например, най-често срещаните букви в английския език са гласните е,а,o,i,u ("e" е най-често срещаната буква, следвана от "а" и т.н. в указания ред). Независимо с коя буква се замества "е", подходът на Шифъра на Цезар ще доведе до това, че тази буква ще бъде най-често срещаната в шифрираното съобщение, ако то е на английски език. Криптоаналитик тогава би преброил колко пъти се среща всяка буква в шифрираното съобщение и с няколко проби би открил местата на всички гласни, което на свой ред би довело до дешифрирането на цели думи. От тези думи на свой ред ще се получи информация за това как се заместват и много от съгласните, което ще доведе до разшифроването на цялото съобщение.

Шифърът на Виженер разчита на ключ, тоест някаква стойност, която е известна само на автора на съобщението и на неговия получател. Този ключ е обикновен текст - дума или цяло изречение, без интервалите. Ключът определя с каква буква ще бъде заменена всяка следваща буква от съобщението. За да извърши заместването, шифърът използва помощно средство, наречено Таблица на Виженер или, ако трябва да използваме оригиналното понятие, "tabula recta".

ABCDEFGH IJKLMNOPQR STUVWXYZ
AABCDEFGHI JKLMNOPQRS TUVWXYZ
BBCDEFGHI JKLMNOPQRS TUVWXYZA
CCDEFGHI JKLMNOPQRS TUVWXYZAB
DDEFGHIJKL MNOPQRSTUV WXYZABC
EEFGHIJKL MNOPQRSTUV WXYZABCD
FFGHIJKL MNOPQRSTUV WXYZABCDE
GGHIJKL MNOPQRSTUV WXYZABCDEF
HHIJKL MNOPQRSTUV WXYZABCDEFG
IIJKL MNOPQRSTUV WXYZABCDEFGH
JJKL MNOPQRSTUV WXYZABCDEF GHI
KKL MNOPQRSTUV WXYZABCDEF GHIJ
LLMNOPQRSTUV WXYZABCDEF GHIJK
MMNOPQRSTUV WXYZABCDEF GHIJKL
NNOPQRSTUV WXYZABCDEF GHIJKLM
OOPQRSTUV WXYZABCDEF GHIJKLMN
PPQRSTUV WXYZABCDEF GHIJKLMNO
QQRSTUV WXYZABCDEF GHIJKLMNOP
RRSTUV WXYZABCDEF GHIJKLMNOP Q
SSTUV WXYZABCDEF GHIJKLMNOP QR
TTUV WXYZABCDEF GHIJKLMNOP QRS
UUV WXYZABCDEF GHIJKLMNOP QRST
VVWXYZABCDEF GHIJKLMNOP QRSTU
WWXYZABCDEF GHIJKLMNOP QRSTUV
XXYZABCDEF GHIJKLMNOP QRSTUVW
YYZABCDEF GHIJKLMNOP QRSTUVWX
ZZABCDEF GHIJKLMNOP QRSTUVWXY

За да шифрира едно съобщение, авторът измисля ключ, например "SQRTLTAF" и го повтаря толкова пъти, колкото е необходимо, за да достигне същия брой букви, колкото е съобщението. Т.е. ако съобщението е "ATTACKATNOONTOMORROW" (20 букви), горният ключ ще бъде "допълнен" до 20 букви чрез повторение - "SQRTLTAFSQRTLTAFSQRT".

По този начин, след като съобщението и ключът се поставят един под други, се формират двойки буви - една от съобщението и една от ключа. След това, авторът търси тези двойки в Таблицата на Виженер. Буквата от съобщението се търси в заглавието на колоните (първият ред на поместената по-горе таблица, изписан с удебелен шрифт), а буквата от ключа се търси в заглавието на редовете (първата колона на таблицата, изписана с удебелен шрифт). Там където колоната, избрана от буквата на съобщението, и редът, избран от буквата на ключа, се пресекат, се намира буквата, която се записва в шифрираното съобщение.

Дешифрирането работи по подобен начин. Получателят знае ключа и избира реда, определен от поредната му буква, след което намира буквата от шифрираното съобщение, която дава колоната, в чието заглавие се намира буквата, която е част от оригиналното съобщение.

На този етап читателят е приканен да забележи, че при ключ "DDDDDDDD...", Шифърът на Виженер би бил еквивалентен на Шифъра на Цезар, а ключ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" би отговарял на използването редовете на tabula recta в последователен ред. Въщност tabula recta възниква преди шифъра на Виженер и опосредства множество схеми, при които няма ключ, а редовете на таблицата се използват в някакъв фиксиран ред, например последователно.

Голямото нововъведение при Шифъра на Виженер е използването на ключ вместо фиксирана последователност от редове. Разбира се, тъй като фиксираните последователности от редове могат да бъдат идентифицирани с даден ключ от Шифъра на Виженер, това означава, че този шифър за пръв път в историята е направил прехода от фиксиран ключ за всички ползватели към индивидуален ключ, който може да се подели само между две страни! Бихме могли да опишем този забележителен факт и по друг начин - тайната на комуникацията за пръв път в историята не зависи от опазването на шифриращия алгоритъм в тайна от потенциалните опоненти! Днес това е известно като Принцип на Керкхоф и е основно изискване към всеки модерен шифър.

Горните факти могат да бъдат обобщени с на малко по-математизиран език. Шифърът на Цезар прилага една и съща пермутация - (DEFGHIJKLMNOPQRSTUVWXYZABC) - към буква от текста на съобщението, докато Шифърът на Виженер позволява буквата от ключа да определи коя от 26 пермутации да бъде използвана към дадената буква от съобщението.

Много често вместо tabula recta във втората половина на 19-ти век се е използвал шифродиск, като показания тук диск на Конфедерацията от гражданската война в САЩ. Шифродискът се състои от два концентрични кръга с букви, които се въртят един спрямо друг. Той е по-компактно средство от таблицата, като на всяка следваща стъпка нужният ред (пермутация) се получава, като се завърти дискът на нужната позиция.

Какво би видял криптоаналитик, ако се опита да приложи честотния анализ към съобщение, кодирано с Шифъра на Виженер? Накратко, честотният анализ няма да работи без модификация, защото броят на различните букви в кодираното съобщение ще е приблизително еднакъв. Нека да опитаме да илюстрираме това свойство на шифъра с пример. Нека приемем, че съобщението е "ALOHA". Ако ключът е избран произволно, това означава, че на позиция 1 на ключа ще има случайно избрана буква, която ще избере с равна вероятност една от 26-те пермутации в tabula recta. Ако на позиция 1 на ключа се намира буквата "A", това ще избере първата пермутация (ред), която ще превърне "A" от съобщението в "В". Ако на позиция 1 обаче има буквата "B", това е втората пермутация, която ще превърне "А" от съобщението в "C" и т.н.

Не е трудно да се види, че независимо коя буква е на първа позиция в съобщението, една от 26-те възможни пермутации, определени от първата буква на ключа, може да превърне буквата от съобщението във всяка друга буква. Нещо повече, тъй като пермутациите са еднакво вероятни, това означава, че независимо каква е буквата на съобщението, ключът ще я превърне с равна вероятност във всяка друга възможна буква. Това е вярно и за всяка друга позиция в съобщението. Читателят може да проследи например колона F на Таблицата на Виженер, за да види коя пермутация превръща F във A,B,C,D и пр. букви в азбуката.

На пръв поглед това е доста безнадеждно. Ако имаме само шифротекста, ние сме пред тежка задача. Да речем, че той е "FEZE". Ако ключът е бил "SQLR", съобщението е гласяло "NOON". Ако ключът е бил "NELA", съобщението е гласяло "STOP". Ако ключовете са произволно избрани, всеки от тях е еднакво вероятен, а вероятността нашето съобщение да е произлязло от която и да било четирибуквена дума е напълно еднаква с вероятността на която и да било друга четирибуквена дума. Можем да мислим за шифрирането като процес, който "маскира" структурата на текста до пълна неузнаваемост. Този процес може да бъде "обърнат" само ако знаем ключа.

И така, как Касиски успява да разбие шифъра? Той прави много просто наблюдение. Тъй като типично съобщенията са много по дълги от ключа (за разлика от горния четирибуквен пример), ключът се повтаря многократно. Това нарушава напълно случайния му характер, което се отразява катастрофално върху сигурността за шифъра. Касиски забелязал, че повторението на ключа позволява Шифъра на Виженер да се разглежда като множество отделни копия на Шифъра на Цезар. Така например, ако ключът (без повторенията) е дълъг 7 символа, всеки символ от шифрирания текст на позиция 1, 8, 15, 22, 29, 26, 33 и т.н. е шифриран с една и съща пермутация (не много различно от Шифъра на Цезар). Същото е вярно за позиции 2,9,16,23,30,27,34 и т.н., макар тук пермутацията да е различна от тази на предните позиции. Също така 3,9,17,24,31,28,35 и т.н. са шифрирани с една и съща пермутация. Тоест, Шифърът на Виженер "вплита" няколко Шифъра на Цезар, чиито брой зависи от дължината на ключа.

Aко вместо преброяване на всички символи от съобщението ние изброим честотата на символите само на позиции 1,8,15,22,29,26,33 и т.н., ние ще имаме Шифър на Цезар (една единствена пермутация, макар и не точно такава, заместваща всеки символ с третия след него). Честотният анализ на тази последователност от символи ще работи без проблеми, което ще ни позволи за отгатнем множество букви. След това ще преминем към броенето на честотите на символите от позиции 2,9,16,23 и т.н. Процедурата ще се повтори за всички възможни групи позиции. Колкото по-дълго е съобщението и по-кратък е ключа, толкова по-лесно ще се справим със задачата на дешифрирането.

По този начин за да разбием Виженер и всички други "позиционни" схеми преди него, на нас ни трябва просто да знаем дължината на ключа. Това разбира се, може да се установи с проба, особено когато говорим за кратки ключове. При всички грешни дължини, броенето само на първите групи, формирани от символите на позиции 1, 1+предполагаема_дължина,1+2*предполагаема_дължина,1+3*предполагаема дължина и т.н. няма да дава някаква отчетлива тенденция и броят на буквите от всеки вид ще е приблизително еднакъв. При успешно отгатване на дължината на ключа обаче, ще видим ситуация, при която едни букви ще се срещат значително по-често от останалите.

БЕЛЕЖКИ:

1. Авторът си позволи да "открадне" тази ярка илюстрация от Брус Шнайер, който получава безспорния кредит за формулирането и.

сряда, 24 февруари 2016 г.

Емулатор на немска шифровъчна машина Енигма

Част III. Програмният код

Настоящата статия е разделена в няколко публикации на блога:
Част I. Принцип на работа на Енигма машина
Част II. Детайли за роторите и математически модел на машината
Част III. Програмният код
Последна редакция: 26.02.2016г.
Кодът не е тестван достатъчно за грешки.

След подробното излагане на принципите и особеностите на Енигма машините, сме готови да преминем към написването на програмния код. Започваме с няколко "сервизни" класове и интерфейси.

Alphabet е клас, който съдържа константата ALPHABET със стойност "ABCDEFG...". Ролята на ALPHABET е от нея да се изчитат позициите на буквите в азбуката, които се използват от Енигма роторите.

/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */

package code.lalev.enigmasim;

public final class Alphabet {
 /**
  * Constant string with all capital Latin letters of the alphabet in alphabetical 
  * order. It's needed to establish the order of letters. A is 0, B is 1 etc. 
  * and we get it by checking the position of each letter the string.
  */ 
 public final static String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
}

Интерфейсът EnigmaComponent се реализира от класове, които емулират елемент на Енигма машината, внасящ пермутация в математическия ѝ модел.

/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */

package code.lalev.enigmasim;

public interface EnigmaComponent {
 public char permuteChar (char input);
 public char invertChar (char input);
}

Имаме и специфично инстанцирано изключение.

/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */

package code.lalev.enigmasim;

public class InvalidEnigmaConfiguration extends IllegalArgumentException {
 public  InvalidEnigmaConfiguration(String message) {
  super(message);
 }
}

EnigmaPermutation инициализира пермутация на буквите от латинската азбука в нотация на Коши и предоставя методи за прилагането и върху букви от латинската азбука.

/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */

package code.lalev.enigmasim;

/**
 * Encapsulates the permutation functionality needed by Enigma. 
 * <p>
 * Enigma machines permute the letters of Latin alphabet and are 
 * case-insensitive. So this class facilitates working
 * with permutations of capital letters of the Latin alphabet. 
 * 
 * @author Angelin Lalev
 *
 */

public class EnigmaPermutation { 

 protected String permutation;

 
 /**
  * No-argument constructor, which creates neutral permutation of the captial latin letters
  * (Each letter is sent to itself).
  */
 public EnigmaPermutation() {
  permutation = Alphabet.ALPHABET;
 }
 
 /**
  * Creates user-specified permutation.   
  * @param permutation gives the permutation in Cauchy notation. More specifically, 
  * it gives the second line of the "table". In example the value for @param permutation of 
  * "ZABCDEFGHIJKLMNOPQRSTUVWXY" sets the permutation so, that it sends 
  * A to Z, B to A, C to B etc.
  */
 
 public EnigmaPermutation(String permutation) { 
  for (int i=0; i<Alphabet.ALPHABET.length(); i++) {
   if (!permutation.contains(Character.toString(Alphabet.ALPHABET.charAt(i)))) {
    throw new InvalidEnigmaConfiguration("Invalid Enigma permutation");
   }
  }
  
  if (Alphabet.ALPHABET.length()!=permutation.length()) {
   throw new InvalidEnigmaConfiguration("Invalid Enigma permutation");
  }
  this.permutation = permutation;
 }
 
 
 /**
  * Applies the permutation to a letter
  * 
  * @param input contains the letter to be permuted
  * @return returns the resulting letter
  */
 public char permute(char input) {
  return permutation.charAt(Alphabet.ALPHABET.indexOf(input));
 }
 
 /**
  * Applies the <em>inverse</em> permutation to a letter
  * 
  * @param input contains the letter to be permuted
  * @return returns the resulting letter
  */ 
 public char invert(char input) {
  return Alphabet.ALPHABET.charAt(permutation.indexOf(input)); 
 }
}

EnigmaRhoPermutation описва пермутации, които отговарят на въртенето на ротора, както и на отместванията на азбучните пръстени на роторите. Тези пермутации всъщност са циклична група с генератор "BCDEF...YZA".

/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */

package code.lalev.enigmasim;

/**
 * Creates a member of the group of permutations that are needed to represent the movement of an 
 * Enigma rotor. This group is cyclic. All of the permutations in the group are compositions of a basic 
 * "generator" permutation with itself. The basic permutation sends A to B, 
 * B to C, C to D etc. it can be "composed" several times with itself, giving any other member 
 * of this group.  
 * 
 * @author Angelin Lalev
 *
 */

public class EnigmaRhoPermutation extends EnigmaPermutation {

 protected int times;
 
 /**
  * No-argument constructor that creates an instance that represents neutral permutation of the 
  * capital Latin letters (Each letter is sent to itself).
  */

 public EnigmaRhoPermutation() {
  super();
  times=0;
 }


 /**
  * Creates an instance, representing the n-th member of the permutation group. 
  */
 
 public EnigmaRhoPermutation(int times) {
  super();
  advance(times);
 }

 /**
  * Applies the basic permutation to the current one n times to get the n-th next permutation in the 
  * group. The result becomes the new current permutation, which is represented by the instance. 
  */
 
 public void advance(int times) {
  String newperm = permutation; 
  int tmptimes = times % permutation.length();
  
  for (int i=0; i<tmptimes; i++) {
   newperm = permutation.substring(1, permutation.length());
   permutation = newperm+permutation.substring(0,1);
  }
  
  this.times = (tmptimes+times)%permutation.length();
 }
}

Основният клас, който инкапсулира функционалността на Енигма машината, разглеждана като формула, е EnigmaMachine.

/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */

package code.lalev.enigmasim;

import java.util.ArrayList;

/**
 * Class that encapsulates the functionality of the Enigma machine.
 * 
 * @author Angelin Lalev
 *
 */
public class EnigmaMachine {
 public static final int MIN_ROTOR_COUNT = 3;
 
 protected ArrayList<EnigmaRotor> rotors;
 protected EnigmaReflector reflector;
 protected EnigmaSteckerBoard steckerboard;
 protected EnigmaETW etw;
 
 protected boolean valid;
 
 /**
  * Constructs classical EnigmaMachine
  * @param rotor1 - The rightmost rotor
  * @param rotor2 - The middle rotor
  * @param rotor3 - The leftmost rotor
  * @param rotor4 - Eventually, a forth rotor, to the left of the current leftmost rotor
  * @param reflector - The reflector of the machine
  * @param steckerboard - The steckerboard of the machine
  * @param etw - The ETW ring of the machine
  */
 public EnigmaMachine(EnigmaRotor rotor1, EnigmaRotor rotor2, EnigmaRotor rotor3, 
     EnigmaRotor rotor4, EnigmaReflector reflector, EnigmaSteckerBoard steckerboard,
     EnigmaETW etw) {
  if ((rotor1 == null) || (rotor2==null) || (rotor3==null)) {
   throw new InvalidEnigmaConfiguration("At least rotors 1, 2 and 3 must be intialized");
  } 
  
  if (reflector==null) {
   throw new InvalidEnigmaConfiguration("Reflector of the Enigma machine must be initialized.");
  }

  if (steckerboard==null) {
   this.steckerboard = new EnigmaSteckerBoard();
  }
  
  if (etw == null) {
   this.etw = new EnigmaETW();
  }

  rotors = new ArrayList<EnigmaRotor>(MIN_ROTOR_COUNT);
  
  this.rotors.add(rotor1);
  this.rotors.add(rotor2);
  this.rotors.add(rotor3);
  if (rotor4!=null) {
   this.rotors.add(rotor4);
  }
  this.reflector = reflector;
  this.steckerboard = steckerboard;

  this.valid = true;
 }
 
 public EnigmaMachine() {
  rotors = new ArrayList<EnigmaRotor>(MIN_ROTOR_COUNT);
  this.valid = false;
 }
 
 public void addRotor(EnigmaRotor rotor) {
  if (rotor!=null) {
   rotors.add(rotor);
  }
 }
 
 public void addReflector(EnigmaReflector reflector) {
  if (reflector!=null) {
   this.reflector = reflector;
  }
 }
 
 public void addSteckerBoard(EnigmaSteckerBoard steckerboard) {
  if (steckerboard!=null) {
   this.steckerboard = steckerboard;
  }
 }
 
 public void addETW(EnigmaETW etw) {
  if (etw!=null) {
   this.etw = etw;
  }
 }

 public void validate() {
  for (int i=0; i<MIN_ROTOR_COUNT; i++) {
   try {
    if (rotors.get(i)==null) {
     throw new InvalidEnigmaConfiguration("At least "+ MIN_ROTOR_COUNT + "rotors must be present in an Enigma machine");
    }
   } catch (IndexOutOfBoundsException e) {
    throw new InvalidEnigmaConfiguration("At least " + MIN_ROTOR_COUNT + "rotors must be present in an Enigma machine");
   }
  }

  if (reflector==null) {
   throw new InvalidEnigmaConfiguration("No reflector added to machine");
  }
  
  
  if (steckerboard==null) {
   throw new InvalidEnigmaConfiguration("No steckerboard added to machine");
  }
  
  if (etw==null) {
   throw new InvalidEnigmaConfiguration("No etw added to machine");
  }
  
  valid = true;
 }
 
 /**
  * Moves the rotors according to notches and takes into account 
  * the double step phenomena.
  *  
  */
 public void stepRotors() {
  if (!valid) {
   throw new InvalidEnigmaConfiguration("Enigma configuration must be validated before being used!");
  }

  
  rotors.get(0).markForMovement();
  
  for (int i=1; i<rotors.size(); i++) {
   if (rotors.get(i-1).onNotch()) {
     rotors.get(i).markForMovement();
     rotors.get(i-1).markForMovement();
   }
  }
  
  for (int i=0; i<rotors.size(); i++) {
   rotors.get(i).move();
  }
 }
 
 /**
  * Returns a string that represent the current rotor poisitions
  * of each rotor. The leftmost char is the leftmost rotor.
  *  
  * @return
  */
 public String getRotorPositions() {
  String result="";
  for (int i=rotors.size()-1; i>=0; i--) {
   result=result+rotors.get(i).getPositionChar();
  }
  return result;
 }
 
 public char encodeChar(char input) {
  char output;

  if (!valid) {
   throw new InvalidEnigmaConfiguration("Enigma configuration must be validated before being used!");
  }
  
  stepRotors();

  output = steckerboard.permuteChar(input);

  output = etw.permuteChar(output);
  
  for (int i=0; i<rotors.size(); i++) {
   output = rotors.get(i).permuteChar(output);
  }

  output = reflector.permuteChar(output);
  
  for (int i=rotors.size()-1; i>=0; i--) {
   output = rotors.get(i).invertChar(output);
  }

  output = etw.invertChar(output);
  
  output = steckerboard.invertChar(output);

  return output;
 }
 
 public String encodeMessage(String message) {
  String result="";
  for (int i=0; i<message.length(); i++) {
   result = result+encodeChar(message.charAt(i));
  }
  return result;
 }
}

EnigmaMachine притежава списък с ротори, които са реализирани като инстанции на EnigmaRotor.

/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */

package code.lalev.enigmasim;

/**
 * Emulates an Engima rotor. 
 * <p>
 * It does so by creating a EnigmaPermutation that corresponds to 
 * the rotor wirings. Additionally, it creates two EnigmaRhoPermutations
 * that correspond respectively to ring offset and rotor position.
 * One that has the rotor position can "advance" to simulate the rotor 
 * movement.   
 * 
 * @author Angelin Lalev
 *
 */

public class EnigmaRotor implements EnigmaComponent {

 public static final String[][] ROTOR_CONFIGURATIONS = {{"I","EKMFLGDQVZNTOWYHXUSPAIBRCJ", "Q"},
   {"II", "AJDKSIRUXBLHWTMCQGZNPYFVOE", "E"}, 
   {"III", "BDFHJLCPRTXVZNYEIWGAKMUSQO", "V"},
   {"IV", "ESOVPZJAYQUIRHXLNFTGKDCMWB", "J"},
   {"V", "VZBRGITYUPSDNHLXAWMJQOFECK", "Z"},
   {"VI", "JPGVOUMFYQBENHZRDKASXLICTW", "ZM"},
   {"VII", "NZJHGRCXMYSWBOUFAIVLPEKQDT", "ZM"},
   {"VIII", "FKQHTLXOCBJSPDZRAMEWNIUYGV", "ZM"},
  };

// private String type;
 private String notches;
 private EnigmaRhoPermutation positionPerm;
 private EnigmaRhoPermutation ringPerm;
 private EnigmaPermutation rotorPerm;
 private char positionChar;
// private char ringChar;
 private boolean markedForMovement;
 
 public char permuteChar(char input) {

  return positionPerm.invert(
     ringPerm.permute(
       rotorPerm.permute(
         ringPerm.invert(
           positionPerm.permute(input)
    ))));
 }
 
 public char invertChar(char input) {
  return positionPerm.invert(
     ringPerm.permute(
       rotorPerm.invert(
         ringPerm.invert(
           positionPerm.permute(input)
    ))));
 } 
 
 public void makeStep() {
  positionPerm.advance(1);
  if (positionChar=='Z') {
   positionChar='A';
  } else {
   positionChar++;
  }
 }

 public void markForMovement() {
  markedForMovement = true;
 }
 
 public void move() {
  if (markedForMovement) {
   makeStep();
  };
  markedForMovement = false;
 }
 
 public boolean onNotch() {
  return notches.contains(Character.toString(positionChar));
 }
 
 /**
  * Returns current position on the alphabetic ring of the rotor.
  */
 public char getPositionChar() {
  return positionChar;
 }
 
 /**
  * Create EnigmaRotor with historically correct configuration 
  * 
  * @param rotorType - the type of the rotor. Military Enigma machines had eight standard rotor configurations.
  * @param ringOffset - the offset of the alphabet ring
  * @param rotorPosition - the initial position of the rotor
  */
 
 public EnigmaRotor(String rotorType, char ringOffset, char rotorPosition) {

//  this.ringChar = ringOffset;
  this.positionChar = rotorPosition;
  
  this.ringPerm = new EnigmaRhoPermutation(Alphabet.ALPHABET.indexOf(ringOffset));
  this.positionPerm = new EnigmaRhoPermutation(Alphabet.ALPHABET.indexOf(rotorPosition)); 
  this.markedForMovement = false;
  
  for (int c=0; c<ROTOR_CONFIGURATIONS.length; c++) {
   if (ROTOR_CONFIGURATIONS[c][0].equals(rotorType)) {
//    this.type=ROTOR_CONFIGURATIONS[c][0];
    this.rotorPerm = new EnigmaPermutation(ROTOR_CONFIGURATIONS[c][1]);
    this.notches=ROTOR_CONFIGURATIONS[c][2];
    return;
   }
  }
  throw new InvalidEnigmaConfiguration("Rotor "+rotorType+" is not in the list of known rotors.");
 }
 
 /**
  * Construct custom Enigma rotor
  * @param permutation - gives the permutation of the alphabet that this rotor facilitates.
  * @param rotorType - readable user-specified description of the rotor.
  * @param ringOffset - offset of the alphabet ring of the rotor
  * @param rotorPosition - the initial position of the rotor 
  * @param notches - a String that describes all the notches of the ring with the corresponding letter names on the ring.
  */

 public EnigmaRotor(String permutation, String rotorType, char ringOffset, char rotorPosition, String notches) {
  this.ringPerm = new EnigmaRhoPermutation(Alphabet.ALPHABET.indexOf(ringOffset));
  this.positionPerm = new EnigmaRhoPermutation(Alphabet.ALPHABET.indexOf(rotorPosition));
  this.markedForMovement = false;
  
  //this.type = rotorType;
  this.rotorPerm = new EnigmaPermutation(permutation);
  this.notches = notches;
 }
}

... и един рефлектор, който е реализиран от класа EnigmaReflector.

/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */

package code.lalev.enigmasim;

public class EnigmaReflector implements EnigmaComponent  {
 public static final String[][] REFLECTOR_CONFIGURATIONS = {{"A","EJMZALYXVBWFCRQUONTSPIKHGD"},
   {"B", "YRUHQSLDPXNGOKMIEBFZCWVJAT"}, 
   {"C", "FVPJIAOYEDRZXWGCTKUQSBNMHL"},
   {"B Thin", "ENKQAUYWJICOPBLMDXZVFTHRGS"},
   {"C Thin", "RDOBJNTKVEHMLFCWZAXGYIPSUQ"},
 };

// private String type;
 protected EnigmaPermutation reflectorPerm;
 
 /**
  * Creates a historically accurate Enigma reflector 
  * 
  * @param reflectorType
  */

 public EnigmaReflector(String reflectorType) {
  for (int c=0; c<REFLECTOR_CONFIGURATIONS.length; c++) {
   if (reflectorType.equals(REFLECTOR_CONFIGURATIONS[c][0])) {
//    type=REFLECTOR_CONFIGURATIONS[c][0];
    reflectorPerm = new EnigmaPermutation(REFLECTOR_CONFIGURATIONS[c][1]);
    return;
   }
  }
  
  throw new InvalidEnigmaConfiguration("Reflector "+reflectorType+" is not in the list of known reflectors.");
 }

 public EnigmaReflector(String permutation, String reflectorType) {
  reflectorPerm = new EnigmaPermutation(permutation);
  
  for (int i=0; i<permutation.length(); i++) {
   if (reflectorPerm.permute(reflectorPerm.permute(Alphabet.ALPHABET.charAt(i)))!=Alphabet.ALPHABET.charAt(i)) {
    throw new InvalidEnigmaConfiguration("Reflector permutation must be inverse to itself.");
   }
  }
 }
 
 
 public char permuteChar(char input) {
  return reflectorPerm.permute(input);
 }
 
 public char invertChar(char input) {
  return reflectorPerm.invert(input);
 }
 
};

EnigmaSteckerBoard реализира щекерно табло.

/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */

package code.lalev.enigmasim;

public class EnigmaSteckerBoard implements EnigmaComponent {
 protected EnigmaPermutation steckerPerm;
 
 public EnigmaSteckerBoard(String permutation) {
  this.steckerPerm = new EnigmaPermutation(permutation);
 }
 
 public EnigmaSteckerBoard() {
  this.steckerPerm = new EnigmaPermutation(Alphabet.ALPHABET);
 }
 
 public char permuteChar(char input) {
  return steckerPerm.permute(input);
 } 
 
 public char invertChar(char input) {
  return steckerPerm.invert(input);
 }  
}

А EnigmaETW реализира пермутацията на входното колело.

/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */

package code.lalev.enigmasim;

public class EnigmaETW implements EnigmaComponent {
 protected EnigmaPermutation etwPerm;
  
 /**
  * Creates custom ETW 
  * @param permutation - String that describe 
  */
 public EnigmaETW (String permutation) {
  this.etwPerm = new EnigmaPermutation(permutation);
 }
 
 /**
  * Constructs an ETW that does not permute anything. Such ETW is typical 
  * for military Enigmas.
  */
 public EnigmaETW () {
  this.etwPerm = new EnigmaPermutation(Alphabet.ALPHABET);
 } 
 
 public char permuteChar(char input) {
  return etwPerm.permute(input);
 } 
  
 public char invertChar(char input) {
  return etwPerm.invert(input);
 }  
}
И за финал, елементарна тестова програма, която проверява коректността на различни параметри на кода.
/*
 * This file is authored by Angelin Lalev and may be
 * distributed under GPLv3 license which may be 
 * obtained from:
 * http://www.gnu.org/licenses/gpl-3.0.en.html
 */


package code.lalev.engimasim.unittests;

import code.lalev.enigmasim.*;

public class EnigmaTest {

 public static void main(String[] args) {
  System.out.println("Unit tests for code.lalev.enigmasim package");
  System.out.println("===========================================");

  System.out.print("Constructing I,II,III,B Enigma with no steckerboard or etw ... ");
  EnigmaMachine enigma = new EnigmaMachine();
  EnigmaRotor rotor = new EnigmaRotor("I",'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("II", 'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("III", 'A', 'A');
  enigma.addRotor(rotor);
  EnigmaReflector reflector = new EnigmaReflector("B");
  enigma.addReflector(reflector);
  EnigmaETW etw = new EnigmaETW();
  enigma.addETW(etw);
  EnigmaSteckerBoard board = new EnigmaSteckerBoard();
  enigma.addSteckerBoard(board);
  enigma.validate();
  System.out.println("SUCCESS");
  
  System.out.print("Constructing IV,V,VI,VII,VII,C Enigma with (AB)(CD)(EF)(GH) steckerboard and no etw ... ");
  enigma = new EnigmaMachine();
  rotor = new EnigmaRotor("IV",'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("V", 'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("VI", 'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("VII", 'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("VIII", 'A', 'A');
  enigma.addRotor(rotor);
  reflector = new EnigmaReflector("C");
  enigma.addReflector(reflector);
  etw = new EnigmaETW();
  enigma.addETW(etw);
  board = new EnigmaSteckerBoard("BADCFEHGIJKLMNOPQRSTUVWXYZ");
  enigma.addSteckerBoard(board);
  enigma.validate();
  System.out.println("SUCCESS");

  System.out.print("Catching invalid Enigma with 2 rotors... ");
  enigma = new EnigmaMachine();
  rotor = new EnigmaRotor("IV",'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("V", 'A', 'A');
  enigma.addRotor(rotor);
  reflector = new EnigmaReflector("C");
  enigma.addReflector(reflector);
  etw = new EnigmaETW();
  enigma.addETW(etw);
  board = new EnigmaSteckerBoard("BADCFEHGIJKLMNOPQRSTUVWXYZ");
  enigma.addSteckerBoard(board);
  try {
   enigma.validate();
   System.out.println("FAIL");
  } catch (Exception e) {
   System.out.println("SUCCESS");  
  }
  
  System.out.println("Rotor movement and double stepping tests ...IN PROGRESS");
  enigma = new EnigmaMachine();
  rotor = new EnigmaRotor("I",'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("II", 'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("III", 'A', 'A');
  enigma.addRotor(rotor);
  reflector = new EnigmaReflector("C");
  enigma.addReflector(reflector);
  etw = new EnigmaETW();
  enigma.addETW(etw);
  board = new EnigmaSteckerBoard();
  enigma.addSteckerBoard(board);
  enigma.validate();

  System.out.println("Test 1");
  String desiredoutput = "AAA AAB AAC AAD AAE AAF AAG AAH AAI AAJ ";
  String realoutput=""; 
  System.out.println("Correct: "+desiredoutput);
  for (int i=0; i<10; i++) {
   realoutput=realoutput+enigma.getRotorPositions()+" ";
   enigma.stepRotors();
  }
  System.out.println("Result: "+realoutput);
  if (desiredoutput.equals(realoutput)) {
   System.out.println("SUCCESS");
  } else {
   System.out.println("FAILURE");
  }
  
  enigma = new EnigmaMachine();
  rotor = new EnigmaRotor("I",'A', 'O');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("II", 'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("III", 'A', 'A');
  enigma.addRotor(rotor);
  reflector = new EnigmaReflector("C");
  enigma.addReflector(reflector);
  etw = new EnigmaETW();
  enigma.addETW(etw);
  board = new EnigmaSteckerBoard();
  enigma.addSteckerBoard(board);
  enigma.validate();

  System.out.println("Test 2");
  desiredoutput = "AAO AAP AAQ ABR ABS ABT ";
  realoutput=""; 
  System.out.println("Correct: "+desiredoutput);
  for (int i=0; i<6; i++) {
   realoutput=realoutput+enigma.getRotorPositions()+" ";
   enigma.stepRotors();
  }
  System.out.println("Result: "+realoutput); 
  if (desiredoutput.equals(realoutput)) {
   System.out.println("SUCCESS");
  } else {
   System.out.println("FAILURE");
  }
  
  enigma = new EnigmaMachine();
  rotor = new EnigmaRotor("III",'Z', 'U');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("II", 'P', 'D');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("I", 'Q', 'A');
  enigma.addRotor(rotor);
  reflector = new EnigmaReflector("C");
  enigma.addReflector(reflector);
  etw = new EnigmaETW();
  enigma.addETW(etw);
  board = new EnigmaSteckerBoard();
  enigma.addSteckerBoard(board);
  enigma.validate();

  System.out.println("Test 3 (Double step)");
  desiredoutput = "ADU ADV AEW BFX BFY ";
  realoutput=""; 
  System.out.println("Correct: "+desiredoutput);
  for (int i=0; i<5; i++) {
   realoutput=realoutput+enigma.getRotorPositions()+" ";
   enigma.stepRotors();
  }
  System.out.println("Result: "+realoutput); 
  if (desiredoutput.equals(realoutput)) {
   System.out.println("SUCCESS");
  } else {
   System.out.println("FAILURE");
  }  
  
  
  System.out.println("Testing encryption ... IN PROGRESS"); 
  System.out.println("Test 1. Encrypt: Rotors I, II, III, GrundStellung:AAA Ringstellung:AAA, Refl. B, no etw, Steckerboard:(AB)(CD)");
  enigma = new EnigmaMachine();
  rotor = new EnigmaRotor("I",'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("II", 'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("III", 'A', 'A');
  enigma.addRotor(rotor);
  reflector = new EnigmaReflector("B");
  enigma.addReflector(reflector);
  etw = new EnigmaETW();
  enigma.addETW(etw);
  board = new EnigmaSteckerBoard("BADCEFGHIJKLMNOPQRSTUVWXYZ");
  enigma.addSteckerBoard(board);
  enigma.validate();  
  
  String plaintext = "AAAAAAAA";
  String ciphertext = "WUPGNWOJ";
  String result = enigma.encodeMessage(plaintext);
  System.out.println("Plaintext:"+plaintext);
  System.out.println("Ciphertext:"+ciphertext);
  System.out.println("Result:"+result);
  if (result.equals(ciphertext)) {
   System.out.println("SUCCESS");
  } else {
   System.out.println("FAILURE");
  }

  System.out.println("Test 2. Decrypt: Rotors I, II, III, GrundStellung:AAA Ringstellung:AAA, Refl. B, no etw, Steckerboard:(AB)(CD)");
  enigma = new EnigmaMachine();
  rotor = new EnigmaRotor("I",'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("II", 'A', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("III", 'A', 'A');
  enigma.addRotor(rotor);
  reflector = new EnigmaReflector("B");
  enigma.addReflector(reflector);
  etw = new EnigmaETW();
  enigma.addETW(etw);
  board = new EnigmaSteckerBoard("BADCEFGHIJKLMNOPQRSTUVWXYZ");
  enigma.addSteckerBoard(board);
  enigma.validate();  
  
  plaintext = "AAAAAAAA";
  ciphertext = "WUPGNWOJ";
  result = enigma.encodeMessage(ciphertext);
  System.out.println("Plaintext:"+plaintext);
  System.out.println("Ciphertext:"+ciphertext);
  System.out.println("Result:"+result);
  if (result.equals(plaintext)) {
   System.out.println("SUCCESS");
  } else {
   System.out.println("FAILURE");
  }  
  
  System.out.println("Test 3. Encrypt: Rotors IV, V, VI, GrundStellung:AAA Ringstellung:BBB, Refl.C, no etw, Steckerboard:(AB)(CD)");
  enigma = new EnigmaMachine();
  rotor = new EnigmaRotor("VI",'B', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("V", 'B', 'A');
  enigma.addRotor(rotor);
  rotor = new EnigmaRotor("IV", 'B', 'A');
  enigma.addRotor(rotor);
  reflector = new EnigmaReflector("C");
  enigma.addReflector(reflector);
  etw = new EnigmaETW();
  enigma.addETW(etw);
  board = new EnigmaSteckerBoard("BADCEFGHIJKLMNOPQRSTUVWXYZ");
  enigma.addSteckerBoard(board);
  enigma.validate();  
 
  plaintext = "AAAAAAAA";
  ciphertext = "TDKWFFQE";
  result = enigma.encodeMessage(plaintext);
  System.out.println("Plaintext:"+plaintext);
  System.out.println("Ciphertext:"+ciphertext);
  System.out.println("Result:"+result);
  if (result.equals(ciphertext)) {
   System.out.println("SUCCESS");
  } else {
   System.out.println("FAILURE");
  }  
 }
}