diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..c822caf --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,106 @@ +name: CI +# Run on master, tags, or any pull request +on: + schedule: + - cron: '0 2 * * *' # Daily at 2 AM UTC (8 PM CST) + push: + branches: [master] + tags: ["*"] + pull_request: +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - "1.0" # LTS + - "1.1" + - "1.5" + - "nightly" + os: + - ubuntu-latest + - macOS-latest + - windows-latest + arch: + - x64 + - x86 + exclude: + # Test 32-bit only on Linux + - os: macOS-latest + arch: x86 + - os: windows-latest + arch: x86 + include: + # Add a 1.3 job because that's what Invenia actually uses + - os: ubuntu-latest + version: 1.3 + arch: x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-buildpkg@latest + - run: | + git config --global user.name Tester + git config --global user.email te@st.er + - uses: julia-actions/julia-runtest@latest + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info + + slack: + name: Notify Slack Failure + needs: test + runs-on: ubuntu-latest + if: github.event == 'schedule' + steps: + - uses: technote-space/workflow-conclusion-action@v2 + - uses: voxmedia/github-action-slack-notify-build@v1 + if: env.WORKFLOW_CONCLUSION == 'failure' + with: + channel: nightly-dev + status: FAILED + color: danger + env: + SLACK_BOT_TOKEN: ${{ secrets.DEV_SLACK_BOT_TOKEN }} + + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: '1' + - run: | + git config --global user.name name + git config --global user.email email + git config --global github.user username + - run: | + julia --project=docs -e ' + using Pkg; + Pkg.develop(PackageSpec(path=pwd())); + Pkg.instantiate();' + - run: | + julia --project=docs -e ' + using Documenter: doctest + using PkgTemplates + doctest(PkgTemplates)' + - run: julia --project=docs docs/make.jl + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml new file mode 100644 index 0000000..cba9134 --- /dev/null +++ b/.github/workflows/CompatHelper.yml @@ -0,0 +1,16 @@ +name: CompatHelper +on: + schedule: + - cron: 0 0 * * * + workflow_dispatch: +jobs: + CompatHelper: + runs-on: ubuntu-latest + steps: + - name: Pkg.add("CompatHelper") + run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: CompatHelper.main() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} + run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 0000000..e72d645 --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,13 @@ +name: TagBot +on: + schedule: + - cron: 0 0 * * * + workflow_dispatch: +jobs: + TagBot: + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.gitignore b/.gitignore index d0e30c2..2e02d30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,27 @@ +# Files generated by invoking Julia with --code-coverage *.jl.cov *.jl.*.cov + +# Files generated by invoking Julia with --track-allocation *.jl.mem + +# System-specific files and directories generated by the BinaryProvider and BinDeps packages +# They contain absolute paths specific to the host computer, and so should not be committed +deps/deps.jl +deps/build.log +deps/downloads/ +deps/usr/ +deps/src/ + +# Build artifacts for creating documentation generated by the Documenter package +docs/build/ +docs/site/ + +# File generated by Pkg, the package manager, based on a corresponding Project.toml +# It records a fixed state of all packages used by the project. As such, it should not be +# committed for packages, but should be committed for applications that require a static +# environment. Manifest.toml -.vscode/* + +# Silly macOS stuff +.DS_Store diff --git a/README.md b/README.md index bcbf68c..dd4ed4a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ -[![Build Status](https://travis-ci.org/Smaug123/ClassicalCiphers.jl.svg?branch=master)](https://travis-ci.org/Smaug123/ClassicalCiphers.jl) [![Coverage Status](https://coveralls.io/repos/Smaug123/ClassicalCiphers.jl/badge.svg?branch=master&service=github)](https://coveralls.io/github/Smaug123/ClassicalCiphers.jl?branch=master) [![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle) +

+ ClassicalCiphers.jl +

