Implement Enigma encryption/decryption.

This commit is contained in:
Smaug123
2016-01-11 23:04:28 +00:00
parent bb27d3ba81
commit 448f307184
6 changed files with 378 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ The Solitaire cipher is included for completeness, though it is perhaps not stri
* [Portas]
* [Hill]
* [Playfair]
* [Enigma (M3 Army)][Enigma]
* [Solitaire]
## Gotchas
@@ -284,6 +285,65 @@ and then padding 'X's were introduced to ensure no digraph was a double letter.
Finally, an 'X' was appended to the string, to ensure that the string was not of odd
length.
### Enigma
The variant of Enigma implemented is the M3 Army version.
This has five possible rotors, of which three are chosen in some distinct order.
The plugboard may be specified either as a `Array{Tuple{Char, Char}}` or a string.
For example, both the following plugboards have the same effect:
```julia
"ABCDEF"
[('A', 'B'), ('C', 'D'), ('E', 'F')]
```
For no plugboard, use `Tuple{Char, Char}[]` or `""`.
The rotor order may be specified as `[5, 1, 2]` indicating that the leftmost rotor should be rotor 5, the middle should be rotor 1, and the rightmost should be rotor 2.
That is, when a letter goes into Enigma, it passes first through rotor 2, then rotor 1, then rotor 5.
(That is, letters move through the machine from right to left, before being reflected.)
The ring settings may be specified as a three-character string.
For example, `"AAA"` indicates no adjustment to the rings.
TODO: expand this.
The initial key may be specified as a three-character string.
For example, `"AQY"` indicates that the leftmost rotor should start at position `'A'`, the middle rotor at position `'Q'`, and the rightmost at position `'Y'`.
Three reflectors are given; they may be specified with `reflector_id='A'` or `'B'` or `'C'`.
Alternatively, specify `reflector_id="YRUHQSLDPXNGOKMIEBFZCWVJAT"` to use a custom reflector; this particular example happens to be reflector `'B'`, so is equivalent to `reflector_id='B'`.
For example, the following encrypts `"AAA"` with rotors 1, 2, 3, with key `"ABC"`, an empty plugboard, the default `'B'` reflector, and ring `"AAA"`:
```julia
encrypt_enigma("AAA", [1,2,3], "ABC")
# outputs "CXT"
```
This is synonymous with:
```julia
encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id='B', stecker="")
```
And also with:
```julia
encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id="YRUHQSLDPXNGOKMIEBFZCWVJAT", stecker="")
```
And also with:
```julia
encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id='B', stecker=Tuple{Char, Char}[])
```
The arguments to `decrypt_enigma` are identical.
(In fact, `decrypt_enigma` and `encrypt_enigma` are essentially the same function, because Enigma is reversible.)
As ever, `encrypt_enigma` uppercases its input, and `decrypt_enigma` lowercases it.
### Solitaire cipher
Encrypt the text "Hello, World!" with the Solitaire cipher, key "crypto":
@@ -308,3 +368,4 @@ decrypt_solitaire("EXKYI ZSGEH UNTIQ", collect(1:54))
[Portas]: http://practicalcryptography.com/ciphers/porta-cipher/
[Hill]: https://en.wikipedia.org/wiki/Hill_cipher
[Playfair]: https://en.wikipedia.org/wiki/Playfair_cipher
[Enigma]: https://en.wikipedia.org/wiki/Enigma_machine

View File

@@ -11,6 +11,7 @@ include("portas.jl")
include("affine.jl")
include("hill.jl")
include("playfair.jl")
include("enigma.jl")
export encrypt_monoalphabetic, decrypt_monoalphabetic, crack_monoalphabetic,
encrypt_caesar, decrypt_caesar, crack_caesar,
@@ -20,6 +21,7 @@ export encrypt_monoalphabetic, decrypt_monoalphabetic, crack_monoalphabetic,
encrypt_solitaire, decrypt_solitaire,
encrypt_hill, decrypt_hill,
encrypt_playfair, decrypt_playfair,
encrypt_enigma, decrypt_enigma,
string_fitness, index_of_coincidence
end # module

View File

