DEV Community

Cover image for Building an Enigma machine with only TypeScript and then use Angular DI system to properly instantiate it
Maxime
Maxime

Posted on • Edited on

Building an Enigma machine with only TypeScript and then use Angular DI system to properly instantiate it

This blog post is the second of a series of 3, called "Enigma: Understand it, implement it, crack it":

Table of contents

If you find any typo please just make the edit yourself here: https://github.com/maxime1992/my-dev.to/blob/master/blog-posts/enigma-part-2/enigma-part-2.md and submit a pull request 👌

Intro

In the first blog post of this series, we've seen the internal mechanism of Enigma. In this one, I'll explain how I decided to implement it.

The Enigma library I've built has nothing to do with Angular, it's just pure TypeScript. The reasons behind that are:

  • It shouldn't in the first place because it could be used as a separate package with vanilla JS or any other framework
  • [⚠️ Spoiler alert ⚠️] To crack Enigma in the next blog post of the series, we will use a web worker and importing anything from Angular within the worker context would break it as it's not aware of the DOM at all

BUT. For Angular lovers, worry no more. We will use Angular and especially its dependency injection API to build the UI that'll consume Enigma library.

Note: In order to correctly manage potential errors, the library does some checks (on the reflectors, the rotors, etc). Those checks have been skipped in the code examples to keep the main logic as small as possible. When that's the case, I've added a comment "// [skipped] and the reason" but feel free to check the complete source code here: https://github.com/maxime1992/my-dev.to/tree/master/libs/enigma/enigma-machine

1 - Enigma library

In order to build the machine, we will do so from bottom to top, which means starts with the reflector, then with the rotors and finally the machine itself.

A - Reflector

Reminder: a reflector is a simple map where an index is connected to another.

Multiple reflectors were available so the first thing to do is being able to set the reflector configuration. If we take the reflector called "Wide B": yruhqsldpxngokmiebfzcwvjat it means that A (index 0) is mapping to Y (index 24) and etc. So when someone types a letter on Enigma, it goes through the 3 rotors and after the last one, will go through the reflector. The rotor input might be at any index between 0 and 25 and we want to be able to find in a simple way the corresponding output:

export class ReflectorService {
  private reflectorConfig: number[] = [];

  constructor(reflectorConfig: string) {
    this.setReflectorConfig(reflectorConfig);
  }

  private setReflectorConfig(reflectorConfig: string): void {
    // [skipped] check that the reflector config is valid

    this.reflectorConfig = this.mapLetterToAbsoluteIndexInAlphabet(reflectorConfigSplit);

    // [skipped] check that every entry of the reflector maps to a different one
  }

