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