Merge pull request #6 from Smaug123/pull-request/d6813fa1

Add Hill encrypt/decrypt
This commit is contained in:
Smaug123
2016-01-07 13:54:51 +00:00
5 changed files with 188 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,52 @@ 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.
The key matrix must be invertible mod 26. That is, its determinant must be
coprime to 26.
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 +255,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:")