Merge pull request #7 from Smaug123/pull-request/8fb21fd0

Add Playfair encryption/decryption
This commit is contained in:
Smaug123
2016-01-08 20:14:47 +00:00
5 changed files with 208 additions and 1 deletions

View File

@@ -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

View File

@@ -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

131
src/playfair.jl Normal file
View File

@@ -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

24
test/playfair.jl Normal file
View File

@@ -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"

View File

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