Add Hill cipher encryption/decryption

This commit is contained in:
Smaug123
2016-01-07 12:27:34 +00:00
parent e59ff6173a
commit 4208df943b
5 changed files with 185 additions and 1 deletions

View File

@@ -14,6 +14,7 @@ The Solitaire cipher is included for completeness, though it is perhaps not stri
* [Monoalphabetic substitution]
* [Vigenère]
* [Portas]
* [Hill]
* [Solitaire]
## Gotchas
@@ -186,6 +187,49 @@ decrypt_portas("URYYB, JBEYQ!", "ab")
Notice that the input has been made lowercase.
### Hill cipher
Encrypt the text "Hello, World!" with a Hill key of matrix `[1 2; 5 7]`:
```julia
encrypt_hill("Hello, World!", [1 2; 5 7])
# outputs "PHHRGUWQRV"
```
Notice that the input has been made uppercase and symbols have been stripped out.
Encrypt the same text with the same key, this time represented as a string:
```julia
encrypt_hill("Hello, World!", "bcfh")
# outputs "PLHCGQWHRY"
```
If the plaintext-length is not a multiple of the dimension of the key matrix,
it is padded with X:
```julia
encrypt_hill("Hello", "bcfh")
# outputs "PLHCIX"
decrypt_hill("PLHCIX", "bcfh")
# outputs "hellox"
```
Decrypt the text "PLHCGQWHRY" with key of `[1 2; 5 7]`:
```julia
decrypt_hill("PLHCGQWHRY", [1 2; 5 7])
# outputs "helloworld"
```
Do the same, but using the string representation of the key:
```julia
decrypt_hill("PLHCGQWHRY", "bcfh")
# outputs "helloworld"
```
### Solitaire cipher
Encrypt the text "Hello, World!" with the Solitaire cipher, key "crypto":
@@ -208,3 +252,4 @@ decrypt_solitaire("EXKYI ZSGEH UNTIQ", collect(1:54))
[Monoalphabetic substitution]: https://en.wikipedia.org/wiki/Substitution_cipher
[Solitaire]: https://en.wikipedia.org/wiki/Solitaire_(cipher)
[Portas]: http://practicalcryptography.com/ciphers/porta-cipher/
[Hill]: https://en.wikipedia.org/wiki/Hill_cipher

View File

@@ -9,6 +9,7 @@ include("vigenere.jl")
include("solitaire.jl")
include("portas.jl")
include("affine.jl")
include("hill.jl")
export encrypt_monoalphabetic, decrypt_monoalphabetic, crack_monoalphabetic,
encrypt_caesar, decrypt_caesar, crack_caesar,
@@ -16,6 +17,7 @@ export encrypt_monoalphabetic, decrypt_monoalphabetic, crack_monoalphabetic,
encrypt_vigenere, decrypt_vigenere, crack_vigenere,
encrypt_portas, decrypt_portas,
encrypt_solitaire, decrypt_solitaire,
encrypt_hill, decrypt_hill,
string_fitness, index_of_coincidence
end # module

108
src/hill.jl Normal file
View File