@@ -21,7 +21,15 @@ function rotateLeft(arr, n)
ans
end
flatten{T}(a::Array{T,1}) = any(map(x->isa(x,Array),a))? flatten(vcat(map(flatten,a)...)): a
function rotateLeftStr(st::AbstractString, n)
join(rotateLeft(split(st, ""), n), "")
end
function rotateRightStr(st::AbstractString, n)
join(rotateRight(split(st, ""), n), "")
end
flatten{T}(a::Array{T,1}) = any(x->isa(x,Array),a)? flatten(vcat(map(flatten,a)...)): a
flatten{T}(a::Array{T}) = reshape(a,prod(size(a)))
flatten(a)=a

240
src/enigma.jl Normal file
View File

@@ -0,0 +1,240 @@
import Base.uppercase
function uppercase(a::Tuple{Char, Char})
(uppercase(a[1]), uppercase(a[2]))
end
function parse_stecker(stecker::AbstractString)
if length(stecker) % 2 != 0
error("Stecker setting must be of even length.")
end
if stecker == ""
steck_parsed = Tuple{Char, Char}[]
else
sp = split(stecker, "")
steck_parsed = [(sp[i][1], sp[i+1][1]) for i in 1:2:length(sp)]
end
steck_parsed
end
function parse_stecker(stecker::Array{Tuple{Char, Char}})
if stecker == []
return Array{Tuple{Char, Char}, 1}()
else
return stecker
end
end
function parse_reflector(reflector::Char)
if uppercase(reflector) == 'A'
return "EJMZALYXVBWFCRQUONTSPIKHGD"
elseif uppercase(reflector) == 'B'
return "YRUHQSLDPXNGOKMIEBFZCWVJAT"
elseif uppercase(reflector) == 'C'
return "FVPJIAOYEDRZXWGCTKUQSBNMHL"
else
error("Reflector $(reflector) unrecognised.")
end
end
function parse_reflector(reflector::AbstractString)
if length(reflector) != 26
error("Reflector must be a 26-char string.")
end
ans = uppercase(reflector)
if ans != join(unique(ans), "")
error("Reflector must not contain any character used more than once.")
end
ans
end
"""
Encrypts the given plaintext according to the Enigma (M3, army version).
Arguments are in the order: plaintext, stecker, rotors, ring, key.
Plaintext is a string; punctuation is stripped out and it is made lowercase.
Rotors is an array - for example, [1,2,3] - being the order of the rotors.
Each entry should be a distinct integer between 1 and 5 inclusive.
Key is a string of three letters, indicating the starting positions of the rotors.
Optional:
reflector_id='B', which sets whether to use reflector A, B or C.
Can also be specified as a 26-char string.
Stecker is either an array - for example, [('A','B'), ('D', 'E')] specifying
that A, B are swapped and D, E are swapped - or a string ("ABDE" accomplishing
the same thing). No letter may appear more than once.
Ring is a string - for example, "AAA" - being the offset applied to each rotor.
"AAA", for example, signifies no offset. The string must be three letters.
"""
function encrypt_enigma{I <: Integer}(plaintext,
rotors::Array{I, 1}, key::AbstractString;
reflector_id='B', ring::AbstractString = "AAA",
stecker = Tuple{Char, Char}[])
parsed_stecker = parse_stecker(stecker)
# validate stecker settings
if flatten(parsed_stecker) != unique(flatten(parsed_stecker))
error("No letter may appear more than once in stecker settings.")
end
parsed_stecker = map(uppercase, parsed_stecker)
# validate ring settings
if length(ring) != 3
error("Ring settings must be a string of length 3.")
end
ring = uppercase(ring)
for ch in ring
if !('A' <= ch <= 'Z')
error("Ring settings must be a string of Roman letters.")
end
end
# validate key settings
if length(key) != 3
error("Key settings must be a string of length 3.")
end
key = uppercase(key)
for ch in key
if !('A' <= ch <= 'Z')
error("Key settings must be a string of Roman letters.")
end
end
# validate rotor settings
for i in rotors
if !(1 <= i <= 5)
error("Each rotor must be an integer between 1 and 5.")
end
end
if rotors != unique(rotors)
error("No rotor may appear more than once.")
end
# validate reflector settings
reflector = parse_reflector(reflector_id)
# sanitise plaintext
plaintext = uppercase(letters_only(plaintext))
# initialisation of the machine
rotor_layouts = ["EKMFLGDQVZNTOWYHXUSPAIBRCJ",
"AJDKSIRUXBLHWTMCQGZNPYFVOE",
"BDFHJLCPRTXVZNYEIWGAKMUSQO",
"ESOVPZJAYQUIRHXLNFTGKDCMWB",
"VZBRGITYUPSDNHLXAWMJQOFECK"]
notches = [17,5,22,10,26]
rotor1 = rotor_layouts[rotors[1]]
notch1 = notches[rotors[1]]
rotor2 = rotor_layouts[rotors[2]]
notch2 = notches[rotors[2]]
rotor3 = rotor_layouts[rotors[3]]
notch3 = notches[rotors[3]]
# apply the key as part of initialisation; incorporates ring
key_offsets = [26+Int(ch)-65 for ch in key]
notch1 = (key_offsets[1]*26+notch1-key_offsets[1]) % 26
notch2 = (key_offsets[2]*26+notch2-key_offsets[2]) % 26
notch3 = (key_offsets[3]*26+notch3-key_offsets[3]) % 26
key_offsets = key_offsets .- [Int(ring[i])-65 for i in 1:3]
# We receive a character; the rotors increment; then:
# the character goes through the plugboard
# the character then goes through rotor3, then rotor2, then rotor1
# then the reflector, then the inverse of rotor 1, 2, 3
# finally the plugboard again
plugboard_dict = Dict([parsed_stecker; map(reverse, parsed_stecker)])
ans = IOBuffer()
rotor3movements = key_offsets[3]
rotor2movements = key_offsets[2]
rotor1movements = key_offsets[1]
for i in 1:length(plaintext)
working_ch = plaintext[i]
# rotate rotors
notch3 -= 1
rotor3movements += 1
if notch3 == 0
notch3 = 26
rotor2movements += 1
notch2 -= 1
if notch2 == 0
notch2 = 26
rotor1movements += 1
notch1 -= 1
if notch1 == 0
notch1 = 26
end
end
end
# double step of rotor
if notch3 == 25 && notch2 == 1
notch2 = 26
rotor2movements += 1
rotor1movements += 1
notch1 -= 1
if notch1 == 0
notch1 = 26
end
end
# plugboard
working_ch = encrypt_monoalphabetic(working_ch, plugboard_dict)[1]
# rotors
# comes in as…
working_ch = Char(65+((rotor3movements+Int(working_ch)-65) % 26))
working_ch = encrypt_monoalphabetic(working_ch, rotor3)[1]
# comes in as…
working_ch = Char(65+(((26*rotor3movements)-rotor3movements+rotor2movements+Int(working_ch)-65) % 26))
working_ch = encrypt_monoalphabetic(working_ch, rotor2)[1]
# comes in as…
working_ch = Char((((26*rotor2movements) + Int(working_ch)-65 - rotor2movements + rotor1movements) % 26) + 65)
working_ch = encrypt_monoalphabetic(working_ch, rotor1)[1]
# reflector
# comes in as…
working_ch = Char((26*rotor1movements + Int(working_ch) - 65 - rotor1movements) % 26 + 65)
working_ch = encrypt_monoalphabetic(working_ch, reflector)[1]
# rotors
# comes in as…
working_ch = Char((Int(working_ch)-65+rotor1movements) % 26 + 65)
working_ch = uppercase(decrypt_monoalphabetic(working_ch, rotor1))[1]
working_ch = Char(65+((rotor1movements*26 + rotor2movements - rotor1movements +Int(working_ch)-65) % 26))
working_ch = uppercase(decrypt_monoalphabetic(working_ch, rotor2))[1]
working_ch = Char(65+((26*rotor2movements + rotor3movements-rotor2movements+Int(working_ch)-65) % 26))
working_ch = uppercase(decrypt_monoalphabetic(working_ch, rotor3))[1]
# plugboard
# comes in as…
working_ch = Char(65+(((26*rotor3movements)-rotor3movements+Int(working_ch)-65) % 26))
working_ch = encrypt_monoalphabetic(working_ch, plugboard_dict)[1]
print(ans, working_ch)
end
uppercase(takebuf_string(ans))
end
function decrypt_enigma(args1...; args2...)
lowercase(encrypt_enigma(args1...; args2...))
end

