simple_cipher
This commit is contained in:
parent
b21db6b2f9
commit
2b764143c4
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"DoggettCK"
|
||||||
|
],
|
||||||
|
"contributors": [
|
||||||
|
"angelikatyborska",
|
||||||
|
"Cohen-Carlisle",
|
||||||
|
"devonestes",
|
||||||
|
"neenjaw",
|
||||||
|
"sotojuan"
|
||||||
|
],
|
||||||
|
"files": {
|
||||||
|
"solution": [
|
||||||
|
"lib/simple_cipher.ex"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"test/simple_cipher_test.exs"
|
||||||
|
],
|
||||||
|
"example": [
|
||||||
|
".meta/example.ex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"blurb": "Implement a simple shift cipher like Caesar and a more secure substitution cipher.",
|
||||||
|
"source": "Substitution Cipher at Wikipedia",
|
||||||
|
"source_url": "https://en.wikipedia.org/wiki/Substitution_cipher"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
{"track":"elixir","exercise":"simple-cipher","id":"9a856e2b11c04ba49a484ea3501c5e70","url":"https://exercism.org/tracks/elixir/exercises/simple-cipher","handle":"negrienko","is_requester":true,"auto_approve":false}
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
|
@ -0,0 +1,24 @@
|
||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where third-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||||
|
/.fetch
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
simple_cipher-*.tar
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Help
|
||||||
|
|
||||||
|
## Running the tests
|
||||||
|
|
||||||
|
From the terminal, change to the base directory of the exercise then execute the tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mix test
|
||||||
|
```
|
||||||
|
|
||||||
|
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
|
||||||
|
|
||||||
|
Documentation:
|
||||||
|
|
||||||
|
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
|
||||||
|
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
|
||||||
|
|
||||||
|
## Pending tests
|
||||||
|
|
||||||
|
In test suites of practice exercises, all but the first test have been tagged to be skipped.
|
||||||
|
|
||||||
|
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# @tag :pending
|
||||||
|
test "shouting" do
|
||||||
|
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mix test --include pending
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# ExUnit.configure(exclude: :pending, trace: true)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful `mix test` options
|
||||||
|
|
||||||
|
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
|
||||||
|
* `--failed` - runs only tests that failed the last time they ran
|
||||||
|
* `--max-failures` - the suite stops evaluating tests when this number of test failures
|
||||||
|
is reached
|
||||||
|
* `--seed 0` - disables randomization so the tests in a single file will always be ran
|
||||||
|
in the same order they were defined in
|
||||||
|
|
||||||
|
## Submitting your solution
|
||||||
|
|
||||||
|
You can submit your solution using the `exercism submit lib/simple_cipher.ex` command.
|
||||||
|
This command will upload your solution to the Exercism website and print the solution page's URL.
|
||||||
|
|
||||||
|
It's possible to submit an incomplete solution which allows you to:
|
||||||
|
|
||||||
|
- See how others have completed the exercise
|
||||||
|
- Request help from a mentor
|
||||||
|
|
||||||
|
## Need to get help?
|
||||||
|
|
||||||
|
If you'd like help solving the exercise, check the following pages:
|
||||||
|
|
||||||
|
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
|
||||||
|
- The [Elixir track's programming category on the forum](https://forum.exercism.org/c/programming/elixir)
|
||||||
|
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
|
||||||
|
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
|
||||||
|
|
||||||
|
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
|
||||||
|
|
||||||
|
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
|
|
@ -0,0 +1,89 @@
|
||||||
|
# Simple Cipher
|
||||||
|
|
||||||
|
Welcome to Simple Cipher on Exercism's Elixir Track.
|
||||||
|
If you need help running the tests or submitting your code, check out `HELP.md`.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
Implement a simple shift cipher like Caesar and a more secure substitution cipher.
|
||||||
|
|
||||||
|
## Step 1
|
||||||
|
|
||||||
|
"If he had anything confidential to say, he wrote it in cipher, that is, by so changing the order of the letters of the alphabet, that not a word could be made out.
|
||||||
|
If anyone wishes to decipher these, and get at their meaning, he must substitute the fourth letter of the alphabet, namely D, for A, and so with the others."
|
||||||
|
—Suetonius, Life of Julius Caesar
|
||||||
|
|
||||||
|
Ciphers are very straight-forward algorithms that allow us to render text less readable while still allowing easy deciphering.
|
||||||
|
They are vulnerable to many forms of cryptanalysis, but Caesar was lucky that his enemies were not cryptanalysts.
|
||||||
|
|
||||||
|
The Caesar Cipher was used for some messages from Julius Caesar that were sent afield.
|
||||||
|
Now Caesar knew that the cipher wasn't very good, but he had one ally in that respect: almost nobody could read well.
|
||||||
|
So even being a couple letters off was sufficient so that people couldn't recognize the few words that they did know.
|
||||||
|
|
||||||
|
Your task is to create a simple shift cipher like the Caesar Cipher.
|
||||||
|
This image is a great example of the Caesar Cipher:
|
||||||
|
|
||||||
|
![Caesar Cipher][img-caesar-cipher]
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
Giving "iamapandabear" as input to the encode function returns the cipher "ldpdsdqgdehdu".
|
||||||
|
Obscure enough to keep our message secret in transit.
|
||||||
|
|
||||||
|
When "ldpdsdqgdehdu" is put into the decode function it would return the original "iamapandabear" letting your friend read your original message.
|
||||||
|
|
||||||
|
## Step 2
|
||||||
|
|
||||||
|
Shift ciphers quickly cease to be useful when the opposition commander figures them out.
|
||||||
|
So instead, let's try using a substitution cipher.
|
||||||
|
Try amending the code to allow us to specify a key and use that for the shift distance.
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
Given the key "aaaaaaaaaaaaaaaaaa", encoding the string "iamapandabear"
|
||||||
|
would return the original "iamapandabear".
|
||||||
|
|
||||||
|
Given the key "ddddddddddddddddd", encoding our string "iamapandabear"
|
||||||
|
would return the obscured "ldpdsdqgdehdu"
|
||||||
|
|
||||||
|
In the example above, we've set a = 0 for the key value.
|
||||||
|
So when the plaintext is added to the key, we end up with the same message coming out.
|
||||||
|
So "aaaa" is not an ideal key.
|
||||||
|
But if we set the key to "dddd", we would get the same thing as the Caesar Cipher.
|
||||||
|
|
||||||
|
## Step 3
|
||||||
|
|
||||||
|
The weakest link in any cipher is the human being.
|
||||||
|
Let's make your substitution cipher a little more fault tolerant by providing a source of randomness and ensuring that the key contains only lowercase letters.
|
||||||
|
|
||||||
|
If someone doesn't submit a key at all, generate a truly random key of at least 100 lowercase characters in length.
|
||||||
|
|
||||||
|
## Extensions
|
||||||
|
|
||||||
|
Shift ciphers work by making the text slightly odd, but are vulnerable to frequency analysis.
|
||||||
|
Substitution ciphers help that, but are still very vulnerable when the key is short or if spaces are preserved.
|
||||||
|
Later on you'll see one solution to this problem in the exercise "crypto-square".
|
||||||
|
|
||||||
|
If you want to go farther in this field, the questions begin to be about how we can exchange keys in a secure way.
|
||||||
|
Take a look at [Diffie-Hellman on Wikipedia][dh] for one of the first implementations of this scheme.
|
||||||
|
|
||||||
|
[img-caesar-cipher]: https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Caesar_cipher_left_shift_of_3.svg/320px-Caesar_cipher_left_shift_of_3.svg.png
|
||||||
|
[dh]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
### Created by
|
||||||
|
|
||||||
|
- @DoggettCK
|
||||||
|
|
||||||
|
### Contributed to by
|
||||||
|
|
||||||
|
- @angelikatyborska
|
||||||
|
- @Cohen-Carlisle
|
||||||
|
- @devonestes
|
||||||
|
- @neenjaw
|
||||||
|
- @sotojuan
|
||||||
|
|
||||||
|
### Based on
|
||||||
|
|
||||||
|
Substitution Cipher at Wikipedia - https://en.wikipedia.org/wiki/Substitution_cipher
|
|
@ -0,0 +1,84 @@
|
||||||
|
defmodule SimpleCipher do
|
||||||
|
@chars ?a..?z
|
||||||
|
|
||||||
|
defguardp is_char(char) when char in @chars
|
||||||
|
defguardp is_sign(sign) when sign in [-1, 1]
|
||||||
|
|
||||||
|
defp cipher(text, key) do
|
||||||
|
length = String.length(text)
|
||||||
|
times = div(length, String.length(key)) + 1
|
||||||
|
|
||||||
|
key
|
||||||
|
|> Kernel.to_charlist()
|
||||||
|
|> Enum.map(&(&1 - ?a))
|
||||||
|
|> List.duplicate(times)
|
||||||
|
|> List.flatten()
|
||||||
|
|> Enum.take(length)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rotate(text, key, sign) when is_binary(text) and is_sign(sign) do
|
||||||
|
cipher = cipher(text, key)
|
||||||
|
|
||||||
|
text
|
||||||
|
|> Kernel.to_charlist()
|
||||||
|
|> Enum.with_index(fn element, index -> rotate(element, Enum.at(cipher, index), sign) end)
|
||||||
|
|> Kernel.to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rotate(char, shift, sign) when is_sign(sign) and is_char(char) and is_char(char + sign * shift), do: char + sign * shift
|
||||||
|
defp rotate(char, shift, sign) when is_sign(sign) and is_char(char), do: char + sign * shift - sign * 26
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Given a `plaintext` and `key`, encode each character of the `plaintext` by
|
||||||
|
shifting it by the corresponding letter in the alphabet shifted by the number
|
||||||
|
of letters represented by the `key` character, repeating the `key` if it is
|
||||||
|
shorter than the `plaintext`.
|
||||||
|
|
||||||
|
For example, for the letter 'd', the alphabet is rotated to become:
|
||||||
|
|
||||||
|
defghijklmnopqrstuvwxyzabc
|
||||||
|
|
||||||
|
You would encode the `plaintext` by taking the current letter and mapping it
|
||||||
|
to the letter in the same position in this rotated alphabet.
|
||||||
|
|
||||||
|
abcdefghijklmnopqrstuvwxyz
|
||||||
|
defghijklmnopqrstuvwxyzabc
|
||||||
|
|
||||||
|
"a" becomes "d", "t" becomes "w", etc...
|
||||||
|
|
||||||
|
Each letter in the `plaintext` will be encoded with the alphabet of the `key`
|
||||||
|
character in the same position. If the `key` is shorter than the `plaintext`,
|
||||||
|
repeat the `key`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
plaintext = "testing"
|
||||||
|
key = "abc"
|
||||||
|
|
||||||
|
The key should repeat to become the same length as the text, becoming
|
||||||
|
"abcabca". If the key is longer than the text, only use as many letters of it
|
||||||
|
as are necessary.
|
||||||
|
"""
|
||||||
|
def encode(plaintext, key), do: rotate(plaintext, key, 1)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Given a `ciphertext` and `key`, decode each character of the `ciphertext` by
|
||||||
|
finding the corresponding letter in the alphabet shifted by the number of
|
||||||
|
letters represented by the `key` character, repeating the `key` if it is
|
||||||
|
shorter than the `ciphertext`.
|
||||||
|
|
||||||
|
The same rules for key length and shifted alphabets apply as in `encode/2`,
|
||||||
|
but you will go the opposite way, so "d" becomes "a", "w" becomes "t",
|
||||||
|
etc..., depending on how much you shift the alphabet.
|
||||||
|
"""
|
||||||
|
def decode(ciphertext, key), do: rotate(ciphertext, key, -1)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generate a random key of a given length. It should contain lowercase letters only.
|
||||||
|
"""
|
||||||
|
def generate_key(length) do
|
||||||
|
1..length
|
||||||
|
|> Enum.map(fn _i -> Enum.random(@chars) end)
|
||||||
|
|> to_string()
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule SimpleCipher.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :simple_cipher,
|
||||||
|
version: "0.1.0",
|
||||||
|
# elixir: "~> 1.8",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help compile.app" to learn about applications.
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger, :crypto]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help deps" to learn about dependencies.
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||||
|
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,116 @@
|
||||||
|
defmodule SimpleCipherTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
|
||||||
|
test "encoding with 'a' returns the original text" do
|
||||||
|
assert SimpleCipher.encode("a", "a") == "a"
|
||||||
|
assert SimpleCipher.encode("b", "a") == "b"
|
||||||
|
assert SimpleCipher.encode("c", "a") == "c"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "encoding with another key returns shifted text" do
|
||||||
|
assert SimpleCipher.encode("a", "d") == "d"
|
||||||
|
assert SimpleCipher.encode("b", "d") == "e"
|
||||||
|
assert SimpleCipher.encode("c", "d") == "f"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "decoding with 'a' returns the original text" do
|
||||||
|
assert SimpleCipher.decode("a", "a") == "a"
|
||||||
|
assert SimpleCipher.decode("b", "a") == "b"
|
||||||
|
assert SimpleCipher.decode("c", "a") == "c"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "decoding with another key returns unshifted text" do
|
||||||
|
assert SimpleCipher.decode("d", "d") == "a"
|
||||||
|
assert SimpleCipher.decode("e", "d") == "b"
|
||||||
|
assert SimpleCipher.decode("f", "d") == "c"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "key uses per-letter translation for encoding" do
|
||||||
|
key = "abc"
|
||||||
|
|
||||||
|
assert SimpleCipher.encode("abc", key) == "ace"
|
||||||
|
assert SimpleCipher.encode("bcd", key) == "bdf"
|
||||||
|
assert SimpleCipher.encode("cde", key) == "ceg"
|
||||||
|
assert SimpleCipher.encode("aaaaaaaaaa", "abcdefghij") == "abcdefghij"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "key uses per-letter translation for decoding" do
|
||||||
|
key = "abc"
|
||||||
|
|
||||||
|
assert SimpleCipher.decode("ace", key) == "abc"
|
||||||
|
assert SimpleCipher.decode("bdf", key) == "bcd"
|
||||||
|
assert SimpleCipher.decode("ceg", key) == "cde"
|
||||||
|
assert SimpleCipher.decode("abcdefghij", "abcdefghij") == "aaaaaaaaaa"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reversible" do
|
||||||
|
key = "abcdefghij"
|
||||||
|
plaintext = "abcdefghij"
|
||||||
|
|
||||||
|
assert plaintext |> SimpleCipher.encode(key) |> SimpleCipher.decode(key) == plaintext
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can double shift encode" do
|
||||||
|
assert SimpleCipher.encode("iamapandabear", "iamapandabear") == "qayaeaagaciai"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can wrap on encode" do
|
||||||
|
assert SimpleCipher.encode("zzzzzzzzzz", "abcdefghij") == "zabcdefghi"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can wrap on decode" do
|
||||||
|
assert SimpleCipher.decode("zabcdefghi", "abcdefghij") == "zzzzzzzzzz"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can encode messages longer than the key" do
|
||||||
|
assert SimpleCipher.encode("abc", "a") == "abc"
|
||||||
|
assert SimpleCipher.encode("abcdefghi", "abc") == "acedfhgik"
|
||||||
|
assert SimpleCipher.encode("iamapandabear", "abc") == "iboaqcnecbfcr"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can decode messages longer than the key" do
|
||||||
|
assert SimpleCipher.decode("abc", "a") == "abc"
|
||||||
|
assert SimpleCipher.decode("acedfhgik", "abc") == "abcdefghi"
|
||||||
|
assert SimpleCipher.decode("iboaqcnecbfcr", "abc") == "iamapandabear"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "if key is longer than text, only use as much as needed" do
|
||||||
|
key = "somewhatlongkey"
|
||||||
|
|
||||||
|
assert SimpleCipher.encode("abc", key) == "spo"
|
||||||
|
assert SimpleCipher.decode("abc", key) == "inq"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "if you know both the encoded and decoded text, you can figure out the key" do
|
||||||
|
key = "supersecretkey"
|
||||||
|
|
||||||
|
plaintext = "attackxatxdawn"
|
||||||
|
ciphertext = SimpleCipher.encode(plaintext, key)
|
||||||
|
|
||||||
|
assert SimpleCipher.decode(ciphertext, plaintext) == key
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can generate keys of given length" do
|
||||||
|
assert String.length(SimpleCipher.generate_key(16)) == 16
|
||||||
|
assert String.length(SimpleCipher.generate_key(128)) == 128
|
||||||
|
assert String.length(SimpleCipher.generate_key(256)) == 256
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generated keys are random" do
|
||||||
|
keys = Enum.map(0..100, fn _ -> SimpleCipher.generate_key(16) end)
|
||||||
|
assert keys == Enum.uniq(keys)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generated keys contain lowercase letters only" do
|
||||||
|
keys = Enum.map(0..100, fn _ -> SimpleCipher.generate_key(16) end)
|
||||||
|
assert Enum.all?(keys, &(&1 =~ ~r/^[a-z]*$/))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generated keys can be used to encode and decode text" do
|
||||||
|
Enum.each(0..100, fn _ ->
|
||||||
|
key = SimpleCipher.generate_key(16)
|
||||||
|
plaintext = "iamapandabear"
|
||||||
|
assert plaintext |> SimpleCipher.encode(key) |> SimpleCipher.decode(key) == plaintext
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,2 @@
|
||||||
|
ExUnit.start()
|
||||||
|
ExUnit.configure(exclude: :pending, trace: true)
|
Loading…
Reference in New Issue