@@ -0,0 +1,108 @@
using Iterators
"""
Encrypts the given plaintext according to the Hill cipher.
The key may be given as a matrix (that is, two-dimensional Array{Int}) or as a string.
If the key is given as a string, the string is converted to uppercase before use, and
symbols are removed. It is assumed to be of square integer length, and the matrix entries
are filled top-left to top-right, then next-top left to next-top right, and so on down
to bottom-left to bottom-right. If the string is not of square integer length, an error
is thrown.
The matrix must be invertible modulo 26. If it is not, an error is thrown.
"""
function encrypt_hill{I <: Integer}(plaintext, key::Array{I, 2})
if round(Integer, det(key)) % 26 == 0
error("Key must be invertible mod 26.")
end
keysize = size(key, 1)
text = uppercase(letters_only(plaintext))
# split the plaintext up into that-size chunks
# pad if necessary
if length(text) % keysize != 0
text = text * ("X"^(keysize - (length(text) % keysize)))
end
chars = [Int(ch[1]) - 65 for ch in split(text, "")]
# split
split_text = reshape(chars, (keysize, div(length(text), keysize)))
encrypted = mapslices(group -> 65 .+ ((key * group) .% 26), split_text, 1)
ans = IOBuffer()
for group in encrypted
for x in group
print(ans, Char(x))
end
end
takebuf_string(ans)
end
function encrypt_hill(plaintext, key::AbstractString)
if round(Integer, sqrt(length(key))) != sqrt(length(key))
error("Hill key must be of square integer size.")
end
matrix_dim = round(Integer, sqrt(length(key)))
key_string = uppercase(letters_only(key))
key_matrix = Array{Integer}(matrix_dim, matrix_dim)
for i in 1:length(key)
key_matrix[ind2sub([matrix_dim, matrix_dim], i)...] = Int(key_string[i]) - 65
end
encrypt_hill(plaintext, transpose(key_matrix))
end
function minor{I <: Integer}(mat::Array{I, 2}, i, j)
d = det(mat[[1:i-1; i+1:end], [1:j-1; j+1:end]])
round(Integer, d)
end
"""
Computes the adjugate matrix for given matrix.
"""
function adjugate{I <: Integer}(mat::Array{I, 2})
arr = [(-1)^(i+j) * minor(mat, i, j) for (i, j) in product(1:size(mat, 1), 1:size(mat, 2))]
ans = reshape(arr, size(mat))
Array{Integer, 2}(transpose(ans))
end
function decrypt_hill{I <: Integer}(ciphertext, key::Array{I, 2})
if ndims(key) != 2
error("Key must be a two-dimensional matrix.")
end
if round(Integer, det(key)) % 26 == 0
error("Key must be invertible mod 26.")
end
# invert the input array mod 26
inv = (adjugate(key) .% 26)
inv = invmod(round(Integer, det(key)), 26) .* inv
inv = inv .% 26
inv = (inv .+ (26 * 26)) .% 26
lowercase(encrypt_hill(ciphertext, inv))
end
function decrypt_hill(ciphertext, key::AbstractString)
if round(Integer, sqrt(length(key))) != sqrt(length(key))
error("Hill key must be of square integer size.")
end
matrix_dim = round(Integer, sqrt(length(key)))
key_string = uppercase(letters_only(key))
key_matrix = Array{Integer}(matrix_dim, matrix_dim)
for i in 1:length(key)
key_matrix[ind2sub([matrix_dim, matrix_dim], i)...] = Int(key_string[i]) - 65
end
decrypt_hill(ciphertext, transpose(key_matrix))
end

28
test/hill.jl Normal file
View File

@@ -0,0 +1,28 @@
using ClassicalCiphers
using Base.Test
# Wikipedia examples
@test encrypt_hill("help!", [3 3; 2 5]) == "HIAT"
@test decrypt_hill("hiat", [3 3; 2 5]) == "help"
@test encrypt_hill("act", "GYBNQKURP") == "POH"
@test decrypt_hill("POH", "GYBNQKURP") == "act"
@test encrypt_hill("cat", "GYBNQKURP") == "FIN"
@test decrypt_hill("fin", "GYBNQKURP") == "cat"
# doc examples
@test encrypt_hill("Hello, World!", [1 2; 5 7]) == "PLHCGQWHRY"
@test encrypt_hill("Hello, World!", "bcfh") == "PLHCGQWHRY"
@test encrypt_hill("Hello", "bcfh") == "PLHCIX"
@test decrypt_hill("PLHCIX", "bcfh") == "hellox"
@test decrypt_hill("PLHCGQWHRY", [1 2; 5 7]) == "helloworld"
@test decrypt_hill("PLHCGQWHRY", "bcfh") == "helloworld"
# Practical Cryptography examples
@test encrypt_hill("att", [2 4 5; 9 2 1; 3 17 7]) == "PFO"
@test encrypt_hill("the gold is buried in orono", [5 17; 4 15]) == uppercase("gzscxnvcdjzxeovcrclsrc")
@test decrypt_hill("gzscxnvcdjzxeovcrclsrc", [5 17; 4 15]) == "thegoldisburiedinorono"

View File

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