64
test/enigma.jl Normal file
View File

@@ -0,0 +1,64 @@
using ClassicalCiphers
using Base.Test
@test (encrypt_enigma("AAA", [1,2,3], "AAA") == "BDZ")
@test (decrypt_enigma("BDZ", [1,2,3], "AAA") == "aaa")
@test (encrypt_enigma("AAA", [1,2,3], "ABC") == "CXT")
@test (decrypt_enigma("CXT", [1,2,3], "ABC") == "aaa")
@test (encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id='B', stecker="") == "CXT")
@test (decrypt_enigma("CXT", [1,2,3], "ABC", ring="AAA", reflector_id='B', stecker="") == "aaa")
@test (encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id="YRUHQSLDPXNGOKMIEBFZCWVJAT", stecker="") == "CXT")
@test (decrypt_enigma("CXT", [1,2,3], "ABC", ring="AAA", reflector_id="YRUHQSLDPXNGOKMIEBFZCWVJAT", stecker="") == "aaa")
@test (encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id='B', stecker=Tuple{Char, Char}[]) == "CXT")
@test (decrypt_enigma("CXT", [1,2,3], "ABC", ring="AAA", reflector_id='B', stecker=Tuple{Char, Char}[]) == "aaa")
@test encrypt_enigma("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", [1,2,3], "AAA") == "BDZGOWCXLTKSBTMCDLPBMUQOFXYHCXTGYJFL"
@test decrypt_enigma("BDZGOWCXLTKSBTMCDLPBMUQOFXYHCXTGYJFL", [1,2,3], "AAA") == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
@test (stecker = [('P','O'), ('M','L'), ('I','U'), ('K','J'), ('N','H'), ('Y','T'), ('G','B'), ('V','F'), ('R','E'), ('D','C')];
encrypt_enigma("ABCDEF", [1,2,3], "AAA", stecker=stecker) == "GWKGRC")
@test (stecker = [('P','O'), ('M','L'), ('I','U'), ('K','J'), ('N','H'), ('Y','T'), ('G','B'), ('V','F'), ('R','E'), ('D','C')];
decrypt_enigma("GWKGRC", [1,2,3], "AAA", stecker=stecker) == "abcdef")
@test (encrypt_enigma("there are fifteen possible answers to this question",
[1,2,3],
"UQI") == "SPPBXWOFVOEAKDRFKLDOLYHKSNTFBPERQFZCDTRAKXCE")
@test (decrypt_enigma("SPPBXWOFVOEAKDRFKLDOLYHKSNTFBPERQFZCDTRAKXCE",
[1,2,3],
"UQI") == "therearefifteenpossibleanswerstothisquestion")
@test (encrypt_enigma("AAAAAAAA", [1,2,3], "UQI") == "OWLDUWEL")
@test (decrypt_enigma("OWLDUWEL", [1,2,3], "UQI") == "aaaaaaaa")
@test (encrypt_enigma("AAAAAAAA", [1,2,3], "UQI", stecker="") == "OWLDUWEL")
@test (decrypt_enigma("OWLDUWEL", [1,2,3], "UQI", stecker="") == "aaaaaaaa")
@test (encrypt_enigma("AAAAAAAA", [1,2,3], "AAA", ring="AAB") == "UBDZGOWC")
@test (decrypt_enigma("UBDZGOWC", [1,2,3], "AAA", ring="AAB") == "aaaaaaaa")
@test (encrypt_enigma("AAAA", [1,2,3], "UQI", ring="EFG") == "YHKD")
@test (decrypt_enigma("YHKD", [1,2,3], "UQI", ring="EFG") == "aaaa")
@test (encrypt_enigma("when shall we three meet again", [1,2,3], "yev", ring="aaa", stecker="POMLIUKJNHYTGBVFREDC") == "PDTTELKJEYFAQZCHRWVREXFFK")
@test (encrypt_enigma("when shall we three meet again", [5,1,4], "yev", ring="aaa", stecker="POMLIUKJNHYTGBVFREDC") == "TSJKKEKWKLFPOCKAXUVSDWVOW")
@test (encrypt_enigma("WHENSHALLWETHREEMEETAGAIN", [5, 1, 4], "yev", ring="aac", stecker="POMLIUKJNHYTGBVFREDC") == "AVYFURCOELTCLFMZGABKOWDKH")
@test (encrypt_enigma("WHENSHALLWETHREEMEETAGAIN", [5, 1, 4], "yev", ring="aab", stecker="POMLIUKJNHYTGBVFREDC") == "OIXYQZDEWUHADNZKXBUGZZNQW")
@test (encrypt_enigma("WHENSHALLWETHREEMEETAGAIN", [5, 1, 4], "yev", ring="nqz", stecker="POMLIUKJNHYTGBVFREDC") == "GDCTXXBEIGWEMLLUDNUXZJDKU")
@test (encrypt_enigma("AAA", [1,2,3], "QEV") == "LNP")
@test (plain = "WHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAIN";
ans = "TSJKKEKWKLFPOCKAXUVSDWVOWSSDCKBGJDNBJYWZPOFKMSMQKMFQVZKGHFWHWEMKXLELAXRRNHWLCBXAQWUEPVEVXWGPMDVNXLHLBGLALOYQKLMKOJBWWOMUGXNPCZRLQDMWRCDNNENKNGGYBYOUMQNFKJPMQANZLRAJIGDABYMWGYWQZVILLMSQAPQGPZLHXKLGMZEMJJYJGAYCTHRVAKRJNXWHFLOARTZICGUODQTNPBMTGXOHIUBIDXNVBZVWSGSHVVGFHKRUWQCKMLKZEUAZDHUUFPPKKOFIDSIPNGVOLYFCWZYVKPGJPTHLVJPUNRNUTUTKYGZYHCEZIOAMCCUJNEPWKGZNMOBMCKHVWJLZGFZHUHTPBLGTVDIKKDVUVCWWOYAXNQWUROBHHYWSHSPUULKPUVAOLTVZWNRVOEEREEXHYOVKRBZIKCWKPVYUATVPDZUNJPMNDXIKQHPVNBDWEERCBBSABVEQOMGEPJWIOMVEVLKZXORJCYYDELDYKKJIZSBVNTTJEYPNSYKMAJKZIFVDQPHJOBHTTDZBLZMLTPTLHDPKWHSWAGVIEUADKUFRSFYSAOUGIZWWYZPXEOQJXXXFXSQZGDBEGNRDIPVXNVVGWMEFMHYVHMHURIVNINPRQWYEDG";
encrypt_enigma(plain, [5, 1, 4], "yev", ring="aaa", stecker="POMLIUKJNHYTGBVFREDC") == ans)
@test (plain = "WHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAINWHENSHALLWETHREEMEETAGAIN";
ans = "UGVATRLVMDBJBTUUVZOJZRICCJPJVKLZTHIMVRDRTWTSGWLWGBLPRDITLGEUWIJQCDQKZXFTCYYNJSBMLBPWQSNWJUFJKHSDRDEQCZHVXLECUNQENXFCLJJHYXULFHZQLYQPGBBHDQLUJKUXFPHGVKSPRDWFGXJYXFGCVHNODSZRSSHQTLUXTHJEZAXKYFRYWWNEJQDJUFFPASFEHOGETXSVVIWKKITUWALMTWCPGZLKOSJYDFUJHUYCSHMFBPMKVYRNQDEBQFQTFJKYIBTNLMQAQLDAMLAIJKGVGVWLBQOCPOQJXQYYUCXVEXPIHOGMDFUYCJCOIKDZIHHAJYAKHXRBHYANVKMQSXVYCTAKUNPPHSTKPEDFQUXAQSQZNLXWDNXLJTFLBWBQNZMKLUJKNUEARFIXAFLMVNRZMMGEHGFLZONEJQMUCXYZBJLNJSZTZMPAVVIJJMDQOJRXYOAFHINLKNFPASLEPKUHRMKXTNOBLSPXMPQSISGJLXBEXSOYCOVLESCHDASAWKKMWDXGNMTJBLWBKGMGXEDCJMICXKIBCBKUOQNPRUGTIUVODQVPKRGWWQWTYLIPCRSNUBHJWQBXCGNCYJRNGNCYZIMXHNHNZPTSEFRYQPAGJJOTXVMMKQLSMGBJNW";
encrypt_enigma(plain, [5, 1, 4], "yev", ring="ezk", stecker="POMLIUKJNHYTGBVFREDC") == ans)

View File

@@ -1,7 +1,8 @@
using ClassicalCiphers
tests = ["vigenere", "monoalphabetic", "solitaire",
"caesar", "portas", "affine", "hill", "playfair"]
"caesar", "portas", "affine", "hill", "playfair",
"enigma"]
println("Running tests:")