  private mapLetterToAbsoluteIndexInAlphabet(alphabet: Alphabet): number[] {
    return alphabet.reduce((map: number[], letter: Letter, index: number) => {
      map[index] = getLetterIndexInAlphabet(letter);

      return map;
    }, []);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Now that we've remapped the string to an array that lets us find the output index for a given input, we need to expose a method so that the machine itself will be able to go through the rotor for a given index:

public goThroughFromRelativeIndex(index: number): number {
  return this.reflectorConfig[index];
}
Enter fullscreen mode Exit fullscreen mode

As you can see, implementing the reflector was quite an easy task. Let's take a look to the rotors now.

B - Rotor

Reminder: a rotor consist of 2 disks connected together with wires. So for a given input index, the output could be the same as the input (in contrary to the reflector).

For a given rotor, we express the rotor configuration with letters, just like we did for the reflector. For example, the first rotor has the following configuration: ekmflgdqvzntowyhxuspaibrcj. As a rotor will spin, instead of thinking with letters, I found it much easier to think of it and deal with it through relative indexes.

For example with the configuration above, we can represent it like the following:

a   b   c   d   ...  w   x   y   z   Alphabet...
|   |   |   |   ...  |   |   |   |   is remapped to...
e   k   m   f   ...  b   r   c   j   a new alphabet

But internally we want is as:

0   1   2   3   ...  22  23  24  25
|   |   |   |   ...  |   |   |   |
+4  +9  +10 +2  ...  +5  +20 +4  +10
Enter fullscreen mode Exit fullscreen mode
export class EnigmaRotorService {
  private rotor: BiMap;
  private currentRingPosition = 0;

  constructor(rotorConfig: string, currentRingPosition: number = LetterIndex.A) {
    const rotorConfigSplit: string[] = rotorConfig.split('');

    // [skipped] check that the string is correctly mapping to alphabet

    this.rotor = createBiMapFromAlphabet(rotorConfigSplit);

    this.setCurrentRingPosition(currentRingPosition);
  }

  public setCurrentRingPosition(ringPosition: number): void {
    // [skipped] check that the ring position is correct

    this.currentRingPosition = ringPosition;
  }

  public getCurrentRingPosition(): number {
    return this.currentRingPosition;
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

The above implementation seems relatively small but what's the function createBiMapFromAlphabet? It's the function in charge of doing the remapping from a string to a bi map with relative indexes. The reason to have a bi map here is because we want to be able to go through the rotor from left to right and right to left. The challenge here is that we do not want to have to deal with negative indexes at any time. So if the current position of the rotor is Z and the relative input is 0, we know that Z --> J with is equivalent to index 25 --> +10. On the contrary, when going from right to left, if we're on the letter J (index 10) it's going to map to Z which won't be -10 but +17. Here's the implementation:

export const createBiMapFromAlphabet = (alphabet: Alphabet): BiMap => {
  return alphabet.reduce(
    (map: BiMap, letter: Letter, index: number) => {
      const letterIndex: number = getLetterIndexInAlphabet(letter);
      map.leftToRight[index] = moduloWithPositiveOrNegative(ALPHABET.length, letterIndex - index);
      map.rightToLeft[letterIndex] = moduloWithPositiveOrNegative(ALPHABET.length, -(letterIndex - index));

      return map;
    },
    { leftToRight: [], rightToLeft: [] } as BiMap,
  );
};
Enter fullscreen mode Exit fullscreen mode

Now, we've got 3 things left for the public API of the rotor:

  • Being able to get the current position
  • Being able to go through the rotor from left to right
  • Being able to go through the rotor from right to left
public getCurrentRingPosition(): number {
  return this.currentRingPosition;
}

private goThroughRotor(
  from: 'left' | 'right',
  relativeIndexInput: number
): number {
  const currentRelativeIndexOutput = this.rotor[
    from === 'left' ? 'leftToRight' : 'rightToLeft'
  ][(this.currentRingPosition + relativeIndexInput) % ALPHABET.length];

  return (relativeIndexInput + currentRelativeIndexOutput) % ALPHABET.length;
}

public goThroughRotorLeftToRight(relativeIndexInput: number): number {
  return this.goThroughRotor('left', relativeIndexInput);
}

public goThroughRotorRightToLeft(relativeIndexInput: number): number {
  return this.goThroughRotor('right', relativeIndexInput);
}
Enter fullscreen mode Exit fullscreen mode

Last remaining bit of the library: The machine itself!

C - Machine

The machine is conducting the orchestra and making all letters of a message go through rotors/reflector/rotors plus spinning the rotors when needed. It has a public API to get/set the initial state of the rotors, get the current state of the rotors and encrypt/decrypt a message.

Let's look at first at how to keep track of the internal state for the rotors (initial and current state):

interface EnigmaMachineState {
  initialStateRotors: RotorsStateInternalApi;
  currentStateRotors: RotorsStateInternalApi;
}

export class EnigmaMachineService {
  private readonly state$: BehaviorSubject<EnigmaMachineState>;

  private readonly initialStateRotorsInternalApi$: Observable<
    RotorsStateInternalApi
  >;
  private readonly currentStateRotorsInternalApi$: Observable<
    RotorsStateInternalApi
  >;

  public readonly initialStateRotors$: Observable<RotorsState>;
  public readonly currentStateRotors$: Observable<RotorsState>;

  // ...
Enter fullscreen mode Exit fullscreen mode

Using Redux for this class would be slightly overkill but reusing the concepts feels great. We use a BehaviorSubject to hold the whole state which is immutable. Easier to debug, easier to share as observables, it will also help for performance and let us set all our components to ChangeDetectionStrategy.OnPush 🔥.

I usually prefer to set all the properties directly but in our case, before setting them we want to make sure that the ones passed are correct and we make the checks + assignments in the constructor:

export class EnigmaMachineService {
  // ...
  constructor(private enigmaRotorServices: EnigmaRotorService[], private reflectorService: ReflectorService) {
    // [skipped] check that the rotor services are correctly defined

    // instantiating from the constructor as we need to check first
    // that the `enigmaRotorService` instances are correct
    const initialStateRotors: RotorsStateInternalApi = this.enigmaRotorServices.map(enigmaRotorService =>
      enigmaRotorService.getCurrentRingPosition(),
    ) as RotorsStateInternalApi;

    this.state$ = new BehaviorSubject({
      initialStateRotors,
      currentStateRotors: initialStateRotors,
    });

    this.initialStateRotorsInternalApi$ = this.state$.pipe(
      select(state => state.initialStateRotors),
      shareReplay({ bufferSize: 1, refCount: true }),
    );
    this.currentStateRotorsInternalApi$ = this.state$.pipe(
      select(state => state.currentStateRotors),
      shareReplay({ bufferSize: 1, refCount: true }),
    );

    this.initialStateRotors$ = this.initialStateRotorsInternalApi$.pipe(
      map(this.mapInternalToPublic),
      shareReplay({ bufferSize: 1, refCount: true }),
    );
    this.currentStateRotors$ = this.currentStateRotorsInternalApi$.pipe(
      map(this.mapInternalToPublic),
      shareReplay({ bufferSize: 1, refCount: true }),
    );

    this.currentStateRotorsInternalApi$
      .pipe(
        tap(currentStateRotors =>
          this.enigmaRotorServices.forEach((rotorService, index) =>
            rotorService.setCurrentRingPosition(currentStateRotors[index]),
          ),
        ),
        takeUntilDestroyed(this),
      )
      .subscribe();
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Few things to note from the code above:

All the properties that we expose as observables are driven from our store (the only source of truth). Every time the current state changes, we set the rotors positions accordingly. We also keep track or the initial state and current state of the rotors in 2 different ways: One is internal, the other is not. For us, it's easier to deal with indexes instead of letters (internal) but when we expose them (to display in the UI for e.g.) we don't want the consumer to figure out that 18 stands for s, we just return s.

The other interesting part in the code above is the usage of shareReplay with the argument { bufferSize: 1, refCount: true }. It'll allow us to share our observables instead of re-subscribing to them multiple times 👍. Using shareReplay(1) would work but would be quite dangerous as if no one is listening anymore to the observable it wouldn't unsubscribe. That is why we need to pass refCount as true.

Now that we've seen how we share the state of our Enigma machine with the rest of the app, let see how the main part of the app works: Encoding a letter through the machine:

export class EnigmaMachineService {
  // ...
  private readonly encodeLetterThroughMachine: (letter: Letter) => Letter = flow(
    // the input is always emitting the signal of a letter
    // at the same position so this one is absolute
    getLetterIndexInAlphabet,
    this.goThroughRotorsLeftToRight,
    this.goThroughReflector,
    this.goThroughRotorsRightToLeft,
    getLetterFromIndexInAlphabet,
  );
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Is that... it? Yes! Pretty much.

In the above code, flow will run all those functions sequentially and pass to the next function the result of the previous one, which works quite nicely in this case as the result of the input (keyboard) goes to the first rotor, the result of the first rotor goes to the second rotor, etc.

Neat, right?

export class EnigmaMachineService {
  // ...

  private encryptLetter(letter: Letter): Letter {
    // [skipped] check that the letter is valid

    // clicking on a key of the machine will trigger the rotation
    // of the rotors so it has to be made first
    this.goToNextRotorCombination();

    return this.encodeLetterThroughMachine(letter);
  }

  public encryptMessage(message: string): string {
    this.resetCurrentStateRotorsToInitialState();

    return message
      .toLowerCase()
      .split('')
      .map(letter =>
        // enigma only deals with the letters from the alphabet
        // but in this demo, typing all spaces with an "X" would
        // be slightly annoying so devianting from original a bit
        letter === ' ' ? ' ' : this.encryptLetter(letter as Letter),
      )
      .join('');
  }

  private resetCurrentStateRotorsToInitialState(): void {
    const state: EnigmaMachineState = this.state$.getValue();

    this.state$.next({
      ...state,
      currentStateRotors: [...state.initialStateRotors] as RotorsStateInternalApi,
    });
  }

  private goToNextRotorCombination(): void {
    const state: EnigmaMachineState = this.state$.getValue();

    this.state$.next({
      ...state,
      currentStateRotors: goToNextRotorCombination(state.currentStateRotors),
    });
  }

  private goThroughRotorsLeftToRight(relativeInputIndex: number): number {
    return this.enigmaRotorServices.reduce(
      (relativeInputIndexTmp, rotorService) => rotorService.goThroughRotorLeftToRight(relativeInputIndexTmp),
      relativeInputIndex,
    );
  }

  private goThroughRotorsRightToLeft(relativeInputIndex: number): number {
    return this.enigmaRotorServices.reduceRight(
      (relativeInputIndexTmp, rotorService) => rotorService.goThroughRotorRightToLeft(relativeInputIndexTmp),
      relativeInputIndex,
    );
  }

  private goThroughReflector(relativeInputIndex: number): number {
    return this.reflectorService.goThroughFromRelativeIndex(relativeInputIndex);
  }

  public setInitialRotorConfig(initialStateRotors: RotorsState): void {
    const state: EnigmaMachineState = this.state$.getValue();

    this.state$.next({
      ...state,
      initialStateRotors: initialStateRotors.map(rotorState =>
        getLetterIndexInAlphabet(rotorState),
      ) as RotorsStateInternalApi,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, the most important bits are:

  • encryptLetter calls goToNextRotorCombination first and then encodeLetterThroughMachine. It's what happened on the machine, every time a key was pressed, the rotors spin first and then we get the path for the new letter
  • When calling encryptMessage we also call resetCurrentStateRotorsToInitialState because that method simulates every keystrokes by splitting the string into chars and calling encryptLetter on every one of them (which make the rotors move forward on every letter)
  • resetCurrentStateRotorsToInitialState, goToNextRotorCombination and setInitialRotorConfig are updating the state in an immutable way
  • goThroughRotorsLeftToRight and goThroughRotorsRightToLeft are respectively using reduce and reduceRight to go through the rotors left to right and right to left. Using reduce* here feels "natural" as from one rotor we go through the next one by passing the previous output

We've now built an Enigma library with a public API that should let us encrypt/decrypt messages in easy way. Let's now move on to the app itself.

2 - Enigma app

The goal is now to build the following:

View of the app

We want to have:

  • An initial config where we can set the rotors where Enigma should start
  • Another display of the rotors but this time with the current state. Every time a new letter will be typed, the current state will update to show the new combination
  • The text to encrypt/decrypt on the left (input) and the output on the right

A - Display the initial config rotors and current ones

We can see that both the initial config and current state are the same so we will have a shared component containing the 3 letters.

I've decided to build that component using ngx-sub-form. If you're interested in that library you can read more on the Github project itself and in one of my previous posts here: https://dev.to/maxime1992/building-scalable-robust-and-type-safe-forms-with-angular-3nf9

rotors-form.component.ts

interface RotorsForm {
  rotors: RotorsState;
}

@Component({
  selector: 'app-rotors-form',
  templateUrl: './rotors-form.component.html',
  styleUrls: ['./rotors-form.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RotorsFormComponent extends NgxAutomaticRootFormComponent<RotorsState, RotorsForm>
  implements NgxFormWithArrayControls<RotorsForm> {
  @DataInput()
  @Input('rotors')
  public dataInput: RotorsState | null | undefined;

  @Output('rotorsUpdate')
  public dataOutput: EventEmitter<RotorsState> = new EventEmitter();

  protected emitInitialValueOnInit = false;

  protected getFormControls(): Controls<RotorsForm> {
    return {
      rotors: new FormArray([]),
    };
  }

  protected transformToFormGroup(letters: RotorsState | null): RotorsForm {
    return {
      rotors: letters ? letters : [Letter.A, Letter.A, Letter.A],
    };
  }

  protected transformFromFormGroup(formValue: RotorsForm): RotorsState | null {
    return formValue.rotors;
  }

  protected getFormGroupControlOptions(): FormGroupOptions<RotorsForm> {
    return {
      validators: [
        formGroup => {
          if (
            !formGroup.value.rotors ||
            !Array.isArray(formGroup.value.rotors) ||
            formGroup.value.rotors.length !== NB_ROTORS_REQUIRED
          ) {
            return {
              rotorsError: true,
            };
          }

          return null;
        },
      ],
    };
  }

  public createFormArrayControl(
    key: ArrayPropertyKey<RotorsForm> | undefined,
    value: ArrayPropertyValue<RotorsForm>,
  ): FormControl {
    switch (key) {
      case 'rotors':
        return new FormControl(value, [Validators.required, containsOnlyAlphabetLetters({ acceptSpace: false })]);
      default:
        return new FormControl(value);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

When using ngx-sub-form, we are able to provide data to a parent component without having it knowing anything about the form at all. In the case above we use the rotorsUpdate output. Internally, we manage everything through a formGroup. The view is also kept simple (and type safe!):

<div [formGroup]="formGroup">
  <ng-container [formArrayName]="formControlNames.rotors">
    <span *ngFor="let rotor of formGroupControls.rotors.controls; let index = index">
      <mat-form-field>
        <input matInput [placeholder]="'Rotor ' + (index + 1)" [formControl]="rotor" maxlength="1" />
      </mat-form-field>
    </span>
  </ng-container>
</div>
Enter fullscreen mode Exit fullscreen mode

Now, on the rotors-initial-config we have to retrieve the initial config from the machine and update that state when needed:

rotors-initial-config.component.ts

@Component({
  selector: 'app-rotors-initial-config',
  templateUrl: './rotors-initial-config.component.html',
  styleUrls: ['./rotors-initial-config.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RotorsInitialConfigComponent {
  constructor(private enigmaMachineService: EnigmaMachineService) {}

  public initialStateRotors$: Observable<RotorsState> = this.enigmaMachineService.initialStateRotors$;

  public rotorsUpdate(rotorsConfiguration: RotorsState): void {
    // [skipped] check that the config is valid

    this.enigmaMachineService.setInitialRotorConfig(rotorsConfiguration);
  }
}
Enter fullscreen mode Exit fullscreen mode

The view is as simple as:

<app-rotors-form
  *ngIf="(initialStateRotors$ | async) as initialStateRotors"
  [rotors]="initialStateRotors"
  (rotorsUpdate)="rotorsUpdate($event)"
></app-rotors-form>
Enter fullscreen mode Exit fullscreen mode

For the current state, even simpler. We just need to retrieve the current state from the machine.

rotors-current-state.component.ts

@Component({
  selector: 'app-rotors-current-state',
  templateUrl: './rotors-current-state.component.html',
  styleUrls: ['./rotors-current-state.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RotorsCurrentStateComponent {
  constructor(private enigmaMachineService: EnigmaMachineService) {}

  public currentStateRotors$: Observable<RotorsState> = this.enigmaMachineService.currentStateRotors$;
}
Enter fullscreen mode Exit fullscreen mode

B - Encrypt a message from the app

Now that we're able to display the rotors state, let's get started with the most important part of the app: The encryption of a message 🙌!

B1 - Logic and template

In order to keep things as minimal as possible with the examples, I've decided to remove everything from Angular Material in the following code and keep only what's important to understand the logic.

To get something that looks like the previous screenshot, we want to display for the rotors, the initial config, the current state, a text area for the text that will go through Enigma and another text area (disabled) that will show the output from Enigma.

Here's our template:

<h1>Initial config</h1>
<app-rotors-initial-config></app-rotors-initial-config>

<h1>Current state</h1>
<app-rotors-current-state></app-rotors-current-state>

<textarea [formControl]="clearTextControl"></textarea>

<div *ngIf="clearTextControl.hasError('invalidMessage')">
  Please only use a-z letters
</div>

<textarea disabled [value]="encryptedText$ | async"></textarea>
Enter fullscreen mode Exit fullscreen mode

Nothing magic or complicated in the above code but let's take a look at how we're going to implement the logic now:

@Component({
  selector: 'app-encrypt',
  templateUrl: './encrypt.component.html',
  styleUrls: ['./encrypt.component.scss'],
  providers: [...DEFAULT_ENIGMA_MACHINE_PROVIDERS],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EncryptComponent {
  private initialStateRotors$: Observable<RotorsState> = this.enigmaMachineService.initialStateRotors$;

  public clearTextControl: FormControl = new FormControl('', containsOnlyAlphabetLetters({ acceptSpace: true }));

  private readonly clearTextValue$: Observable<string> = this.clearTextControl.valueChanges;

  public encryptedText$ = combineLatest([
    this.clearTextValue$.pipe(
      sampleTime(10),
      distinctUntilChanged(),
      filter(() => this.clearTextControl.valid),
    ),
    this.initialStateRotors$,
  ]).pipe(map(([text]) => this.enigmaMachineService.encryptMessage(text)));

  constructor(private enigmaMachineService: EnigmaMachineService) {}
}
Enter fullscreen mode Exit fullscreen mode

Have you seen the line providers: [...DEFAULT_ENIGMA_MACHINE_PROVIDERS]? We'll get back to that in the next section!

First thing to notice is that apart from the injected service and the FormControl, everything is a stream. Let's take the time to break down every properties.

Bind the observable containing the initial state of the rotors:

private initialStateRotors$: Observable<RotorsState> = this.enigmaMachineService.initialStateRotors$;
Enter fullscreen mode Exit fullscreen mode

Create a FormControl to bind the value into the view and use a custom validator to make sure the letters used are valid. This will prevent us to pass invalid characters to Enigma:

public clearTextControl: FormControl = new FormControl(
  '',
  containsOnlyAlphabetLetters({ acceptSpace: true })
);
Enter fullscreen mode Exit fullscreen mode

Finally, prepare an observable representing the output of Enigma for a given message. The output can vary based on 2 things:

  • The input text
  • The initial rotor state
public encryptedText$ = combineLatest([
  this.clearTextValue$.pipe(
    sampleTime(10),
    distinctUntilChanged(),
    filter(() => this.clearTextControl.valid)
  ),
  this.initialStateRotors$
]).pipe(map(([text]) => this.enigmaMachineService.encryptMessage(text)));
Enter fullscreen mode Exit fullscreen mode

So we use the combineLatest operator to make sure that when any of the stream is updated we encrypt the message again with the new text and/or the new initial state.

B2 - Create an Enigma machine using dependency injection

I mentioned at the beginning of the article that we would use the dependency injection mechanism provided by Angular. I also mentioned in the previous part that we'd come back to the line defined on the component:

providers: [...DEFAULT_ENIGMA_MACHINE_PROVIDERS];
Enter fullscreen mode Exit fullscreen mode

Now is a good time as the app is nearly ready, the last missing piece is just to create an Enigma machine. Instead of providing the service at a module level, we provide the service at a component level so that if we want to have multiple instances to work with multiple messages at the same time, we can.

Remember what the EnigmaMachineService takes as arguments? Here a little help:

constructor(
  private enigmaRotorServices: EnigmaRotorService[],
  private reflectorService: ReflectorService
)
Enter fullscreen mode Exit fullscreen mode

In order to create an instance of the service within our EncryptComponent we could manually create a ReflectorService, manually create 3 EnigmaRotorService and manually create an EnigmaMachineService by providing as argument what we just created. Let's take a look how that'd look:

const reflectorService: ReflectorService = new ReflectorService();

const enigmaRotorService1: EnigmaRotorService = new EnigmaRotorService();
const enigmaRotorService2: EnigmaRotorService = new EnigmaRotorService();
const enigmaRotorService3: EnigmaRotorService = new EnigmaRotorService();

const enigmaMachineService: EnigmaMachineService = new EnigmaMachineService(
  [enigmaRotorService1, enigmaRotorService2, enigmaRotorService3],
  reflectorService,
);
Enter fullscreen mode Exit fullscreen mode

But...

  • Should that responsibility belong to the EncryptComponent?
  • How would we be able to later test the EncryptComponent with mocked data for example?
  • What if we want to be able to customize the rotors and reflector on a component basis?
  • What if we want to be able to add or remove rotors on a component basis?

All the above would be really hard to achieve. If we use dependency injection on the other hand, it'd be quite simple. The idea being: Let someone else be in charge of creating those services while still being able to customize how we create them at the providers level.

So all we want in the end is to just ask Angular to give us an instance of EnigmaMachineService through dependency injection:

export class EncryptComponent {
  // ...
  constructor(private enigmaMachineService: EnigmaMachineService) {}
  // ...
}
Enter fullscreen mode Exit fullscreen mode

But hold on. How can that even work? Our EnigmaMachineService is a simple class and we do not have a @Injectable() decorator. So we can't just specify the service into the provider array and inject it through the constructor as we'd usually do. Angular DI system got us covered 👌.

Let's take a closer look at the following line:

providers: [...DEFAULT_ENIGMA_MACHINE_PROVIDERS];
Enter fullscreen mode Exit fullscreen mode

Here's the DEFAULT_ENIGMA_MACHINE_PROVIDERS constant:

export const ROTORS: InjectionToken<EnigmaRotorService[]> = new InjectionToken<
  EnigmaRotorService[]
>('EnigmaRotorServices');

export const getReflectorService = (reflector: string) => {
  return () => new ReflectorService(reflector);
};

export const getRotorService = (rotor: string) => {
  return () => new EnigmaRotorService(rotor);
};

export const getEnigmaMachineService = (
  rotorServices: EnigmaRotorService[],
  reflectorService: ReflectorService
) => {
  return new EnigmaMachineService(rotorServices, reflectorService);
};

export const DEFAULT_ENIGMA_MACHINE_PROVIDERS: (
  | Provider
  | FactoryProvider)[] = [
  {
    provide: ROTORS,
    multi: true,
    useFactory: getRotorService((`ekmflgdqvzntowyhxuspaibrcj`)
  },
  {
    provide: ROTORS,
    multi: true,
    useFactory: getRotorService(`ajdksiruxblhwtmcqgznpyfvoe`)
  },
  {
    provide: ROTORS,
    multi: true,
    useFactory: getRotorService(`fvpjiaoyedrzxwgctkuqsbnmhl`)
  },
  {
    provide: ReflectorService,
    useFactory: getReflectorService('yruhqsldpxngokmiebfzcwvjat')
  },
  {
    provide: EnigmaMachineService,
    deps: [ROTORS, ReflectorService],
    useFactory: getEnigmaMachineService
  }
];
Enter fullscreen mode Exit fullscreen mode

It's a lot to take in 😱! Once again, let's break it down, piece by piece.

The first thing we want to do is create an injection token that will represent the array of rotors we want to use:

export const ROTORS: InjectionToken<EnigmaRotorService[]> = new InjectionToken<EnigmaRotorService[]>(
  'EnigmaRotorServices',
);
Enter fullscreen mode Exit fullscreen mode

Then, we create functions that will be used as factories. Which means that they will be used to create instances (in that case, instances of classes):

export const getReflectorService = (reflector: string) => {
  return () => new ReflectorService(reflector);
};

export const getRotorService = (rotor: string) => {
  return () => new EnigmaRotorService(rotor);
};

export const getEnigmaMachineService = (rotorServices: EnigmaRotorService[], reflectorService: ReflectorService) => {
  return new EnigmaMachineService(rotorServices, reflectorService);
};
Enter fullscreen mode Exit fullscreen mode

The reason we will need factories is because all the classes we will be creating require arguments and because we're not using the @Injectable decorator on those classes. So Angular cannot instantiate them magically for us, we need to do it ourselves.

After that, we create an array that will be used by the providers property of the component and it'll contain the services. Let's start with the creation of the 3 rotors:

[
  {
    provide: ROTORS,
    multi: true,
    useFactory: getRotorService((`ekmflgdqvzntowyhxuspaibrcj`)
  },
  {
    provide: ROTORS,
    multi: true,
    useFactory: getRotorService(`ajdksiruxblhwtmcqgznpyfvoe`)
  },
  {
    provide: ROTORS,
    multi: true,
    useFactory: getRotorService(`fvpjiaoyedrzxwgctkuqsbnmhl`)
  },
  // ...
]
Enter fullscreen mode Exit fullscreen mode

With Angular DI system, we can either pass a service decorated with the @Injectable decorator or pass an object to be more specific. You can learn more about Angular's DI system here: https://angular.io/guide/dependency-injection

The interesting part in that case is that we're using the multi and useFactory properties. The above code says: "Register in the ROTORS token array every rotor I will give you". Instead of having ROTORS as a single value, thanks to the multi: true property it will now be an array. Then, we use the factory we've defined earlier by passing as a parameter the rotor configuration.

Then we've got the ReflectorService with nothing particular on that one:

[
  // ...
  {
    provide: ReflectorService,
    useFactory: getReflectorService('yruhqsldpxngokmiebfzcwvjat'),
  },
  // ...
];
Enter fullscreen mode Exit fullscreen mode

And finally, the EnigmaMachineService that will pass to the factory some arguments: The freshly created rotors and the reflector:

  // ...
  {
    provide: EnigmaMachineService,
    deps: [ROTORS, ReflectorService],
    useFactory: getEnigmaMachineService
  },
  // ...
Enter fullscreen mode Exit fullscreen mode

With the deps property, we let Angular know that when calling the getEnigmaMachineService it will have to provide those dependencies.

Last but not least, I want to get your attention on the fact that the factories are returning a function in charge of creating the class and not directly an instance of the class. Why? Because it'll leverage the fact that a service needs to be created only when it's required, not before. Example: Defining a service in the providers array of a module won't create the service. The service will only be instantiated once a component or another service requires it.

Conclusion

Within this blog post we've seen one possible implementation with TypeScript of a real machine used during WW2 to send secret messages. We've also seen how it's possible to properly consume a non-angular library into our Angular app thanks to the dependency injection mechanism provided by Angular.

I've had a lot of fun building the Enigma library and the Angular app and I hope had some too while reading this blog post! 😄

I'd be delighted to see another implementation of Enigma so if you manage to build your own version let me know in the comments section 👇.

Next and final article of the series will be about cracking an encrypted message from Enigma without knowing the initial rotors position FROM THE BROWSER.

Stay tuned and thanks for reading!

Found a typo?

If you've found a typo, a sentence that could be improved or anything else that should be updated on this blog post, you can access it through a git repository and make a pull request. Instead of posting a comment, please go directly to https://github.com/maxime1992/my-dev.to and open a new pull request with your changes. If you're interested how I manage my dev.to posts through git and CI, read more here.

Follow me

           
Dev Github Twitter Reddit Linkedin Stackoverflow

Top comments (0)