diff --git a/README.md b/README.md index 3b109c9..e509810 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The Solitaire cipher is included for completeness, though it is perhaps not stri * [Vigenère] * [Portas] * [Hill] +* [Playfair] * [Solitaire] ## Gotchas @@ -233,6 +234,54 @@ decrypt_hill("PLHCGQWHRY", "bcfh") # outputs "helloworld" ``` +### Playfair cipher + +Encrypt the text "Hello, World!" with the Playfair cipher, key "playfair example": +```julia +encrypt_playfair("Hello, World!", "playfair example") +# outputs "DMYRANVQCRGE" +``` + +The key is converted to "PLAYFIREXM", removing duplicate letters and punctuation. +The padding character used to separate double letters, and to ensure the final +plaintext is of even length, is 'X'; the backup character is 'Z' (used for separating +consecutive 'X's). + +Encrypt the same text using an explicitly specified keysquare: + +```julia +arr = ['P' 'L' 'A' 'Y' 'F'; 'I' 'R' 'E' 'X' 'M'; 'B' 'C' 'D' 'G' 'H'; 'K' 'N' 'O' 'Q' 'S'; 'T' 'U' 'V' 'W' 'Z'] +encrypt_playfair("Hello, World!", arr) +# outputs "DMYRANVQCRGE" +``` + +Note that the keysquare must be 25 letters, in a 5x5 array. + +Optionally specify the two letters which are to be combined (default 'I','J'): + +```julia +encrypt_playfair("IJXYZA", "PLAYFIREXM", combined=('I', 'J')) +# outputs "RMRMFWYE" +encrypt_playfair("IJXYZA", "PLAYFIREXM", combined=('X', 'Z')) +# outputs "BSGXEY" +``` + +In this case, the letters are combined in the plaintext, and then treated as one throughout. + +Decrypt the same text: + +```julia +decrypt_playfair("RMRMFWYE", "playfair example") +# outputs "ixixyzax" +``` + +The decrypting function does not attempt to delete padding letters. +Note that in the above example, the text originally encrypted was "IJXYZA"; +the 'J' was transcribed as 'I', as specified by the default `combined=('I', 'J')`, +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. + ### Solitaire cipher Encrypt the text "Hello, World!" with the Solitaire cipher, key "crypto": @@ -256,3 +305,4 @@ decrypt_solitaire("EXKYI ZSGEH UNTIQ", collect(1:54)) [Solitaire]: https://en.wikipedia.org/wiki/Solitaire_(cipher) [Portas]: http://practicalcryptography.com/ciphers/porta-cipher/ [Hill]: https://en.wikipedia.org/wiki/Hill_cipher +[Playfair]: https://en.wikipedia.org/wiki/Playfair_cipher diff --git a/src/ClassicalCiphers.jl b/src/ClassicalCiphers.jl index cb79399..34e22f2 100644 --- a/src/ClassicalCiphers.jl +++ b/src/ClassicalCiphers.jl @@ -10,6 +10,7 @@ include("solitaire.jl") include("portas.jl") include("affine.jl") include("hill.jl") +include("playfair.jl") export encrypt_monoalphabetic, decrypt_monoalphabetic, crack_monoalphabetic, encrypt_caesar, decrypt_caesar, crack_caesar, @@ -18,6 +19,7 @@ export encrypt_monoalphabetic, decrypt_monoalphabetic, crack_monoalphabetic, encrypt_portas, decrypt_portas, encrypt_solitaire, decrypt_solitaire, encrypt_hill, decrypt_hill, + encrypt_playfair, decrypt_playfair, string_fitness, index_of_coincidence end # module diff --git a/src/playfair.jl b/src/playfair.jl new file mode 100644 index 0000000..69701cc --- /dev/null +++ b/src/playfair.jl @@ -0,0 +1,131 @@ +""" +Converts the given key-string to a Playfair key square. + +Parameter `replacement` is a pair, such as ('I', 'J'), containing +the two letters which are combined. Only the first of these letters will +be present in the keysquare. +""" +function playfair_key_to_square(key::AbstractString, replacement) + # make the key replacement + key = encrypt_monoalphabetic(key, Dict(replacement[2] => replacement[1])) + # delete duplicates etc from key + key_sanitised = union(uppercase(letters_only(key))) + # construct key square + remaining = collect(filter(x -> (x != replacement[2] && findfirst(key_sanitised, x) == 0), 'A':'Z')) + keysquare = transpose(reshape([key_sanitised; remaining], 5, 5)) + + keysquare +end + +function encrypt_playfair(plaintext, key::AbstractString; combined=('I','J')) + keysquare = playfair_key_to_square(key, combined) + + # make combinations in plaintext + plaintext_sanitised = encrypt_monoalphabetic(plaintext, Dict(combined[2] => combined[1])) + + encrypt_playfair(plaintext_sanitised, keysquare, combined=combined) +end + +""" +Encrypts the given plaintext according to the Playfair cipher. +Throws an error if the second entry in the `combined` tuple is present in the key. + +Optional parameters: + +stripped=false. When set to true, encrypt_playfair skips + converting the plaintext to uppercase, removing punctuation, and + combining characters which are to be combined in the key. +combined=('I', 'J'), marks the characters which are to be combined in the text. + Only the first of these two may be present in the output of encrypt_playfair. +""" +function encrypt_playfair(plaintext, key::Array{Char, 2}; stripped=false, combined=('I', 'J')) + if !stripped + if findfirst(key, combined[2]) != 0 + error("Key must not contain symbol $(combined[2]), as it was specified to be combined.") + end + plaintext_sanitised = uppercase(letters_only(plaintext)) + plaintext_sanitised = encrypt_monoalphabetic(plaintext_sanitised, Dict(combined[2] => combined[1])) + else + plaintext_sanitised = plaintext + end + + # add X's as necessary to break up double letters + if combined[2] != 'X' + padding_char = 'X' + else + padding_char = combined[1] + end + if combined[2] != 'Z' + backup_padding_char = 'Z' + else + backup_padding_char = combined[1] + end + + i = 1 + while i < length(plaintext_sanitised) + if plaintext_sanitised[i] == plaintext_sanitised[i+1] + if plaintext_sanitised[i] != padding_char + plaintext_sanitised = plaintext_sanitised[1:i] * string(padding_char) * plaintext_sanitised[i+1:end] + else + plaintext_sanitised = plaintext_sanitised[1:i] * string(backup_padding_char) * plaintext_sanitised[i+1:end] + end + end + i += 2 + end + + if length(plaintext_sanitised) % 2 == 1 + if plaintext_sanitised[end] != padding_char + plaintext_sanitised = plaintext_sanitised * string(padding_char) + else + plaintext_sanitised = plaintext_sanitised * string(backup_padding_char) + end + end + + # start encrypting! + ans = IOBuffer() + + i = 1 + while i < length(plaintext_sanitised) + l1 = plaintext_sanitised[i] + l2 = plaintext_sanitised[i+1] + + l1pos = ind2sub((5, 5), findfirst(key, l1)) + l2pos = ind2sub((5, 5), findfirst(key, l2)) + + @assert l1pos != l2pos + + if l1pos[1] == l2pos[1] + print(ans, key[l1pos[1], (((l1pos[2]+1 - 1) % 5) + 1)]) + print(ans, key[l2pos[1], (((l2pos[2]+1 - 1) % 5) + 1)]) + elseif l1pos[2] == l2pos[2] + print(ans, key[((l1pos[1]+1 - 1) % 5)+1, l1pos[2]]) + print(ans, key[((l2pos[1]+1 - 1) % 5)+1, l2pos[2]]) + else + + print(ans, key[l1pos[1], l2pos[2]]) + print(ans, key[l2pos[1], l1pos[2]]) + end + + i += 2 + end + + takebuf_string(ans) +end + + +function decrypt_playfair(ciphertext, key::AbstractString; combined=('I', 'J')) + keysquare = playfair_key_to_square(key, combined) + decrypt_playfair(ciphertext, keysquare, combined=combined) +end + +""" +Decrypts the given ciphertext according to the Playfair cipher. + +Does not attempt to delete X's inserted as padding for double letters. +""" +function decrypt_playfair(ciphertext, key::Array{Char, 2}; combined=('I', 'J')) + # to obtain the decrypting keysquare, reverse every row and every column + keysquare = mapslices(reverse, key, 2) + keysquare = transpose(mapslices(reverse, transpose(keysquare), 2)) + lowercase(encrypt_playfair(ciphertext, keysquare, combined=combined)) +end \ No newline at end of file diff --git a/test/playfair.jl b/test/playfair.jl new file mode 100644 index 0000000..c236cb7 --- /dev/null +++ b/test/playfair.jl @@ -0,0 +1,24 @@ +using ClassicalCiphers +using Base.Test + +# Wikipedia example +@test encrypt_playfair("Hide the gold in the tree stump", "playfair example") == "BMODZBXDNABEKUDMUIXMMOUVIF" + +# Simon Singh Black Chamber example +@test encrypt_playfair("meet me at hammersmith bridge tonight", "charles") == "GDDOGDRQARKYGDHDNKPRDAMSOGUPGKICQY" + +# doc examples +@test encrypt_playfair("Hello, World!", "playfair example") == "DMYRANVQCRGE" +@test (arr = ['P' 'L' 'A' 'Y' 'F'; 'I' 'R' 'E' 'X' 'M'; 'B' 'C' 'D' 'G' 'H'; 'K' 'N' 'O' 'Q' 'S'; 'T' 'U' 'V' 'W' 'Z']; + encrypt_playfair("Hello, World!", arr) == "DMYRANVQCRGE") + +@test encrypt_playfair("IJXYZA", "PLAYFIREXM", combined=('I', 'J')) == "RMRMFWYE" +@test encrypt_playfair("IJXYZA", "PLAYFIREXM", combined=('X', 'Z')) == "BSGXEY" + +@test decrypt_playfair("BSGXEY", "PLAYFIREXM", combined=('X', 'Z')) == "ijxyxa" +@test decrypt_playfair("RMRMFWYE", "PLAYFIREXM", combined=('I', 'J')) == "ixixyzax" +@test decrypt_playfair("DMYRANVQCRGE", "playfair example") == "helxloworldx" +@test (arr = ['P' 'L' 'A' 'Y' 'F'; 'I' 'R' 'E' 'X' 'M'; 'B' 'C' 'D' 'G' 'H'; 'K' 'N' 'O' 'Q' 'S'; 'T' 'U' 'V' 'W' 'Z']; + decrypt_playfair("DMYRANVQCRGE", arr) == "helxloworldx") +@test decrypt_playfair("GDDOGDRQARKYGDHDNKPRDAMSOGUPGKICQY", "charles") == "meetmeathamxmersmithbridgetonightx" +@test decrypt_playfair("BMODZBXDNABEKUDMUIXMMOUVIF", "playfair example") == "hidethegoldinthetrexestump" \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 01f5b60..3fd7540 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,7 @@ using ClassicalCiphers tests = ["vigenere", "monoalphabetic", "solitaire", - "caesar", "portas", "affine", "hill"] + "caesar", "portas", "affine", "hill", "playfair"] println("Running tests:")