From 2b764143c445dbc8fb11845135f4c67799a3ac1a Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Wed, 21 Aug 2024 20:23:30 -0400 Subject: [PATCH] simple_cipher --- elixir/simple-cipher/.exercism/config.json | 26 ++++ elixir/simple-cipher/.exercism/metadata.json | 1 + elixir/simple-cipher/.formatter.exs | 4 + elixir/simple-cipher/.gitignore | 24 ++++ elixir/simple-cipher/HELP.md | 75 +++++++++++ elixir/simple-cipher/README.md | 89 ++++++++++++++ elixir/simple-cipher/lib/simple_cipher.ex | 84 +++++++++++++ elixir/simple-cipher/mix.exs | 28 +++++ .../simple-cipher/test/simple_cipher_test.exs | 116 ++++++++++++++++++ elixir/simple-cipher/test/test_helper.exs | 2 + 10 files changed, 449 insertions(+) create mode 100644 elixir/simple-cipher/.exercism/config.json create mode 100644 elixir/simple-cipher/.exercism/metadata.json create mode 100644 elixir/simple-cipher/.formatter.exs create mode 100644 elixir/simple-cipher/.gitignore create mode 100644 elixir/simple-cipher/HELP.md create mode 100644 elixir/simple-cipher/README.md create mode 100644 elixir/simple-cipher/lib/simple_cipher.ex create mode 100644 elixir/simple-cipher/mix.exs create mode 100644 elixir/simple-cipher/test/simple_cipher_test.exs create mode 100644 elixir/simple-cipher/test/test_helper.exs diff --git a/elixir/simple-cipher/.exercism/config.json b/elixir/simple-cipher/.exercism/config.json new file mode 100644 index 0000000..81f7974 --- /dev/null +++ b/elixir/simple-cipher/.exercism/config.json @@ -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" +} diff --git a/elixir/simple-cipher/.exercism/metadata.json b/elixir/simple-cipher/.exercism/metadata.json new file mode 100644 index 0000000..f9ab182 --- /dev/null +++ b/elixir/simple-cipher/.exercism/metadata.json @@ -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} \ No newline at end of file diff --git a/elixir/simple-cipher/.formatter.exs b/elixir/simple-cipher/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/simple-cipher/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/simple-cipher/.gitignore b/elixir/simple-cipher/.gitignore new file mode 100644 index 0000000..a4c2317 --- /dev/null +++ b/elixir/simple-cipher/.gitignore @@ -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 + diff --git a/elixir/simple-cipher/HELP.md b/elixir/simple-cipher/HELP.md new file mode 100644 index 0000000..1be2e68 --- /dev/null +++ b/elixir/simple-cipher/HELP.md @@ -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/.exs:LINENUM` - runs only a single test, the test from `.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. \ No newline at end of file diff --git a/elixir/simple-cipher/README.md b/elixir/simple-cipher/README.md new file mode 100644 index 0000000..f41306e --- /dev/null +++ b/elixir/simple-cipher/README.md @@ -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 \ No newline at end of file diff --git a/elixir/simple-cipher/lib/simple_cipher.ex b/elixir/simple-cipher/lib/simple_cipher.ex new file mode 100644 index 0000000..220eae2 --- /dev/null +++ b/elixir/simple-cipher/lib/simple_cipher.ex @@ -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 diff --git a/elixir/simple-cipher/mix.exs b/elixir/simple-cipher/mix.exs new file mode 100644 index 0000000..b337be4 --- /dev/null +++ b/elixir/simple-cipher/mix.exs @@ -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 diff --git a/elixir/simple-cipher/test/simple_cipher_test.exs b/elixir/simple-cipher/test/simple_cipher_test.exs new file mode 100644 index 0000000..42d4117 --- /dev/null +++ b/elixir/simple-cipher/test/simple_cipher_test.exs @@ -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 diff --git a/elixir/simple-cipher/test/test_helper.exs b/elixir/simple-cipher/test/test_helper.exs new file mode 100644 index 0000000..35fc5bf --- /dev/null +++ b/elixir/simple-cipher/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true)