-# ClassicalCiphers + +[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://username.github.io/MyCoolPackage.jl/dev) +[![CI](https://github.com/invenia/PkgTemplates.jl/workflows/CI/badge.svg)](https://github.com/username/MyCoolPackage.jl/actions?query=workflow%3ACI) +[![Coverage Status](https://coveralls.io/repos/Smaug123/ClassicalCiphers.jl/badge.svg?branch=master&service=github)](https://coveralls.io/github/Smaug123/ClassicalCiphers.jl?branch=master) +[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle) ## Main Features -Provides access to encryption and decryption of strings according to a variety of classical algorithms. +Provides access to encryption and decryption of strings according to a variety of classical algorithms. Classical ciphers were created before computers, and thus work on letters rather than bits. + The Solitaire cipher is included for completeness, though it is perhaps not strictly classical. ## Currently Implemented @@ -20,6 +27,7 @@ The Solitaire cipher is included for completeness, though it is perhaps not stri * [Solitaire] * [Rail Fence] * [Substitution] +* [Atbash] ## Gotchas @@ -398,6 +406,12 @@ julia> decrypt_atbash("HLNV GVCG", "abcdefghijklmnopqrstuvwxyz") "some text" ``` +### Atbash + +```julia +encrypt_atbash("hello this is plaintext", "abcdefghijklmnopqrstuvwxyz") == encrypt_substitution("hello this is plaintext", "abcdefghijklmnopqrstuvwxyz", "zyxwvutsrqponmlkjihgfedcba") +``` + [Caesar]: https://en.wikipedia.org/wiki/Caesar_cipher [Affine]: https://en.wikipedia.org/wiki/Affine_cipher [Vigenère]: https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher @@ -409,3 +423,4 @@ julia> decrypt_atbash("HLNV GVCG", "abcdefghijklmnopqrstuvwxyz") [Enigma]: https://en.wikipedia.org/wiki/Enigma_machine [Rail Fence]: https://en.wikipedia.org/wiki/Rail_fence_cipher [Substitution]: https://en.wikipedia.org/wiki/Substitution_cipher +[Atbash]: https://en.wikipedia.org/wiki/Atbash diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..34723a0 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,3 @@ +[deps] +ClassicalCiphers = "ecf26e93-935c-5e64-9b21-bc8ac81b4723" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..fd4ccb5 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,20 @@ +include(joinpath(dirname(@__DIR__), "src", "ClassicalCiphers.jl")) +using Documenter, .ClassicalCiphers + +Documenter.makedocs( + clean = true, + doctest = true, + modules = Module[ClassicalCiphers], + repo = "", + highlightsig = true, + sitename = "ClassicalCiphers Documentation", + expandfirst = [], + pages = [ + "Index" => "index.md", + "Usage" => "usage.md" + ] +) + +deploydocs(; + repo = "github.com/Smaug123/ClassicalCiphers.jl.git", +) diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..da429ff --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,41 @@ +# Home + +This package provides access to encryption and decryption of strings according to a variety of classical algorithms. Classical ciphers were created before computers, and thus work on letters rather than bits. + +The Solitaire cipher is included for completeness, though it is perhaps not strictly classical. + +Currently implemented ciphers: + - [Caesar](https://en.wikipedia.org/wiki/Caesar_cipher) + - [Affine](https://en.wikipedia.org/wiki/Affine_cipher) + - [Monoalphabetic substitution](https://en.wikipedia.org/wiki/Substitution_cipher) + - [Vigenère](https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher) + - [Portas](http://practicalcryptography.com/ciphers/porta-cipher/) + - [Hill](https://en.wikipedia.org/wiki/Hill_cipher) + - [Playfair](https://en.wikipedia.org/wiki/Playfair_cipher) + - [Enigma (M3 Army)](https://en.wikipedia.org/wiki/Enigma_machine) + - [Solitaire](https://en.wikipedia.org/wiki/Solitaire_(cipher)) + - [Rail Fence](https://en.wikipedia.org/wiki/Rail_fence_cipher) + - [Substitution](https://en.wikipedia.org/wiki/Substitution_cipher) + - [Atbash](https://en.wikipedia.org/wiki/Atbash) + + +## Contents +```@contents +``` + +```@meta +CurrentModule = ClassicalCiphers +DocTestSetup = quote + using ClassicalCiphers +end +``` + +## Installing ClassicalCiphers.jl +```@repl +using Pkg +Pkg.add("ClassicalCiphers") +``` + +## Index +```@index +``` diff --git a/docs/src/usage.md b/docs/src/usage.md new file mode 100644 index 0000000..401a2c7 --- /dev/null +++ b/docs/src/usage.md @@ -0,0 +1,5 @@ +## Usage + +```@autodocs +Modules = [ClassicalCiphers] +``` diff --git a/src/affine.jl b/src/affine.jl index 5e6f7b9..cff2c2a 100644 --- a/src/affine.jl +++ b/src/affine.jl @@ -1,4 +1,8 @@ """ +```julia +encrypt_affine(plaintext, mult::Integer, add::Integer; offset::Integer = 0) +``` + Encrypts the given plaintext according to the Affine cipher. The key is given as a pair of integers: first the multiplier, then the additive constant. @@ -7,10 +11,19 @@ The multiplier must be coprime to 26. If it is not, an error is thrown. Converts the input to uppercase, but retains symbols. -Optional argument: offset=0, which specifies what number 'a' should be +Optional argument: `offset=0`, which specifies what number 'a' should be considered as. + +--- + +### Examples + +```julia +julia> encrypt_affine("Hello, World!", 3, 4) +"ZQLLU, SUDLN!" +``` """ -function encrypt_affine(plaintext, mult::T, add::T; offset::T = 0) where {T <: Integer} +function encrypt_affine(plaintext, mult::Integer, add::Integer; offset::Integer = 0) if mult % 2 == 0 || mult % 13 == 0 error("Multiplier must be coprime to 26.") end @@ -20,6 +33,10 @@ function encrypt_affine(plaintext, mult::T, add::T; offset::T = 0) where {T <: I end """ +```julia +decrypt_affine(ciphertext, mult::Integer, add::Integer; offset::Integer=0) +``` + Decrypts the given ciphertext according to the Affine cipher. The key is given as a pair of integers: first the multiplier, then the additive constant. @@ -28,10 +45,19 @@ The multiplier must be coprime to 26. If it is not, an error is thrown. Converts the input to lowercase, but retains symbols. -Optional argument: offset=0, which specifies what number 'a' should be +Optional argument: `offset=0`, which specifies what number 'a' should be considered as. + +--- + +### Examples + +```julia +julia> decrypt_affine("ZQLLU, SUDLN!", 3, 4) +"hello, world!" +``` """ -function decrypt_affine(ciphertext, mult::T, add::T; offset=0) where {T <: Integer} +function decrypt_affine(ciphertext, mult::Integer, add::Integer; offset::Integer = 0) if mult % 2 == 0 || mult % 13 == 0 error("Multiplier must be coprime to 26.") end @@ -61,15 +87,28 @@ function max_by(arr::AbstractArray, f::Function) end """ +```julia +crack_affine(ciphertext; mult::Integer = 0, add::Integer = -1) +``` + Cracks the given ciphertext according to the Affine cipher. -Returns ((multiplier, additive constant), decrypted string). +Returns `((multiplier, additive constant), decrypted string)`. Converts the input to lowercase, but retains symbols. -Optional arguments: mult=0, which specifies the multiplier if known; -add=-1, which specifies the additive constant if known. +Optional arguments: `mult=0`, which specifies the multiplier if known; +`add=-1`, which specifies the additive constant if known. + +--- + +### Examples + +```julia +julia> crack_affine("ZQLLU, SUDLN!") +("hello, world!", (3, 4)) +``` """ -function crack_affine(ciphertext; mult::T = 0, add::T = -1) where {T <: Integer} +function crack_affine(ciphertext; mult::Integer = 0, add::Integer = -1) mults = mult != 0 ? Int[mult] : Int[i for i in filter(x -> (x % 2 != 0 && x % 13 != 0), 1:25)] adds = add != -1 ? Int[add] : (0:25) possible_keys = Iterators.product(mults, adds) diff --git a/src/caesar.jl b/src/caesar.jl index 59cdb91..86fc230 100644 --- a/src/caesar.jl +++ b/src/caesar.jl @@ -1,14 +1,28 @@ """ +```julia +encrypt_caesar(plaintext, key::Integer) +encrypt_caesar(plaintext) +``` + Encrypts the given plaintext according to the Caesar cipher. The key is given as an integer, being the offset of each character; -so encrypt_caesar("abc", 1) == "BCD". +so `encrypt_caesar("abc", 1) == "BCD"`. -Converts the input to uppercase. +Converts the input to uppercase, but retains symbols. Traditionally, the Caesar cipher was used with a shift of 3, so this is the method it will fall back to if only given plaintext. + +--- + +### Examples + +```julia +julia> encrypt_caesar("Hello, World!", 3) +"KHOOR, ZRUOG!" +``` """ -function encrypt_caesar(plaintext, key::T) where {T <: Integer} +function encrypt_caesar(plaintext, key::Integer) # plaintext: string; key: integer offset, so k=1 sends "a" to "b" key = ((key - 1) % 26) + 1 keystr = join(vcat(collect(Char(97 + key):'z'), collect('a':Char(97 + key - 1)))) @@ -17,16 +31,30 @@ end encrypt_caesar(plaintext) = encrypt_caesar(plaintext, 3) """ +```julia +decrypt_caesar(ciphertext, key::Integer) +decrypt_caesar(ciphertext) +``` + Decrypts the given ciphertext according to the Caesar cipher. The key is given as an integer, being the offset of each character; -so decrypt_caesar("abcd", 1) == "zabc". +so `decrypt_caesar("abcd", 1) == "zabc"`. -Converts the input to lowercase. +Converts the input to lowercase, but retains symbols. Traditionally, the Caesar cipher was used with a shift of 3, so this is the method it will fall back to if only given plaintext. + +--- + +### Examples + +```julia +julia> decrypt_caesar("Khoor, Zruog!", 3) +"hello, world!" +``` """ -function decrypt_caesar(ciphertext, key::T) where {T <: Integer} +function decrypt_caesar(ciphertext, key::Integer) # ciphertext: string; key: integer offset, so k=1 decrypts "B" as "a" key = ((key - 1) % 26) + 1 return lowercase(encrypt_caesar(ciphertext, 26 - key)) @@ -34,22 +62,36 @@ end decrypt_caesar(plaintext) = decrypt_caesar(plaintext, 3) """ +```julia +crack_caesar(ciphertext; cleverness::Integer = 1) +``` + Cracks the given ciphertext according to the Caesar cipher. -Returns (plaintext, key::Integer), such that encrypt_caesar(plaintext, key) +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. +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. + +--- + +### Examples + +```julia +julia> crack_caesar("Khoor, Zruog!") +("hello, world!", 3) +``` """ -function crack_caesar(ciphertext; cleverness::T = 1) where {T <: Integer} +function crack_caesar(ciphertext; cleverness::Integer = 1) texts = Tuple{String, Int}[(decrypt_caesar(ciphertext,key), key) for key in 0:25] + 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 - return texts[end] + return last(texts) end diff --git a/src/common.jl b/src/common.jl index 4d3fa8c..e50681f 100644 --- a/src/common.jl +++ b/src/common.jl @@ -3,7 +3,7 @@ function letters_only(text::AbstractString) return filter(x -> ('A' <= x <= 'Z' || 'a' <= x <= 'z'), text) end -function rotate_right(arr::AbstractVector, n::T) where {T <: Integer} +function rotate_right(arr::AbstractVector, n::Integer) # implementation of the Mathematica function rotate_right - or you could try circshift()? ans = copy(arr) for i in 1:length(arr) @@ -13,7 +13,7 @@ function rotate_right(arr::AbstractVector, n::T) where {T <: Integer} return ans end -function rotate_left(arr::AbstractVector, n::T) where {T <: Integer} +function rotate_left(arr::AbstractVector, n::Integer) # implementation of the Mathematica function rotate_left ans = copy(arr) for i in 1:length(arr) @@ -23,9 +23,9 @@ function rotate_left(arr::AbstractVector, n::T) where {T <: Integer} return ans end -rotate_left_str(st::AbstractString, n::T) where {T <: Integer} = +rotate_left_str(st::AbstractString, n::Integer) = join(rotate_left(collect(st), n)) -rotate_right_str(st::AbstractString, n::T) where {T <: Integer} = +rotate_right_str(st::AbstractString, n::Integer) = join(rotate_right(collect(st), n)) function split_by(arr::AbstractVector, func::Function) @@ -85,8 +85,12 @@ function string_fitness(input::AbstractString; alreadystripped::Bool = false) end """ -Finds the frequencies of all characters in the input string, returning a Dict -of 'a' => 4, for instance. Uppercase characters are considered distinct from lowercase. +```julia +frequencies(input::AbstractString) +``` + +Finds the frequencies of all characters in the input string, returning a `Dict` +of `'a' => 4`, for instance. Uppercase characters are considered distinct from lowercase. """ function frequencies(input::AbstractString) ans = Dict{Char, Int}() @@ -102,6 +106,10 @@ function frequencies(input::AbstractString) end """ +```julia +index_of_coincidence(input::AbstractString) +``` + Finds the index of coincidence of the input string. Uppercase characters are considered to be equal to their lowercase counterparts. """ diff --git a/src/enigma.jl b/src/enigma.jl index 14ea115..c84fb31 100644 --- a/src/enigma.jl +++ b/src/enigma.jl @@ -55,30 +55,57 @@ function parse_reflector(reflector::AbstractString) end """ +```julia +function encrypt_enigma(plaintext, + rotors::Array{Integer, 1}, key::AbstractString; + reflector_id='B', ring::AbstractString = "AAA", + stecker = Tuple{Char, Char}[], + skip_stecker_check = false) +``` + Encrypts the given plaintext according to the Enigma (M3, army version). -Arguments are in the order: plaintext, stecker, rotors, ring, key. +Arguments are in the order: `plaintext, stecker, rotors, ring, key.` Plaintext is a string; punctuation is stripped out and it is made lowercase. -Rotors is an array - for example, [1,2,3] - being the order of the rotors. +Rotors is an array - for example, `[1,2,3]` - being the order of the rotors. Each entry should be a distinct integer between 1 and 5 inclusive. Key is a string of three letters, indicating the starting positions of the rotors. Optional: -reflector_id='B', which sets whether to use reflector A, B or C. + * `reflector_id='B'`, which sets whether to use reflector A, B or C. Can also be specified as a 26-char string. -Stecker is either an array - for example, [('A','B'), ('D', 'E')] specifying + * Stecker is either an array - for example,` [('A','B'), ('D', 'E')]` specifying that A, B are swapped and D, E are swapped - or a string ("ABDE" accomplishing the same thing). No letter may appear more than once. -Ring is a string - for example, "AAA" - being the offset applied to each rotor. + * Ring is a string - for example, "AAA" - being the offset applied to each rotor. "AAA", for example, signifies no offset. The string must be three letters. -skip_stecker_check=false, which when `true` skips validation of stecker settings. + * `skip_stecker_check=false`, which when `true` skips validation of stecker settings. + +--- + +### Examples + +```julia +julia> encrypt_enigma("AAA", [1,2,3], "ABC") +"CXT" + +julia> encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id='B', stecker="") # synonymous with above +"CXT" + +julia> encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id="YRUHQSLDPXNGOKMIEBFZCWVJAT", stecker="") # synonymous with above +"CXT" + +julia> encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id='B', stecker=Tuple{Char, Char}[]) # synonymous with above +"CXT" +``` """ function encrypt_enigma(plaintext, rotors::Array{T, 1}, key::AbstractString; reflector_id='B', ring::AbstractString = "AAA", stecker = Tuple{Char, Char}[], - skip_stecker_check = false) where {T <: Integer} + skip_stecker_check = false) where T <: Integer + parsed_stecker = parse_stecker(stecker) # validate stecker settings if !skip_stecker_check @@ -243,5 +270,8 @@ function encrypt_enigma(plaintext, return uppercase(String(take!(ans))) end +""" +See `encrypt_enigma` as this function uses identical arguments. +""" decrypt_enigma(args1...; args2...) = lowercase(encrypt_enigma(args1...; args2...)) diff --git a/src/hill.jl b/src/hill.jl index 8998a8f..49f3b91 100644 --- a/src/hill.jl +++ b/src/hill.jl @@ -1,8 +1,12 @@ using LinearAlgebra """ +```julia +encrypt_hill(plaintext::AbstractString, key::AbstractArray{Integer, 2}) +``` + 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. +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 @@ -11,6 +15,21 @@ to bottom-left to bottom-right. If the string is not of square integer length, a is thrown. The matrix must be invertible modulo 26. If it is not, an error is thrown. + +--- + +### Examples + +```julia +julia> encrypt_hill("Hello, World!", [1 2; 5 7]) # Encrypt the text "Hello, World!" with a Hill key of matrix `[1 2; 5 7]` +"PHHRGUWQRV" + +julia> encrypt_hill("Hello, World!", "bcfh") +"PLHCGQWHRY" + +julia> encrypt_hill("Hello", "bcfh") # If the plaintext-length is not a multiple of the dimension of the key matrix, it is padded with X +"PLHCIX" +``` """ function encrypt_hill(plaintext::AbstractString, key::AbstractArray{T, 2}) where {T <: Integer} if round(Integer, det(key)) % 26 == 0 @@ -60,6 +79,10 @@ function minor(mat::AbstractArray{T, 2}, i::K, j::K) where {T <: Integer, K <: I end """ +```julia +adjugate(mat::AbstractArray{Integer, 2}) +``` + Computes the adjugate matrix for given matrix. """ function adjugate(mat::AbstractArray{T, 2}) where {T <: Integer} @@ -68,7 +91,27 @@ function adjugate(mat::AbstractArray{T, 2}) where {T <: Integer} return Array{Integer, 2}(transpose(ans)) end -function decrypt_hill(ciphertext, key::AbstractArray{T, 2}) where {T<:Integer} +""" +```julia +decrypt_hill(ciphertext, key::AbstractArray{T, 2}) where {T <: Integer} +``` + +--- + +### Examples + +```julia +julia> decrypt_hill("PLHCGQWHRY", [1 2; 5 7]) # Decrypt the text "PLHCGQWHRY" with key of `[1 2; 5 7]` +"helloworld" + +julia> decrypt_hill("PLHCGQWHRY", "bcfh") +"helloworld" + +julia> decrypt_hill("PLHCIX", "bcfh") # If the plaintext-length is not a multiple of the dimension of the key matrix, it is padded with X +"hellox" +``` +""" +function decrypt_hill(ciphertext, key::AbstractArray{T, 2}) where {T <: Integer} if ndims(key) != 2 error("Key must be a two-dimensional matrix.") end diff --git a/src/monoalphabetic.jl b/src/monoalphabetic.jl index d774b19..0671198 100644 --- a/src/monoalphabetic.jl +++ b/src/monoalphabetic.jl @@ -3,15 +3,31 @@ function keystr_to_dict(keystr::AbstractString) end """ +```julia +encrypt_monoalphabetic(plaintext, key::Dict{Char, Char}) +``` + Encrypts the given plaintext according to the monoalphabetic substitution cipher. -The key may be given as a Dict of replacements {'a' => 'b', 'c' => 'd'}, etc, +The key may be given as a Dict of replacements `Dict('a' => 'b', 'c' => 'd')`, etc, or as a 26-length string "keystringbcdfhjlmopqruvwxz", which is shorthand for -{'a' => 'k', 'e' => 'b', …} +`Dict('a' => 'k', 'e' => 'b', ...)` If the key is given as a string, it is assumed that each character occurs only once, and the string is converted to lowercase. If the key is given as a Dict, the only substitutions made are those in the Dict; in particular, the string is not converted to lowercase automatically. + +--- + +### Examples + +```julia +julia> encrypt_monoalphabetic("Hello, World!", "DEFGHIJKLMNOPQRSTUVWXYZABC") +"KHOOR, ZRUOG!" + +julia> encrypt_monoalphabetic("aBcbDd", Dict{Char, Char}('a' => '5', 'B' => '@', 'b' => 'o')) +"5@coDd" +``` """ function encrypt_monoalphabetic(plaintext, key::Dict{Char, Char}) # plaintext: string; key: dictionary of {'a' => 'b'}, etc, for replacing 'a' with 'b' @@ -19,21 +35,34 @@ function encrypt_monoalphabetic(plaintext, key::Dict{Char, Char}) end """ +```julia +decrypt_monoalphabetic(ciphertext, key::Dict{Char, Char}) +``` + Decrypts the given ciphertext according to the monoalphabetic substitution cipher. -The key may be given as a Dict of replacements {'a' => 'b', 'c' => 'd'}, etc, +The key may be given as a Dict of replacements `Dict('a' => 'b', 'c' => 'd')`, etc, or as a 26-length string "keystringbcdfhjlmopqruvwxz", which is shorthand for -{'a' => 'k', 'e' => 'b', …} +`Dict('a' => 'k', 'e' => 'b', ...)` If the key is given as a string, it is assumed that each character occurs only once, and the string is converted to lowercase. If the key is given as a Dict, the only substitutions made are those in the Dict; in particular, the string is not converted to lowercase automatically. + +--- + +### Examples + +```julia +julia> decrypt_monoalphabetic("Khoor, Zruog!", "DEFGHIJKLMNOPQRSTUVWXYZABC") +"hello, world!" +``` """ function decrypt_monoalphabetic(ciphertext, key::Dict{Char, Char}) # ciphertext: string; key: dictionary of {'a' => 'b'}, etc, where the plaintext 'a' was # replaced by ciphertext 'b'. No character should appear more than once # as a value in {key}. - return encrypt_monoalphabetic(ciphertext, Dict{Char, Char}([reverse(a) for a in key])) + return encrypt_monoalphabetic(ciphertext, Dict{Char, Char}(reverse(a) for a in key)) end function encrypt_monoalphabetic(plaintext, key::AbstractString) @@ -47,7 +76,7 @@ function decrypt_monoalphabetic(ciphertext, key::AbstractString) # working in lowercase; key is assumed only to have each element appearing once # and to be in lowercase # so decrypt_monoalphabetic("cb", "cbade…") is "ab" - dict = Dict{Char, Char}(a => Char(96 + findfirst(i -> i == a, lowercase(key))) for a in lowercase(key)) + dict = Dict{Char, Char}(a => Char(96 + findfirst(==(a), lowercase(key))) for a in lowercase(key)) return encrypt_monoalphabetic(lowercase(ciphertext), dict) end @@ -56,6 +85,10 @@ end # The method we use for cracking is simulated annealing. """ +```julia +swap_two(str) +``` + swap_two(string) swaps two of the characters of the input string, at random. The characters are guaranteed to be at different positions, though "aa" would be 'swapped' to "aa". @@ -70,34 +103,55 @@ function swap_two(str) end """ +```julia +crack_monoalphabetic( + ciphertext; + starting_key::AbstractString = "", + min_temp::AbstractFloat = 0.0001, + temp_factor::AbstractFloat = 0.97, + acceptance_prob::AbstractFloat = ((e,ep,t) -> ep > e ? 1. : exp(-(e-ep)/t)), + chatty::Integer = 0, + rounds::Integer = 1 +) +``` + crack_monoalphabetic cracks the given ciphertext which was encrypted by the monoalphabetic substitution cipher. -Returns (the derived key, decrypted plaintext). +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 -common characters being decrypted to the most common English characters. -min_temp=0.0001, which is the temperature at which we stop the simulation. -temp_factor=0.97, which is the factor by which the temperature decreases each step. -chatty=0, which can be set to 1 to print whenever the key is updated, or 2 to print -whenever any new key is considered. -rounds=1, which sets the number of repetitions we perform. Each round starts with the -best key we've found so far. -acceptance_prob=((e, ep, t) -> ep>e ? 1 : exp(-(e-ep)/t)), which is the probability -with which we accept new key of fitness ep, given that the current key has fitness e, -at temperature t. +The various optional arguments to `crack_monoalphabetic` are: +* `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 + common characters being decrypted to the most common English characters. +* `min_temp=0.0001`, which is the temperature at which we stop the simulation. +* `temp_factor=0.97`, which is the factor by which the temperature decreases each step. +* `chatty=0`, which can be set to 1 to print whenever the key is updated, or 2 to print + whenever any new key is considered. +* `rounds=1`, which sets the number of repetitions we perform. Each round starts with the + best key we've found so far. +* `acceptance_prob=((e, ep, t) -> ep>e ? 1 : exp(-(e-ep)/t))`, which is the probability + with which we accept new key of fitness ep, given that the current key has fitness e, + at temperature t. + +--- + +### Examples + +```julia +julia> crack_monoalphabetic(str, chatty=0, rounds=10) +(decrypted_string, key) +``` """ function crack_monoalphabetic( ciphertext; starting_key::AbstractString = "", - min_temp::F = 0.0001, - temp_factor::F = 0.97, - acceptance_prob::F = ((e,ep,t) -> ep > e ? 1. : exp(-(e-ep)/t)), - chatty::T = 0, - rounds::T = 1 -) where {T <: Integer, F <: AbstractFloat} + min_temp::AbstractFloat = 0.0001, + temp_factor::AbstractFloat = 0.97, + acceptance_prob::AbstractFloat = ((e,ep,t) -> ep > e ? 1. : exp(-(e-ep)/t)), + chatty::Integer = 0, + rounds::Integer = 1 +) if isempty(starting_key) # most common letters diff --git a/src/playfair.jl b/src/playfair.jl index 94f2903..982442c 100644 --- a/src/playfair.jl +++ b/src/playfair.jl @@ -1,11 +1,23 @@ +""" +```julia AbstractPair{F, S} = Union{Tuple{F, S}, Pair{F, S}} +``` + +A simple wrapper for a `Pair`, or a `Tuple` representing a pair of objects. +""" +AbstractPair{F, S} = Union{Tuple{F, S}, Pair{F, S}} + parse_abstract_pair(P::AbstractPair) = P isa Tuple{Char, Char} ? Dict(reverse(Pair(P...))) : Dict(reverse(P)) """ +```julia +playfair_key_to_square(key::AbstractString, replacement::AbstractPair{Char, Char}) +``` + Converts the given key-string to a Playfair key square. -Parameter `replacement` is a pair, such as ('I', 'J') or 'I' => 'J', containing +Parameter `replacement` is a pair, such as `('I', 'J')` or `'I' => 'J'`, containing the two letters which are combined. Only the first of these letters will be present in the keysquare. """ @@ -32,16 +44,40 @@ function encrypt_playfair(plaintext, key::AbstractString; combined::AbstractPair end """ +```julia +encrypt_playfair(plaintext, key::Array{Char, 2}; stripped::Bool = false, combined::AbstractPair{Char, Char} = ('I', 'J')) +``` + 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 +`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. +`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`. + +--- + +### Examples + +```julia +julia> encrypt_playfair("Hello, World!", "playfair example") +"DMYRANVQCRGE" + +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']; + +julia> encrypt_playfair("Hello, World!", arr) # Encrypt the same text using an explicitly specified keysquare +"DMYRANVQCRGE" + +julia> encrypt_playfair("IJXYZA", "PLAYFIREXM", combined=('I', 'J')) # Optionally specify the two letters which are to be combined (default 'I','J') +"RMRMFWYE" + +julia> encrypt_playfair("IJXYZA", "PLAYFIREXM", combined=('X', 'Z')) +"BSGXEY" +``` """ function encrypt_playfair(plaintext, key::Array{Char, 2}; stripped::Bool = false, combined::AbstractPair{Char, Char} = ('I', 'J')) if !stripped @@ -125,9 +161,22 @@ function decrypt_playfair(ciphertext, key::AbstractString; combined::AbstractPai end """ +```julia +decrypt_playfair(ciphertext, key::Array{Char, 2}; combined::AbstractPair{Char, Char} = ('I', 'J')) +``` + Decrypts the given ciphertext according to the Playfair cipher. Does not attempt to delete X's inserted as padding for double letters. + +--- + +### Examples + +```julia +julia> decrypt_playfair("RMRMFWYE", "playfair example") +"ixixyzax" +``` """ function decrypt_playfair(ciphertext, key::Array{Char, 2}; combined::AbstractPair{Char, Char} = ('I', 'J')) # to obtain the decrypting keysquare, reverse every row and every column diff --git a/src/portas.jl b/src/portas.jl index 45b2086..ceddadc 100644 --- a/src/portas.jl +++ b/src/portas.jl @@ -1,9 +1,22 @@ """ +```julia +encrypt_portas(plaintext, key_in::AbstractString) +``` + Encrypts the given plaintext with the Portas cipher. The key must be given as a string, whose characters are letters. Converts the text to uppercase. + +--- + +### Examples + +```julia +julia> encrypt_portas("Hello, World!", "ab") +"URYYB, JBEYQ!" +``` """ function encrypt_portas(plaintext, key_in::AbstractString) key = uppercase(letters_only(key_in)) @@ -36,11 +49,24 @@ function encrypt_portas(plaintext, key_in::AbstractString) end """ +```julia +decrypt_portas(ciphertext, key::AbstractString) +``` + Decrypts the given ciphertext with the Portas cipher. The key must be given as a string, whose characters are letters. Converts the text to lowercase. + +--- + +### Examples + +```julia +julia> decrypt_portas("URYYB, JBEYQ!", "ab") +"hello, world!" +``` """ function decrypt_portas(ciphertext, key::AbstractString) return lowercase(encrypt_portas(ciphertext, key)) diff --git a/src/railfence.jl b/src/railfence.jl index aa3713a..79d5c08 100644 --- a/src/railfence.jl +++ b/src/railfence.jl @@ -8,13 +8,25 @@ function construct_railfence(input, fence::AbstractArray, n_rails::Integer) return fence end -""" +@doc raw""" ```julia construct_railfence(input::AbstractString, n_rails::Integer) construct_railfence(input::AbstractArray{T}, n_rails::Integer) where {T <: Number} ``` -See https://en.wikipedia.org/wiki/Rail_fence_cipher. +See [`https://en.wikipedia.org/wiki/Rail_fence_cipher`](https://en.wikipedia.org/wiki/Rail_fence_cipher). + +--- + +### Examples + +```julia +julia> construct_railfence("WE ARE DISCOVERED. FLEE AT ONCE", 3) +3×26 Array{Char,2}: + 'W' '□' '□' '□' 'E' '□' '□' '□' 'C' '□' '□' '□' 'R' … '□' '□' 'F' '□' '□' '□' 'A' '□' '□' '□' 'C' '□' + '□' 'E' '□' 'R' '□' 'D' '□' 'S' '□' 'O' '□' 'E' '□' '□' '.' '□' 'L' '□' 'E' '□' 'T' '□' 'N' '□' 'E' + '□' '□' 'A' '□' '□' '□' 'I' '□' '□' '□' 'V' '□' '□' 'D' '□' '□' '□' 'E' '□' '□' '□' 'O' '□' '□' '□' +``` """ function construct_railfence(input::AbstractString, n_rails::Integer) input = uppercase(replace(input, " " => "")) @@ -25,23 +37,41 @@ function construct_railfence(input::AbstractArray{T}, n_rails::Integer) where {T return construct_railfence(input, zeros(T, n_rails, length(input)), n_rails) end -""" +@doc raw""" ```julia encrypt_railfence(input::AbstractString, n_rails::Integer) ``` -See https://en.wikipedia.org/wiki/Rail_fence_cipher. +See [`https://en.wikipedia.org/wiki/Rail_fence_cipher`](https://en.wikipedia.org/wiki/Rail_fence_cipher). + +--- + +### Examples + +```julia +julia> encrypt_railfence("WE ARE DISCOVERED. FLEE AT ONCE", 3) # this reads the above matrix row by row +"WECRFACERDSOEE.LETNEAIVDEO" +``` """ function encrypt_railfence(input::AbstractString, n_rails::Integer) return join(Char[c for rail in eachrow(construct_railfence(input, n_rails)) for c in rail if c != '□']) end -""" +@doc raw""" ```julia encrypt_railfence(input::AbstractString, n_rails::Integer) ``` -See https://en.wikipedia.org/wiki/Rail_fence_cipher. +See [`https://en.wikipedia.org/wiki/Rail_fence_cipher`](https://en.wikipedia.org/wiki/Rail_fence_cipher). + +--- + +### Examples + +```julia +julia> decrypt_railfence("WECRFACERDSOEE.LETNEAIVDEO", 3) +"wearediscovered.fleeatonce" +``` """ function decrypt_railfence(input::AbstractString, n_rails::Integer) char_positions = Int[n for row in eachrow(construct_railfence(1:length(input), n_rails)) for n in row if n != 0] diff --git a/src/solitaire.jl b/src/solitaire.jl index 8c0145a..08fa13d 100644 --- a/src/solitaire.jl +++ b/src/solitaire.jl @@ -87,10 +87,23 @@ function SolitaireKeyStream(initialDeck::AbstractVector{T}) where {T <: Integer} end """ +```julia +encrypt_solitaire(string::AbstractString, initialDeck::AbstractVector{T}) where {T <: Integer} +``` + Encrypts the given plaintext according to the Solitaire cipher. The key may be given either as a vector initial deck, where the cards are 1 through 54 (the two jokers being 53, 54), or as a string. Schneier's keying algorithm is used to key the deck if the key is a string. + +--- + +### Examples + +```julia +julia> encrypt_solitaire("Hello, World!", "crypto") +"GRNNQISRYA" +``` """ function encrypt_solitaire(string::AbstractString, initialDeck::AbstractVector{T}) where {T <: Integer} inp = uppercase(letters_only(string)) @@ -103,10 +116,23 @@ function encrypt_solitaire(string::AbstractString, initialDeck::AbstractVector{T end """ +```julia +decrypt_solitaire(string::AbstractString, initialDeck::AbstractVector{T}) where {T <: Integer} +``` + Decrypts the given ciphertext according to the Solitaire cipher. The key may be given either as a vector initial deck, where the cards are 1 through 54 (the two jokers being 53, 54), or as a string. Schneier's keying algorithm is used to key the deck if the key is a string. + +--- + +### Examples + +```julia +julia> decrypt_solitaire("EXKYI ZSGEH UNTIQ", collect(1:54)) # as per https://www.schneier.com/code/sol-test.txt +"aaaaaaaaaaaaaaa" +``` """ function decrypt_solitaire(string::AbstractString, initialDeck::AbstractVector{T}) where {T <: Integer} inp = uppercase(letters_only(string)) diff --git a/src/substitution.jl b/src/substitution.jl index 3c3ca7e..24e3f64 100644 --- a/src/substitution.jl +++ b/src/substitution.jl @@ -26,7 +26,22 @@ encrypt_substitution(plaintext, "zyxwvutsrqponmlkjihgfedcba") # this will create As per convention, the output will always be uppercase. -For more information, see https://en.wikipedia.org/wiki/Substitution_cipher. +For more information, see [`https://en.wikipedia.org/wiki/Substitution_cipher`](https://en.wikipedia.org/wiki/Substitution_cipher). + +--- + +### Examples + +```julia +julia> encrypt_substitution("Hello, this is plaintext", "abcdefghijklmnopqrstuvwxyz", "qwertyuiopasdfghjklzxcvbnm") +"ITSSG, ZIOL OL HSQOFZTBZ" + +julia> encrypt_substitution("Hello, this is plaintext", "qwertyuiopasdfghjklzxcvbnm") +"ITSSG, ZIOL OL HSQOFZTBZ" + +julia> encrypt_substitution("xyz", Dict('x' => 'd', 'y' => 'e', 'z' => 't')) +"DET" +``` """ encrypt_substitution(plaintext, sub_dict::Dict{T, S}) where {T, S} = uppercase(join(something_crypt_substitution(lowercase(plaintext), sub_dict))) @@ -58,7 +73,22 @@ decrypt_substitution(ciphertext, "zyxwvutsrqponmlkjihgfedcba"; reverse_dict = tr As per convention, the output will always be lowercase. -For more information, see https://en.wikipedia.org/wiki/Substitution_cipher. +For more information, see [`https://en.wikipedia.org/wiki/Substitution_cipher`](https://en.wikipedia.org/wiki/Substitution_cipher). + +--- + +### Examples + +```julia +julia> decrypt_substitution("ITSSG, ZIOL OL HSQOFZTBZ", "abcdefghijklmnopqrstuvwxyz", "qwertyuiopasdfghjklzxcvbnm", reverse_dict = true) +"hello, this is plaintext" + +julia> encrypt_atbash("some text", "abcdefghijklmnopqrstuvwxyz") +"HLNV GVCG" + +julia> decrypt_atbash("HLNV GVCG", "abcdefghijklmnopqrstuvwxyz") +"some text" +``` """ function decrypt_substitution(ciphertext, sub_dict::Dict{T, S}; reverse_dict::Bool = true) where {T, S} sub_dict = reverse_dict ? reverse(sub_dict) : sub_dict diff --git a/src/vigenere.jl b/src/vigenere.jl index 7ea736d..fbbc7c7 100644 --- a/src/vigenere.jl +++ b/src/vigenere.jl @@ -1,51 +1,81 @@ using Statistics """ +```julia +encrypt_vigenere(plaintext, key::Array) +encrypt_vigenere(ciphertext, key::AbstractString) +``` + Encrypts the given string using the Vigenere cipher according to the given vector of offsets. -For example, encrypt_vigenere("ab", [0, 1]) returns "AC". +For example, `encrypt_vigenere("ab", [0, 1])` returns `"AC"`. + +--- + +### Examples + +```julia +julia> encrypt_vigenere("Hello, World!", "ab") +"HFLMOXOSLE" +``` """ function encrypt_vigenere(plaintext, key::Array) # plaintext: string; key: vector of integer offsets, so [0, 1] encrypts "ab" as "ac" ans = String[encrypt_caesar(chr, key[(i - 1) % length(key) + 1]) for (i, chr) in enumerate(letters_only(plaintext))] return join(ans) end - -""" -Decrypts the given string using the Vigenere cipher according to the given vector of offsets. -For example, decrypt_vigenere("ac", [0, 1]) returns "ab". -""" -function decrypt_vigenere(ciphertext, key::Array) - # ciphertext: string; key: vector of integer offsets, so [0, 1] decrypts "ac" as "ab" - return lowercase(encrypt_vigenere(ciphertext, map(x -> 26 - x, key))) -end - -""" -Encrypts the given string using the Vigenere cipher according to the given keystring. -For example, encrypt_vigenere("ab", "ab") returns "AC". -""" function encrypt_vigenere(ciphertext, key::AbstractString) # ciphertext: string; key: string, so "ab" encrypts "ab" as "AC" return encrypt_vigenere(ciphertext, Int[Int(i) - 97 for i in lowercase(letters_only(key))]) end """ -Decrypts the given string using the Vigenere cipher according to the given keystring. -For example, decrypt_vigenere("ab", "ac") returns "ab". +```julia +decrypt_vigenere(ciphertext, key::Array) +decrypt_vigenere(plaintext, key::AbstractString) +``` + +Decrypts the given string using the Vigenere cipher according to the given vector of offsets. +For example, `decrypt_vigenere("ac", [0, 1])` returns `"ab"`. + +--- + +### Examples + +```julia +julia> decrypt_vigenere("HFLMOXOSLE", [0, 1]) # Notice that the offset `0` corresponds to the key `a`. +"helloworld" +``` """ +function decrypt_vigenere(ciphertext, key::Array) + # ciphertext: string; key: vector of integer offsets, so [0, 1] decrypts "ac" as "ab" + return lowercase(encrypt_vigenere(ciphertext, map(x -> 26 - x, key))) +end function decrypt_vigenere(plaintext, key::AbstractString) # plaintext: string; key: string, so "ab" decrypts "ac" as "ab" return decrypt_vigenere(plaintext, Int[Int(i) - 97 for i in lowercase(letters_only(key))]) end """ +```julia +crack_vigenere(plaintext; keylength::Integer = 0) +``` + Cracks the given text encrypted with the Vigenere cipher. -Returns (derived key, decrypted plaintext). +Returns `(derived key, decrypted plaintext)`. Optional parameters: -keylength=0: if the key length is known, specifying it may help the solver. +`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. + +--- + +### Examples + +```julia +julia> crack_vigenere(str) +``` """ function crack_vigenere(plaintext; keylength::Integer = 0) stripped_text = letters_only(lowercase(plaintext))