Skip to content

Commit d9b7fcc

Browse files
author
Quentin Ménoret
committed
Solution for simple cypher
1 parent 4cf7d0d commit d9b7fcc

File tree

6 files changed

+291
-0
lines changed

6 files changed

+291
-0
lines changed

simple-cipher/README.md

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Simple Cipher
2+
3+
Implement a simple shift cipher like Caesar and a more secure substitution cipher.
4+
5+
## Step 1
6+
7+
"If he had anything confidential to say, he wrote it in cipher, that is,
8+
by so changing the order of the letters of the alphabet, that not a word
9+
could be made out. If anyone wishes to decipher these, and get at their
10+
meaning, he must substitute the fourth letter of the alphabet, namely D,
11+
for A, and so with the others."
12+
—Suetonius, Life of Julius Caesar
13+
14+
Ciphers are very straight-forward algorithms that allow us to render
15+
text less readable while still allowing easy deciphering. They are
16+
vulnerable to many forms of cryptoanalysis, but we are lucky that
17+
generally our little sisters are not cryptoanalysts.
18+
19+
The Caesar Cipher was used for some messages from Julius Caesar that
20+
were sent afield. Now Caesar knew that the cipher wasn't very good, but
21+
he had one ally in that respect: almost nobody could read well. So even
22+
being a couple letters off was sufficient so that people couldn't
23+
recognize the few words that they did know.
24+
25+
Your task is to create a simple shift cipher like the Caesar Cipher.
26+
This image is a great example of the Caesar Cipher:
27+
28+
![Caesar Cipher][1]
29+
30+
For example:
31+
32+
Giving "iamapandabear" as input to the encode function returns the cipher "ldpdsdqgdehdu". Obscure enough to keep our message secret in transit.
33+
34+
When "ldpdsdqgdehdu" is put into the decode function it would return
35+
the original "iamapandabear" letting your friend read your original
36+
message.
37+
38+
## Step 2
39+
40+
Shift ciphers are no fun though when your kid sister figures it out. Try
41+
amending the code to allow us to specify a key and use that for the
42+
shift distance. This is called a substitution cipher.
43+
44+
Here's an example:
45+
46+
Given the key "aaaaaaaaaaaaaaaaaa", encoding the string "iamapandabear"
47+
would return the original "iamapandabear".
48+
49+
Given the key "ddddddddddddddddd", encoding our string "iamapandabear"
50+
would return the obscured "lpdsdqgdehdu"
51+
52+
In the example above, we've set a = 0 for the key value. So when the
53+
plaintext is added to the key, we end up with the same message coming
54+
out. So "aaaa" is not an ideal key. But if we set the key to "dddd", we
55+
would get the same thing as the Caesar Cipher.
56+
57+
## Step 3
58+
59+
The weakest link in any cipher is the human being. Let's make your
60+
substitution cipher a little more fault tolerant by providing a source
61+
of randomness and ensuring that the key is not composed of numbers or
62+
capital letters.
63+
64+
If someone doesn't submit a key at all, generate a truly random key of
65+
at least 100 characters in length, accessible via Cipher#key (the #
66+
syntax means instance variable)
67+
68+
If the key submitted has capital letters or numbers, throw an
69+
ArgumentError with a message to that effect.
70+
71+
## Extensions
72+
73+
Shift ciphers work by making the text slightly odd, but are vulnerable
74+
to frequency analysis. Substitution ciphers help that, but are still
75+
very vulnerable when the key is short or if spaces are preserved. Later
76+
on you'll see one solution to this problem in the exercise
77+
"crypto-square".
78+
79+
If you want to go farther in this field, the questions begin to be about
80+
how we can exchange keys in a secure way. Take a look at [Diffie-Hellman
81+
on Wikipedia][dh] for one of the first implementations of this scheme.
82+
83+
[1]: 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
84+
[dh]: http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
85+
86+
87+
## Getting Started
88+
89+
For installation and learning resources, refer to the
90+
[exercism help page](http://exercism.io/languages/haskell).
91+
92+
## Running the tests
93+
94+
To run the test suite, execute the following command:
95+
96+
```bash
97+
stack test
98+
```
99+
100+
#### If you get an error message like this...
101+
102+
```
103+
No .cabal file found in directory
104+
```
105+
106+
You are probably running an old stack version and need
107+
to upgrade it.
108+
109+
#### Otherwise, if you get an error message like this...
110+
111+
```
112+
No compiler found, expected minor version match with...
113+
Try running "stack setup" to install the correct GHC...
114+
```
115+
116+
Just do as it says and it will download and install
117+
the correct compiler version:
118+
119+
```bash
120+
stack setup
121+
```
122+
123+
## Running *GHCi*
124+
125+
If you want to play with your solution in GHCi, just run the command:
126+
127+
```bash
128+
stack ghci
129+
```
130+
131+
## Feedback, Issues, Pull Requests
132+
133+
The [exercism/xhaskell](https://github.com/exercism/xhaskell) repository on
134+
GitHub is the home for all of the Haskell exercises.
135+
136+
If you have feedback about an exercise, or want to help implementing a new
137+
one, head over there and create an issue. We'll do our best to help you!
138+
139+
## Source
140+
141+
Substitution Cipher at Wikipedia [http://en.wikipedia.org/wiki/Substitution_cipher](http://en.wikipedia.org/wiki/Substitution_cipher)
142+
143+
## Submitting Incomplete Solutions
144+
It's possible to submit an incomplete solution so you can see how others have completed the exercise.
145+

simple-cipher/package.yaml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: simple-cipher
2+
version: 0.1.0.2
3+
4+
dependencies:
5+
- base
6+
7+
library:
8+
exposed-modules: Cipher
9+
source-dirs: src
10+
dependencies:
11+
- random
12+
# - foo # List here the packages you
13+
# - bar # want to use in your solution.
14+
15+
tests:
16+
test:
17+
main: Tests.hs
18+
source-dirs: test
19+
dependencies:
20+
- simple-cipher
21+
- hspec

simple-cipher/simple-cipher.cabal

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-- This file has been generated from package.yaml by hpack version 0.15.0.
2+
--
3+
-- see: https://github.com/sol/hpack
4+
5+
name: simple-cipher
6+
version: 0.1.0.2
7+
build-type: Simple
8+
cabal-version: >= 1.10
9+
10+
library
11+
hs-source-dirs:
12+
src
13+
build-depends:
14+
base
15+
, random
16+
exposed-modules:
17+
Cipher
18+
other-modules:
19+
Paths_simple_cipher
20+
default-language: Haskell2010
21+
22+
test-suite test
23+
type: exitcode-stdio-1.0
24+
main-is: Tests.hs
25+
hs-source-dirs:
26+
test
27+
build-depends:
28+
base
29+
, simple-cipher
30+
, hspec
31+
default-language: Haskell2010

simple-cipher/src/Cipher.hs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module Cipher (caesarDecode, caesarEncode, caesarEncodeRandom) where
2+
3+
import Data.Char (toLower, isLower)
4+
import System.Random
5+
6+
caesarDecode :: String -> String -> String
7+
caesarDecode a b = map r $ zipLoop a b
8+
where
9+
r (x, y) = (toEnum $ fromEnum 'a' + diff x y) :: Char
10+
diff x y = (26 + toPos y - toPos x) `rem` 26
11+
12+
caesarEncode :: String -> String -> String
13+
caesarEncode a b = map r $ zipLoop a b
14+
where
15+
r (x, y) = (toEnum $ fromEnum 'a' + diff x y) :: Char
16+
diff x y = (toPos x + toPos y) `rem` 26
17+
18+
toPos :: Char -> Int
19+
toPos a = (fromEnum . toLower) a - 97
20+
21+
zipLoop :: [a] -> [b] -> [(a, b)]
22+
zipLoop short long = r short long
23+
where
24+
r _ [] = []
25+
r [] a = r short a
26+
r (x:xs) (y:ys) = (x,y):r xs ys
27+
28+
caesarEncodeRandom :: String -> IO (String, String)
29+
caesarEncodeRandom text = do
30+
key <- generateStr 1000
31+
return (key, caesarEncode key text)
32+
33+
generateStr :: Int -> IO String
34+
generateStr 0 = return ""
35+
generateStr len = (:) <$> generateChar <*> generateStr (len - 1)
36+
where
37+
generateChar :: IO Char
38+
generateChar = toEnum <$> getStdRandom (randomR (97, 122))

simple-cipher/stack.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
resolver: lts-8.2

simple-cipher/test/Tests.hs

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Test.Hspec (Spec, it, shouldBe)
2+
import Test.Hspec.Runner (configFastFail, defaultConfig, hspecWith)
3+
4+
import Cipher (caesarDecode, caesarEncode, caesarEncodeRandom)
5+
6+
main :: IO ()
7+
main = hspecWith defaultConfig {configFastFail = True} specs
8+
9+
specs :: Spec
10+
specs = do
11+
12+
it "no-op encode" $ do
13+
caesarEncode "a" ['a'..'z'] `shouldBe` ['a'..'z']
14+
caesarEncode (repeat 'a') ['a'..'z'] `shouldBe` ['a'..'z']
15+
16+
it "no-op decode" $ do
17+
caesarDecode "a" ['a'..'z'] `shouldBe` ['a'..'z']
18+
caesarDecode (repeat 'a') ['a'..'z'] `shouldBe` ['a'..'z']
19+
20+
it "reversible" $ do
21+
let k0 = "alkjsdhflkjahsuid"
22+
k1 = ['z','y'..'a']
23+
caesarDecode k0 (caesarEncode k0 "asdf") `shouldBe` "asdf"
24+
caesarDecode k1 (caesarEncode k1 "asdf") `shouldBe` "asdf"
25+
26+
it "known cipher" $ do
27+
let k = ['a'..'j']
28+
encode = caesarEncode k
29+
decode = caesarDecode k
30+
encode "aaaaaaaaaa" `shouldBe` k
31+
decode k `shouldBe` "aaaaaaaaaa"
32+
decode (encode ['a'..'z']) `shouldBe` ['a'..'z']
33+
encode "zzzzzzzzzz" `shouldBe` 'z':['a'..'i']
34+
35+
it "double shift" $ do
36+
let plaintext = "iamapandabear"
37+
ciphertext = "qayaeaagaciai"
38+
caesarEncode plaintext plaintext `shouldBe` ciphertext
39+
40+
it "shift cipher" $ do
41+
let encode = caesarEncode "d"
42+
decode = caesarDecode "d"
43+
encode "aaaaaaaaaa" `shouldBe` "dddddddddd"
44+
decode "dddddddddd" `shouldBe` "aaaaaaaaaa"
45+
encode ['a'..'j'] `shouldBe` ['d'..'m']
46+
decode (encode ['a'..'j']) `shouldBe` ['a'..'j']
47+
48+
it "random tests" $ do
49+
let plaintext = take 1000 (cycle ['a'..'z'])
50+
p1 <- caesarEncodeRandom plaintext
51+
uncurry caesarDecode p1 `shouldBe` plaintext
52+
p2 <- caesarEncodeRandom plaintext
53+
uncurry caesarDecode p2 `shouldBe` plaintext
54+
-- There's a small chance this could fail, since it's random.
55+
(p1 == p2) `shouldBe` False

0 commit comments

Comments
 (0)