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.
Work principles of the Enigma Machine
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 diagram: enter image description here
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
2 Answers 2
"...giving a presentation on the topic of encryption..."
Make sure that the difference between "enciphering" and "encryption" is understood.
"Rot13" is a well-known "enciphering" scheme that is child's play to decipher.
(The difference is analogous to that between "arithmetic" and "mathematics".)
Code Review
public String encode()
Oops!
In a real Enigma, the right rotor advances 1 step BEFORE the input letter is encoded to an output letter.
(Notches may cause adjacent rotors to also advance 1 step simultaneously.)
Plugboard
Mooted, but not implemented.
Watch this Numberphile video in which James Grimes demonstrates that the plugboard contributes massively to the tally of Enigma's enciphering scheme variations. (Note: @04:48 Dr. Grimes mentions the message recipient "on a ship, somewhere." The Kriegsmarine used 4 rotor Enigma machines.)
Wehrmacht or Kriegsmarine
This code implements the 3 rotor Enigma, but is loaded with "provisional" references to the 4 rotor version.
Since this is a "software simulator", you can do what you want (as long as the results are compatible with those of an actual Enigma machine.)
Suggestion: Define your own "null rotor" (simply passthrough 'A'->'A', etc.), and change the code to always process 4 rotors. For "3 rotor" implementation, use your "null rotor" in the leftmost position. It will behave as if it is not even there.
Once you've got the code written to handle 4 rotors, make sure the 4th rotor does not "step forward" because of a notch (just like the real device.)
(Suddenly, "left", "middle" and "right" seem like sub-optimal names to be used in the code.)
(I've forgotten the exact details, but...
A real 4 rotor Enigma, with a certain reflector and certain 4th rotor combination WILL en-/de-cipher as if it were a 3 rotor Enigma device. Thereby, in a pinch, a "navy" device could be used in an "army" network, provided the day's "army network" configuration settings were available to the operator(s).)
Misleading Comment
"...only letters from the alphabet (a-z A-Z) and (white-)space are supported."
The Enigma machine deals only with UPPERCASE alphabetic characters. This code would "spit up" if provided with a plaintext string that includes whitespace characters.
Suggestion: For this "toy" implementation, ignore the "blocks of 5 characters" expression of ciphertext (output), and simply "passthrough" (unenciphered) any non-alphabetic characters. You want the user to have fun; not to be frustrated by triggering error messages and aborts.
Configuration Validation
The presented code is incomplete. The user has no control over the enciphering settings (reflector, rotor choice, sequence, ring settings, initial position, plugboard pairs.)
Each of these "settings" should be under the control of the user, not the coder.
Code to load and validate "user settings" is both trivial and tedious.
Its absence is understandable (in a way.)
Off topic
Jarod Owen has produced a fantastic animation/explanation of the inner workings of an Enigma machine.
Dein Kind might enjoy exploring Enigma enciphering if s/he and a friend each had a "Pringles Can Enigma" machine they could hold in their hands. Here is a demonstration video of how to use this "poor man's Enigma". The video's description includes a link to the printable PDF you can use to make your own version at home. (Enjoy emptying the can, first.)
-
1\$\begingroup\$ that review was very insightful - i was hoping for some input about "change to a 4-disc-machine". Writing code that is easy to enhance or easy to add features. Another of my nemesis is being not accurate - you just really touched that issue (encryption/encyphering or the thing with the notch) - and i am very glad you did so!! about that invalid input: my implementation uses an
EnigmaTextConverter
internally that turns[a-zA-Z ]
into[A-Z]
- so this is just a input handler that is used before the real input \$\endgroup\$Martin Frank– Martin Frank2024年10月30日 06:10:29 +00:00Commented Oct 30, 2024 at 6:10 -
1\$\begingroup\$ that video is great! \$\endgroup\$Martin Frank– Martin Frank2024年10月30日 06:11:54 +00:00Commented Oct 30, 2024 at 6:11
-
1\$\begingroup\$ haha i wish i could upvote twice ^^ \$\endgroup\$Martin Frank– Martin Frank2024年10月30日 15:37:37 +00:00Commented Oct 30, 2024 at 15:37
-
\$\begingroup\$ @MartinFrank You can both UV and "accept" an answer. Maybe wait another day or two for other answers to be posted. (I suggest Alexander's answer is the best so far. That answer is more about your question: "coding Enigma in java", whereas this one is merely off-the-top-of-my-head. Yes, Jarod Owen has posted a number of very good "explainer" videos to YouTube.) Enjoy playing with your implementation... Cheers! \$\endgroup\$Fe2O3– Fe2O32024年10月30日 19:53:47 +00:00Commented Oct 30, 2024 at 19:53
-
1\$\begingroup\$ @MartinFrank Settings: Reflect:B, Rotors: II III V, Ringset: ENG, Initial: DEU, Plugs: {none}, Ciphertext: "LQC JFZZTTS KUKWK ZFDW QXGN"... There are, of course, online Enigma websites where you can verify the functioning of your own implementation agrees with "public" versions... Cheers!
:-)
\$\endgroup\$Fe2O3– Fe2O32024年10月31日 01:40:18 +00:00Commented Oct 31, 2024 at 1:40
Design
- Reflector
You modeled reflector as just another rotor, but it needs a separate abstraction.
Because unlike rotors, reflector doesn't rotate during encryption process, hence property position
and method rotate()
doesn't make any sense for it.
Additionally, reflector it has only one way to transform a letter and its mappings should describe 13 wires connecting contacts on its single working surface.
Whiles, each rotor has 26 wires connecting its left and right sides. And it can scramble a letter in two ways depending on the flow of signal (whether it goes from the input wheel to the reflector, or in the reversed direction). Hence, unlike reflector, rotor needs two different methods to represent the forward and backward flow.
Different behavior warrants a different abstraction.
- Rotor
The rotor implementation is tightly coupled with the way the rotor mapping and notches are being initialized.
public Rotor(RotorType rotorType, int position, Rotor nextRotor) throws IOException, URISyntaxException {
this.position = position;
this.nextRotor = nextRotor;
encoding = RotorFileReader.readEncoding(rotorType.getFileLocation());
notches = rotorType.hasNotch() ? RotorFileReader.readNotches(rotorType.getFileLocation()) : null;
}
It impedes refactoring and testing. To verify the behavior of an individual rotor without mocks, you need RotorFileReader
and a file describing a corresponding rotor type on a class-path (if you were doing tests-first this design decision will put pressure on you).
Placing complex logic into a constructor isn't a good idea, let alone constructor propagating checked exceptions doesn't look nicely.
Also, there's an issue with the approach of reading the rotor-setting in a piece-mill fashion while constructing a new rotor. Application might crash unexpectedly at any moment because one of the files is missing (for instance, because file-name was misspelled).
Instead, I would suggest introducing a RotorFactory
, that reads, validates and caches all rotor-setting at the application start-up (when a factory instance is constructed).
And make a Rotor
's constructor to expect all the setting needed to create a fully initialized instance, rather than performing IO from it.
- Why describing letters is a
String
?
I don't think you benefit anyhow from String
to represent a letter to be scrambled.
Enigma is meant to work with Latin letters only, and describing them as a String
instead of char
or int
code-point only unnecessary complicates the logic.
If you used numeric primitives, you will be able to leverage them while performing mapping.
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");
}
As a quick improvement, this whole loop can be replaced with ALPHABET.indexOf(letter);
, and you will need only to check against -1
index afterwards. But it still will perform unnecessary linear scanning over the ordered sequence of characters.
If you used char
, or int
instead, this method boils down to a mere letter - 'A'
with a prior check if letter
is in range[A,Z]
.
The reversed mapping would still require linear search. An alternative option to looping through the encoding
string is to use a Map
.
Similarly, notches
can be represented by a Set
. Not for the sake of performance, there were only 1 or 2 notches on a rotor. But to make the code cleaner, replacing yet another loop and condition with a concise expressive statement.
EnigmaMachine
class
This class explicitly defines left
, right
and centert
rotors as its fields.
Even if describing 3-rotor enigma machine is the ultimate goal (and there's no intention to consider 4-rotor variations), it's still not the best approach.
I would prefer modeling them as a structure internally shaped as a linked list
to avoid of fiddling with every individual rotor separately, as you do in the
EnigmaMachine.encodeLetter()
;to simplify the process of passing a letter through rotors;
last but not the least, the ability to construct a ready-to-use enigma instance with any number of rotors (even one) might prove useful in testing, because we can verify by hand data-transformations performed by a one-rotor enigma.
Method mentioned in the point 1)
has another issue which I want to bring up:
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;
}
The thing that catches the eye (apart from micromanaging every character transformation), the method names are misleading and create an impression as if the first part of the method encrypts the given letter and the second part somehow decrypts it back.
Which is not the case, because the reflector sends the signal reversed direction over a different path. Every call scrambles the letter, and the output letter is guaranteed by design never to be equal to the input (which is a vulnerability of the enigma cipher). In order to decrypt a character, it should go through the same path from the input wheal through the rotors to the reflector and then again back though the rotors to the input wheal.
I would advise renaming the rotor methods into something along the lines of mapForward()
and mapBackward()
.
Refactoring
Here's the general outline of how EnigmaMachine
implementation might look like if we introduce a new abstraction encapsulating components responsible for scrambling characters (rotors and reflector), which is called EncryptionBlock
in the code shown below.
public class EnigmaMachine {
private final EncryptionBlock encryptionBlock;
// all-args constructor
public String encrypt(String input) {
return input.chars()
.mapToObj(c -> encryptionBlock.encryptNext((char) c))
.map(String::valueOf)
.collect(joining());
}
}
Rotors are modeled as a doubly-linked list (i.e. each rotor know about the next and the previous one) and EncryptionBlock
has references the first (rightmost) and the last (leftmost) rotors.
public class EncryptionBlock {
private final Rotor firstRotor;
private final Rotor lastRotor;
private final Reflector reflector;
// all-args constructor
public char encryptNext(char input) {
rotateBlock();
return scrambleLetter(input);
}
private void rotateBlock() {
firstRotor.rotate();
}
private char scrambleLetter(char input) {
char forwardMapped = firstRotor.mapForward(input);
char reflected = reflector.map(forwardMapped);
return lastRotor.mapBackward(reflected);
}
}
-
\$\begingroup\$ there is tight coupling between rotor and the reader which is indeed a bad design - even though i tested the functionallity, this is a bad desig choice - you made a good point! Honestly i was not 100% sure if i did the right implementation as i understood the mechanics of the machine not completly (i got other feedback on wrong implementation) and i am still learning. Thank you for making it more correct! \$\endgroup\$Martin Frank– Martin Frank2024年10月30日 12:49:20 +00:00Commented Oct 30, 2024 at 12:49
-
2\$\begingroup\$ "Java is a little unsuitable for letter operations" - when we need to support an arbitrary Unicode character, then
char
is a big no-no. But it's not a valid concern in this case, since Enigma by design is confined to the range of letters[A,Z]
. And in fact, you're already usingString
a mere single-char
containerletter.charAt(0)
(you're implicitly expecting that letter string contains exactly one character). You can map a string to a string with using aHashMap
, but there's no direct way to apply rotation to aString
. \$\endgroup\$Alexander Ivanchenko– Alexander Ivanchenko2024年10月30日 15:28:46 +00:00Commented Oct 30, 2024 at 15:28 -
1\$\begingroup\$ Please forgive me "weighing-in" on the discussion... "Departing from the original 26 characters of Enigma??" Changing the fundamentals might be tempting, but please consider that the ciphertext was to be transmitted in conventional (limited) Morse Code. Is it not enough that the original QWERTZ keyboard is QWERTY in most of the world? You wouldn't put "fuel injection" on a horse, would you?
:-)
\$\endgroup\$Fe2O3– Fe2O32024年10月30日 20:21:22 +00:00Commented Oct 30, 2024 at 20:21 -
1\$\begingroup\$ @MartinFrank Oh, that's a bit more manageable than imagined. One option is to use contiguous Unicode range
[U+0020,U+005A]
(in decimal[32, 90]
) which apart from digits and whitespace will incorporate all punctuation, comparison symbols and arithmetic operation symbols. With this approach, you can continue leveragingchar
type. \$\endgroup\$Alexander Ivanchenko– Alexander Ivanchenko2024年10月31日 13:45:05 +00:00Commented Oct 31, 2024 at 13:45 -
1\$\begingroup\$ @MartinFrank If you prefer to include only selected symbols, you can no longer leverage numeric properties of characters, but it's still doable without linear scanning. I would introduce an abstraction to handle mapping in this case, here's a quick example of how it might look like (I used character, but this mapper can easily be changed to operate with integer code points or strings). Note that adding new symbols would cause rotors located after the first one to rotate less frequently, which can be compensated by increasing the number of notches. \$\endgroup\$Alexander Ivanchenko– Alexander Ivanchenko2024年10月31日 14:05:29 +00:00Commented Oct 31, 2024 at 14:05