diff --git a/README.md b/README.md index 3b8cd76..023676e 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,19 @@ decrypt_vigenere("HFLMOXOSLE", [0, 1]) Notice that the offset `0` corresponds to the key `a`. +Crack a text: + +```julia +crack_vigenere(str) +``` + +This attempts to use the index of coincidence to find the keylength, +and then performs frequency analysis to derive the key. +It returns (key, decrypted text). + +If the keylength is known, specifying it as `crack_vigenere(str, keylength=6)` +may aid decryption. + ### Solitaire cipher Encrypt the text "Hello, World!" with the Solitaire cipher, key "crypto": @@ -134,6 +147,7 @@ decrypt_solitaire("EXKYI ZSGEH UNTIQ", collect(1:54)) # outputs "aaaaaaaaaaaaaaa", as per https://www.schneier.com/code/sol-test.txt ``` + [Caesar]: https://en.wikipedia.org/wiki/Caesar_cipher [Vigenère]: https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher [Monoalphabetic substitution]: https://en.wikipedia.org/wiki/Substitution_cipher diff --git a/src/ClassicalCiphers.jl b/src/ClassicalCiphers.jl index f4fe4ea..c96ec40 100644 --- a/src/ClassicalCiphers.jl +++ b/src/ClassicalCiphers.jl @@ -10,8 +10,8 @@ include("solitaire.jl") export encrypt_monoalphabetic, decrypt_monoalphabetic, crack_monoalphabetic, encrypt_caesar, decrypt_caesar, crack_caesar, - encrypt_vigenere, decrypt_vigenere, + encrypt_vigenere, decrypt_vigenere, crack_vigenere, encrypt_solitaire, decrypt_solitaire, - string_fitness + string_fitness, index_of_coincidence end # module diff --git a/src/caesar.jl b/src/caesar.jl index ab7893f..d3015e6 100644 --- a/src/caesar.jl +++ b/src/caesar.jl @@ -30,10 +30,17 @@ Cracks the given ciphertext according to the Caesar cipher. Returns (plaintext, key::Integer), such that encrypt_caesar(plaintext, key) would return ciphertext. +With cleverness=0, simply does the shift that maximises e's frequency. +With cleverness=1, maximises the string's total fitness. + Converts the input to lowercase. """ -function crack_caesar(ciphertext) +function crack_caesar(ciphertext; cleverness=1) texts = [(decrypt_caesar(ciphertext,key), key) for key in 1:26] - texts = sort(texts, by=(x -> string_fitness(first(x)))) + if cleverness == 1 + texts = sort(texts, by=(x -> string_fitness(first(x)))) + else + texts = sort(texts, by=(x -> length(collect(filter(i -> (i == 'e'), first(x)))))) + end texts[end] end diff --git a/src/common.jl b/src/common.jl index de5c0c8..de81d55 100644 --- a/src/common.jl +++ b/src/common.jl @@ -95,4 +95,20 @@ function frequencies(input) end end ans +end + +""" +Finds the index of coincidence of the input string. Uppercase characters are considered to be +equal to their lowercase counterparts. +""" +function index_of_coincidence(input) + freqs = frequencies(lowercase(letters_only(input))) + len = length(lowercase(letters_only(input))) + + ans = 0 + for i in 'a':'z' + ans += (x -> x*(x-1))(get(freqs, i, 0)) + end + + ans /= (len * (len-1) / 26) end \ No newline at end of file diff --git a/src/monoalphabetic.jl b/src/monoalphabetic.jl index 1af80ce..3ea68a4 100644 --- a/src/monoalphabetic.jl +++ b/src/monoalphabetic.jl @@ -69,6 +69,9 @@ end """ crack_monoalphabetic cracks the given ciphertext which was encrypted by the monoalphabetic substitution cipher. + +Returns (the derived key, decrypted plaintext). + Possible arguments include: starting_key="", which when specified (for example, as "ABCDEFGHIJKLMNOPQRSTUVWXYZ"), starts the simulation at the given key. The default causes it to start with the most diff --git a/src/vigenere.jl b/src/vigenere.jl index 965d078..ec2b796 100644 --- a/src/vigenere.jl +++ b/src/vigenere.jl @@ -36,3 +36,37 @@ function decrypt_vigenere(plaintext, key::AbstractString) # plaintext: string; key: string, so "ab" decrypts "ac" as "ab" decrypt_vigenere(plaintext, [Int(i)-97 for i in lowercase(letters_only(key))]) end + +""" +Cracks the given text encrypted with the Vigenere cipher. + +Returns (derived key, decrypted plaintext). + +Optional parameters: +keylength=0: if the key length is known, specifying it may help the solver. + If 0, the solver will attempt to derive the key length using the index + of coincidence. +""" +function crack_vigenere(plaintext; keylength=0) + stripped_text = letters_only(lowercase(plaintext)) + if keylength == 0 + lens = sort(collect(2:15), by= len -> mean([index_of_coincidence(stripped_text[i:len:end]) for i in 1:len])) + keylength = lens[end] + end + + everyother = [stripped_text[i:keylength:end] for i in 1:keylength] + decr = [crack_caesar(st)[1] for st in everyother] + + ans = IOBuffer() + for i in 1:length(decr[1]) + for j in 1:keylength + if i <= length(decr[j]) + print(ans, decr[j][i]) + end + end + end + + derived_key = join([crack_caesar(st)[2] for st in everyother], "") + (derived_key, takebuf_string(ans)) + +end \ No newline at end of file