My child is giving a presentation on the topic of encryption. As a supportive dad, I found some interesting articles to help my kid with and that article about the Enigma encoding machine hit my attraction.
This is from https://kryptografie.de/kryptografie/chiffre/enigma.htm where you can also find much information about the discs used and nice details.
When reading this article I thought, yes that would be nice to implement.
class RotorConfiguration:
public record RotorConfiguration(RotorType type, int startPosition) {
}
class EnigmaConfiguration:
public record EnigmaConfiguration(
RotorConfiguration left,
RotorConfiguration center,
RotorConfiguration right,
RotorType reflector) {
public static class Builder {
private RotorConfiguration left = null;
private RotorConfiguration center = null;
private RotorConfiguration right = null;
private RotorType reflector = null;
public Builder withLeftRotor(RotorType type, int position) {
left = new RotorConfiguration(type, position);
return this;
}
public Builder withCenterRotor(RotorType type, int position) {
center = new RotorConfiguration(type, position);
return this;
}
public Builder withRightRotor(RotorType type, int position) {
right = new RotorConfiguration(type, position);
return this;
}
public Builder withReflector(RotorType reflector) {
this.reflector = reflector;
return this;
}
public EnigmaConfiguration build() {
if (reflector == null) {
throw new IllegalArgumentException("No reflector specified");
}
if (left == null) {
throw new IllegalArgumentException("No left rotor specified");
}
if (right == null) {
throw new IllegalArgumentException("No right rotor specified");
}
if (center == null) {
throw new IllegalArgumentException("No center rotor specified");
}
return new EnigmaConfiguration(left, right, center, reflector);
}
}
}
class EnigmaTextConverter:
public final class EnigmaTextConverter {
private static final String ALPHABET_PATTERN = "^[a-zA-Z]*$";
private EnigmaTextConverter(){
//no instantiation of this class - it is a utility class
}
public static String toEnigmaPlain(String textToFormat) {
if (textToFormat == null){
return "";
}
String enigmaPlain = textToFormat.replaceAll("\\s+", "");
if (!enigmaPlain.matches(ALPHABET_PATTERN)) {
throw new IllegalArgumentException("Invalid Enigma Plain - only letters from the alphabet (a-z A-Z) and (white-)space are supported. Especially no punctuation");
}
return enigmaPlain.toUpperCase(); //no localization required, its only A-Z
}
public static String format(String textToFormat) {
if (textToFormat == null){
return "";
}
int blockCount = 0;
StringBuilder result = new StringBuilder();
for(int i = 0; i < textToFormat.length(); i++) {
result.append(textToFormat.charAt(i));
if (i % 5 == 4){
result.append(" ");
blockCount++;
}
if(blockCount == 10){
blockCount = 0;
result.append("\n");
}
}
return result.toString();
}
}
class RotorFileReader:
RotorFileReader {
private RotorFileReader(){
//prevent creation of objects since this is a utility class
}
public static String readNotches(String fileLocation) throws IOException, URISyntaxException {
return readLine(fileLocation, 2);
}
public static String readEncoding(String fileLocation) throws IOException, URISyntaxException {
return readLine(fileLocation, 1);
}
private static String readLine(String fileLocation, int lineNumber) throws IOException, URISyntaxException {
URI uri = Objects.requireNonNull(RotorFileReader.class.getClassLoader().getResource(fileLocation)).toURI();
List<String> lines = Files.readAllLines(Paths.get(uri));
return lines.get(lineNumber);
}
}
class RotorType:
@SuppressWarnings("squid:S115") //unusual enum values like gamma, beta etc are valid in this special case
public enum RotorType {
I, II, III, IV, V, VI, VII, VIII, beta, gamma, UKW_A, UKW_B, UKW_C, UKW_Bs, UKW_Cs;
public String getFileLocation() {
return switch (this) {
case I -> "Rotor_I.txt";
case II -> "Rotor_II.txt";
case III -> "Rotor_III.txt";
case IV -> "Rotor_IV.txt";
case V -> "Rotor_V.txt";
case VI -> "Rotor_VI.txt";
case VII -> "Rotor_VII.txt";
case VIII -> "Rotor_VIII.txt";
case beta -> "Rotor_beta.txt";
case gamma -> "Rotor_gamma.txt";
case UKW_A -> "Rotor_UKW_A.txt";
case UKW_B -> "Rotor_UKW_B.txt";
case UKW_C -> "Rotor_UKW_C.txt";
case UKW_Bs -> "Rotor_UKW_Bs.txt";
case UKW_Cs -> "Rotor_UKW_Cs.txt";
};
}
boolean hasNotch() {
return switch (this) {
case I, II, III, IV, V, VI, VII, VIII, beta, gamma -> true;
case UKW_A, UKW_B, UKW_C, UKW_Bs, UKW_Cs -> false;
};
}
}
class Rotor:
public class Rotor {
private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private final String encoding;
private final String notches;
private int position;
private final Rotor connected;
public Rotor(RotorType rotorType, int position, Rotor connected) throws IOException, URISyntaxException {
this.position = position;
this.connected = connected;
encoding = RotorFileReader.readEncoding(rotorType.getFileLocation());
notches = rotorType.hasNotch() ? RotorFileReader.readNotches(rotorType.getFileLocation()) : null;
}
public Rotor(RotorType rotorType) throws IOException, URISyntaxException {
this(rotorType, 1, null);
}
public void rotate() {
position++;
if (position > 26) {
position = 1;
}
if(isNotchedAtCurrentPosition() && connected != null) {
connected.rotate();
}
}
private boolean isNotchedAtCurrentPosition() {
String character = String.valueOf(ALPHABET.charAt(position - 1));
return isNotchedCharacter(character);
}
private boolean isNotchedCharacter(String character) {
for (int i = 0; i < notches.length(); i++) {
if (character.equals(String.valueOf(notches.charAt(i)))) {
return true;
}
}
return false;
}
//visible for tests
String mapCodeOut(String letter){
int posOfLetter = positionInAlphabet(letter);
return String.valueOf(encoding.charAt(posOfLetter));
}
//visible for tests
String mapCodeIn(String letter){
int posOfLetter = positionInEncoding(letter);
return String.valueOf(ALPHABET.charAt(posOfLetter));
}
//visible for tests
String mapRotationOut(String letter){
char rotatedLetter = (char)(letter.charAt(0) - (position-1));
if (rotatedLetter < 'A'){
rotatedLetter = (char)(rotatedLetter + 26);
}
return String.valueOf(rotatedLetter);
}
//visible for tests
String mapRotationIn(String letter){
char rotatedLetter = (char) (letter.charAt(0) + (position - 1));
if (rotatedLetter > 'Z') {
rotatedLetter = (char) (rotatedLetter - 26);
}
return String.valueOf(rotatedLetter);
}
private int positionInAlphabet(String letter) {
for(int i = 0; i < 26; i++) {
if (letter.equals(String.valueOf(ALPHABET.charAt(i)))) {
return i;
}
}
throw new IllegalStateException("not mappable character '"+ letter +"' - must be ABC..XYZ");
}
private int positionInEncoding(String letter) {
for(int i = 0; i < 26; i++) {
if (letter.equals(String.valueOf(encoding.charAt(i)))) {
return i;
}
}
throw new IllegalStateException("not mappable character '"+ letter +"' - must be ABC..XYZ");
}
public String encode(String letter) {
String rotated = mapRotationIn(letter);
return mapCodeIn(rotated);
}
public String decode(String letter) {
String rotated = mapCodeOut(letter);
return mapRotationOut(rotated);
}
}
class EnigmaMachine:
public class EnigmaMachine {
private final Rotor left;
private final Rotor right;
private final Rotor center;
private final Rotor reflector;
public EnigmaMachine(EnigmaConfiguration configuration) throws IOException, URISyntaxException {
reflector = new Rotor(configuration.reflector());
this.left = new Rotor(configuration.left().type(), configuration.left().startPosition(), reflector);
this.center = new Rotor(configuration.center().type(), configuration.center().startPosition(), left);
this.right = new Rotor(configuration.right().type(), configuration.right().startPosition(), center);
}
public String encode(String text) {
String enigmaPlainText = EnigmaTextConverter.toEnigmaPlain(text);
StringBuilder encoded = new StringBuilder();
for(String letter: textToList(enigmaPlainText)){
String encodedLetter = encodeLetter(letter);
encoded.append(encodedLetter);
right.rotate();
}
return encoded.toString();
}
@SuppressWarnings("squid:S1488") //do not return values directly, for more readability
private String encodeLetter(String letter) {
String fromRight = right.encode(letter);
String fromCenter = center.encode(fromRight);
String fromLeft = left.encode(fromCenter);
String reflected = reflector.encode(fromLeft);
String backLeft = left.decode(reflected);
String backCenter = center.decode(backLeft);
String backRight = right.decode(backCenter);
return backRight;
}
private List<String> textToList(String text) {
return IntStream.range(0, text.length()).mapToObj(i -> String.valueOf(text.charAt(i))).toList();
}
}
class App: an example application
public class App {
public static void main(String[] args) throws IOException, URISyntaxException {
//https://kryptografie.de/kryptografie/chiffre/enigma.htm
//text from the side above - just a historical text
// Das Oberkommando der Wehrmacht gibt bekannt: Aachen ist gerettet. Durch gebündelten Einsatz der
// Hilfskräfte konnte die Bedrohung abgewendet und die Rettung der Stadt gegen 18:00 Uhr sichergestellt werden.
String plaintext = """
DAS OBERKOMMANDO DER WEHRMAQT GIBT BEKANNTX AACHENXAACHENX
IST GERETTETX DURQ GEBUENDELTEN EINSATZ DER HILFSKRAEFTE KONNTE
DIE BEDROHUNG ABGEWENDET UND DIE RETTUNG DER STADT GEGEN
XEINSXAQTXNULLXNULLX UHR SIQERGESTELLT WERDENX
""";
System.out.println(plaintext);
String enigmaPlainText = EnigmaTextConverter.toEnigmaPlain(plaintext);
System.out.println(EnigmaTextConverter.format(enigmaPlainText));
System.out.println();
EnigmaConfiguration configuration = new EnigmaConfiguration.Builder()
.withLeftRotor(RotorType.I, 16)
.withCenterRotor(RotorType.IV, 26)
.withRightRotor(RotorType.III, 8)
.withReflector(RotorType.UKW_A)
// .withPlugs("AD", "CN", "ET", "FL", "GI", "JV", "KZ", "PU", "QY", "WX")
.build();
EnigmaMachine encodeEnigma = new EnigmaMachine(configuration);
String chiffre = encodeEnigma.encode(enigmaPlainText);
String enigmaChiffreText = EnigmaTextConverter.format(chiffre);
System.out.println(enigmaChiffreText);
EnigmaMachine decodeEnigma = new EnigmaMachine(configuration);
String decodedPlain = decodeEnigma.encode(chiffre);
System.out.println(decodedPlain);
}
}
testcase class RotorTest:
class RotorTest {
@ParameterizedTest
@CsvSource({"A,1,A", "A,2,B", "A,3,C", "B,1,B", "Z,1,Z", "Z,2,A"})
void testMapRotationIn_changingLetter(String in, int postion, String expectedOut) throws IOException, URISyntaxException {
Rotor rotor = new Rotor(RotorType.I, postion, mock(Rotor.class));
String out = rotor.mapRotationIn(in);
Assertions.assertEquals(expectedOut, out);
}
@ParameterizedTest
@CsvSource({"A,1,A", "A,2,B", "A,3,C", "A,26,Z"})
void testMapRotationIn_changingPosition(String in, int postion, String expectedOut) throws IOException, URISyntaxException {
Rotor rotor = new Rotor(RotorType.I, postion, mock(Rotor.class));
String out = rotor.mapRotationIn(in);
Assertions.assertEquals(expectedOut, out);
}
@ParameterizedTest
@CsvSource({"A,1,A", "B,2,A", "C,3,A", "A,2,Z"})
void testMapRotationOut_changingLetter(String in, int postion, String expectedOut) throws IOException, URISyntaxException {
Rotor rotor = new Rotor(RotorType.I, postion, mock(Rotor.class));
String out = rotor.mapRotationOut(in);
Assertions.assertEquals(expectedOut, out);
}
@ParameterizedTest
@CsvSource({"A,1,A", "A,2,B", "A,3,C", "A,26,Z"})
void testMapRotationOut_changingPosition(String in, int postion, String expectedOut) throws IOException, URISyntaxException {
Rotor rotor = new Rotor(RotorType.I, postion, mock(Rotor.class));
String out = rotor.mapRotationIn(in);
Assertions.assertEquals(expectedOut, out);
}
@ParameterizedTest
@CsvSource({"A,E", "B,K", "C,M"})
void testMapRightToLeft(String in, String expectedOut) throws IOException, URISyntaxException {
Rotor rotor = new Rotor(RotorType.I, 1, mock(Rotor.class));
String out = rotor.mapCodeOut(in);
Assertions.assertEquals(expectedOut, out);
}
@ParameterizedTest
@CsvSource({"E,A", "K,B", "M,C"})
void testMapLeftToRight(String in, String expectedOut) throws IOException, URISyntaxException {
Rotor rotor = new Rotor(RotorType.I, 1, mock(Rotor.class));
String out = rotor.mapCodeIn(in);
Assertions.assertEquals(expectedOut, out);
}
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, 7, 9, 11, 13, 15, 16, 17, 25, 26})
void testRotate(int number) throws IOException, URISyntaxException {
Rotor rotor = new Rotor(RotorType.I, 1, mock(Rotor.class));
String input = "A";
String expected = String.valueOf((char) (input.charAt(0) + (number - 1)));
for (int i = 1; i < number; i++) {
rotor.rotate();
}
Assertions.assertEquals(expected, rotor.mapRotationIn(input));
}
@ParameterizedTest
@EnumSource(
value = RotorType.class,
names = {"I", "II", "III", "IV", "V"})
void testOneNotch(RotorType type) throws IOException, URISyntaxException {
//given
Rotor mock = mock(Rotor.class);
Rotor rotor = new Rotor(type, 1, mock);
//when (1x komplett drehen)
for (int i = 1; i <= 26; i++) {
rotor.rotate();
}
//then
verify(mock, times(1)).rotate();
}
@ParameterizedTest
@EnumSource(
value = RotorType.class,
names = {"VI", "VII", "VIII"})
void testTwoNotches(RotorType type) throws IOException, URISyntaxException {
Rotor mock = mock(Rotor.class);
Rotor rotor = new Rotor(type, 1, mock);
//when (1x komplett drehen)
for (int i = 1; i <= 26; i++) {
rotor.rotate();
}
//then
verify(mock, times(2)).rotate();
}
}
Example of resource text file "Rotor_VI.txt"
ABCDEFGHIJKLMNOPQRSTUVWXYZ
JPGVOUMFYQBENHZRDKASXLICTW
ZM

