Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
237 changes: 127 additions & 110 deletions lib/enigma.ex
Original file line number Diff line number Diff line change
@@ -1,170 +1,187 @@
# Elixir Enigma Machine Simulator
# August 2016

# Mike Boone
# https://github.com/boone
# https://twitter.com/boonedocks

# Trying to learn Elixir by modeling an Enigma machine, inspired by the Ruby
# code written by @albert_still in:
# http://red-badger.com/blog/2015/02/23/understanding-the-enigma-machine-with-30-lines-of-ruby-star-of-the-2014-film-the-imitation-game

# Encrypt/decrypt a message with:
# Enigma.process_string("SECRETMESSAGE")

# To decrypt an encrypted message, supply the encrypted message and the
# matching plugboard, rotor, and reflector configurations:
# Enigma.process_string("LDINKZRVIDGPO", 'YXSDPFLHVQKGOUMEJRCTNIZBAW',
# 'FKDRHSXGVYNBLZIWMEJQOACUTP', 'XTQFWNBCKYVSZODMJIHPGERAUL',
# 'MZEVUBYCLKHOSIWQNADGFTRPXJ', 'OQFGUCDPZKJVXWAHBTYRELNMSI')

defmodule Enigma do
def rotor do
# Return a randomized rotor: a random list of chars from A-Z.
@moduledoc """
Elixir Enigma Machine Simulator
August 2016

Mike Boone
https://github.com/boone
https://twitter.com/boonedocks

Trying to learn Elixir by modeling an Enigma machine, inspired by the Ruby
code written by @albert_still in:
http://red-badger.com/blog/2015/02/23/understanding-the-enigma-machine-with-30-lines-of-ruby-star-of-the-2014-film-the-imitation-game

Encrypt/decrypt a message with:
Enigma.process_string("SECRETMESSAGE")

To decrypt an encrypted message, supply the encrypted message and the
matching plugboard, rotor, and reflector configurations:
Enigma.process_string("LDINKZRVIDGPO", 'YXSDPFLHVQKGOUMEJRCTNIZBAW',
'FKDRHSXGVYNBLZIWMEJQOACUTP', 'XTQFWNBCKYVSZODMJIHPGERAUL',
'MZEVUBYCLKHOSIWQNADGFTRPXJ', 'OQFGUCDPZKJVXWAHBTYRELNMSI')
"""
@ascii_offset 65

@doc """
Return a randomized rotor: a random list of chars from A-Z.
"""
def random_rotor do
Enum.take_random(?A..?Z, 26)
end

def reflector do
# Get a random A-Z character list and break it into pairs.
# For each pair, the first letter will map to the second and vice versa.
# Create a character list similar to the rotors which represents this
# reflected pair relationship.
random_pairs = Enum.chunk(Enum.take_random(?A..?Z, 26), 2)
@doc """
Get a random A-Z character list and break it into pairs.
For each pair, the first letter will map to the second and vice versa.
Create a character list similar to the rotors which represents this
reflected pair relationship.
"""
def random_reflector do
random_pairs = Enum.chunk_every(Enum.take_random(?A..?Z, 26), 2)

# Start with a blank list with 26 empty slots, which we need to fill with
# the pairs.
reflector = List.duplicate(nil, 26)

List.duplicate(nil, 26)
# Fill in the blank list with the pairs.
reflector_iterate(random_pairs, reflector)
|> reflector_iterate(random_pairs)
end

def plugboard do
# The plugboard is like a reflector, but only 10 letters are swapped.
# The remaining letters map to themselves.
random_pairs = Enum.chunk(Enum.take_random(?A..?Z, 26), 2)

# Keep 10 pairs, throw away 6
random_pairs = Enum.take(random_pairs, 10)
@doc """
The plugboard is like a reflector, but only 10 letters are swapped.
The remaining letters map to themselves.
"""
def random_plugboard do
random_pairs =
Enum.chunk_every(Enum.take_random(?A..?Z, 26), 2)
# Keep 10 pairs, throw away 6
|> Enum.take(10)

# Start with an A-Z list.
plugboard = Enum.to_list(?A..?Z)

Enum.to_list(?A..?Z)
# Overwrite list with the pairs, leaving 6 letters unchanged.
reflector_iterate(random_pairs, plugboard)
|> reflector_iterate(random_pairs)
end

def process_string(str, plugboard \\ plugboard(), rotor1 \\ rotor(),
rotor2 \\ rotor(), rotor3 \\ rotor(), reflector \\ reflector()) do

def process_string(
input_str,
plugboard \\ random_plugboard(),
rotor1 \\ random_rotor(),
rotor2 \\ random_rotor(),
rotor3 \\ random_rotor(),
reflector \\ random_reflector()
) do
# We accept any string as input, but we really want a charlist of only
# A-Z characters, no spacing or punctuation.
str = str
|> String.upcase
|> to_charlist
|> Enum.reject(fn(x) -> not(x in ?A..?Z) end)
input_str =
input_str
|> String.upcase()
|> to_charlist
|> Enum.reject(fn x -> not (x in ?A..?Z) end)

# Output the configuration of the Enigma machine.
IO.puts "Plugboard: #{plugboard}"
IO.puts "Rotor 1: #{rotor1}"
IO.puts "Rotor 2: #{rotor2}"
IO.puts "Rotor 3: #{rotor3}"
IO.puts "Reflector: #{reflector}"
IO.puts("Plugboard: #{plugboard}")
IO.puts("Rotor 1: #{rotor1}")
IO.puts("Rotor 2: #{rotor2}")
IO.puts("Rotor 3: #{rotor3}")
IO.puts("Reflector: #{reflector}")

# Process the message!
result = iterate(str, plugboard, rotor1, rotor2, rotor3, reflector, 0, [])
result = iterate(input_str, plugboard, rotor1, rotor2, rotor3, reflector)

IO.puts "#{str} was translated to #{result}"
IO.puts("#{input_str} was translated to #{result}")

to_string(result)
end

defp iterate(
message,
plugboard,
rotor1,
rotor2,
rotor3,
reflector,
count \\ 0,
newlist \\ []
)

defp iterate([head | tail], plugboard, rotor1, rotor2, rotor3, reflector, count, newlist) do
# Spin Rotor 1
rotor1 = tick_rotor(rotor1)

rotor1 = spin_rotor(rotor1)
# Spin Rotor 2 if Rotor 1 has gone all the way around.
rotor2 = case rem(count, 25) do
0 -> tick_rotor(rotor2)
_ -> rotor2
end

rotor2 = rotor2 |> spin_rotor(count, 25)
# Spin Rotor 3 if Rotor 2 has gone all the way around.
rotor3 = case rem(count, 25 * 25) do
0 -> tick_rotor(rotor3)
_ -> rotor3
end

# Send the character through the plugboard.
head = list_value(plugboard, head)

# Send the character through each rotor.
head = list_value(rotor1, head)
head = list_value(rotor2, head)
head = list_value(rotor3, head)

# Send the character through the reflector.
head = list_value(reflector, head)

# Send the character back through the rotors in reverse.
head = inverted_list_value(rotor3, head)
head = inverted_list_value(rotor2, head)
head = inverted_list_value(rotor1, head)

# Send the character back through the plugboard in reverse.
head = inverted_list_value(plugboard, head)
rotor3 = rotor3 |> spin_rotor(count, 25 * 25)

translated_char =
head
|> send_through(plugboard)
|> send_through(rotor1)
|> send_through(rotor2)
|> send_through(rotor3)
|> send_through(reflector)
|> send_back_through(rotor3)
|> send_back_through(rotor2)
|> send_back_through(rotor1)
|> send_back_through(plugboard)

# Append the character to our message.
newlist = List.insert_at(newlist, -1, head)

# Track the iteration count.
count = count + 1
newlist = newlist ++ [translated_char]

# Recurse with the remaining message.
iterate(tail, plugboard, rotor1, rotor2, rotor3, reflector, count, newlist)
iterate(tail, plugboard, rotor1, rotor2, rotor3, reflector, count + 1, newlist)
end

defp iterate([], _, _, _, _, _, _, newlist) do
defp iterate([], _, _, _, _, _, _, full_list) do
# Recursion is complete, return the final character list.
newlist
full_list
end

# Character translations are used in both the rotors and the reflector.
# Here we store them as character lists, where A-Z map to the respective
# position in the character list. Hence we need functions that will find the
# translation for 'A' from the list, and vice versa.

# take the char and find the corresponding translated char in the list
defp list_value(list, char) do
Enum.at(list, char - 65)
@doc """
take the char and find the corresponding translated char in the list
"""
defp send_through(char, list) do
Enum.at(list, char - @ascii_offset)
end

# take the translated char and find the corresponding original char
defp inverted_list_value(list, char) do
(Enum.find_index list, fn(x) -> x == char end) + 65
@doc """
take the translated char and find the corresponding original char
"""
defp send_back_through(char, list) do
Enum.find_index(list, fn x -> x == char end) + @ascii_offset
end

defp reflector_iterate([head | tail], reflector) do
defp reflector_iterate(reflector, [head | tail]) do
# head will be a character list with two elements.

reflector
# Add the first/last relationship to the reflector.
reflector = List.replace_at(reflector, List.first(head) - 65, List.last(head))

|> List.replace_at(List.first(head) - @ascii_offset, List.last(head))
# Add the last/first "reflected" relationship to the reflector.
reflector = List.replace_at(reflector, List.last(head) - 65, List.first(head))

|> List.replace_at(List.last(head) - @ascii_offset, List.first(head))
# Recurse until complete.
reflector_iterate(tail, reflector)
|> reflector_iterate(tail)
end

defp reflector_iterate([], reflector) do
defp reflector_iterate(reflector, []) do
# Recursion is complete, return the final reflector.
reflector
end

defp tick_rotor(rotor) do
# Spin the rotor to the next position.
# ABCDEFGHIJKLMNOPQRSTUVWXYZ shifts to BCDEFGHIJKLMNOPQRSTUVWXYZA
List.insert_at(List.delete_at(rotor, 0), 25, List.first(rotor))
@doc """
Spin the rotor to the next position.
"""
defp spin_rotor([head | tail]) do
tail ++ [head]
end

defp spin_rotor(rotor, count, condition) when rem(count, condition) == 0 do
spin_rotor(rotor)
end

defp spin_rotor(rotor, _, _) do
rotor
end
end
28 changes: 6 additions & 22 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,14 @@ defmodule Enigma.Mixfile do
use Mix.Project

def project do
[app: :enigma,
version: "0.1.0",
elixir: "~> 1.3",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps()]
[
app: :enigma,
version: "0.1.0",
elixir: "~> 1.5",
deps: deps()
]
end

# Configuration for the OTP application
#
# Type "mix help compile.app" for more information
def application do
[applications: [:logger]]
end

# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# Type "mix help deps" for more examples and options
defp deps do
[]
end
Expand Down
Loading