Skip to content

Commit 29653fc

Browse files
authored
Add affine-cipher (#136)
1 parent 10ba0a9 commit 29653fc

9 files changed

Lines changed: 310 additions & 0 deletions

File tree

config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,14 @@
618618
"prerequisites": [],
619619
"difficulty": 4
620620
},
621+
{
622+
"slug": "affine-cipher",
623+
"name": "Affine Cipher",
624+
"uuid": "b9d2eda3-0e01-4f08-b8e3-b5fd19759d5e",
625+
"practices": [],
626+
"prerequisites": [],
627+
"difficulty": 5
628+
},
621629
{
622630
"slug": "binary-search-tree",
623631
"name": "Binary Search Tree",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
return {
2+
default = {
3+
ROOT = { '.' }
4+
}
5+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Instructions
2+
3+
Create an implementation of the affine cipher, an ancient encryption system created in the Middle East.
4+
5+
The affine cipher is a type of monoalphabetic substitution cipher.
6+
Each character is mapped to its numeric equivalent, encrypted with a mathematical function and then converted to the letter relating to its new numeric value.
7+
Although all monoalphabetic ciphers are weak, the affine cipher is much stronger than the Atbash cipher, because it has many more keys.
8+
9+
[//]: # " monoalphabetic as spelled by Merriam-Webster, compare to polyalphabetic "
10+
11+
## Encryption
12+
13+
The encryption function is:
14+
15+
```text
16+
E(x) = (ai + b) mod m
17+
```
18+
19+
Where:
20+
21+
- `i` is the letter's index from `0` to the length of the alphabet - 1.
22+
- `m` is the length of the alphabet.
23+
For the Latin alphabet `m` is `26`.
24+
- `a` and `b` are integers which make up the encryption key.
25+
26+
Values `a` and `m` must be _coprime_ (or, _relatively prime_) for automatic decryption to succeed, i.e., they have number `1` as their only common factor (more information can be found in the [Wikipedia article about coprime integers][coprime-integers]).
27+
In case `a` is not coprime to `m`, your program should indicate that this is an error.
28+
Otherwise it should encrypt or decrypt with the provided key.
29+
30+
For the purpose of this exercise, digits are valid input but they are not encrypted.
31+
Spaces and punctuation characters are excluded.
32+
Ciphertext is written out in groups of fixed length separated by space, the traditional group size being `5` letters.
33+
This is to make it harder to guess encrypted text based on word boundaries.
34+
35+
## Decryption
36+
37+
The decryption function is:
38+
39+
```text
40+
D(y) = (a^-1)(y - b) mod m
41+
```
42+
43+
Where:
44+
45+
- `y` is the numeric value of an encrypted letter, i.e., `y = E(x)`
46+
- it is important to note that `a^-1` is the modular multiplicative inverse (MMI) of `a mod m`
47+
- the modular multiplicative inverse only exists if `a` and `m` are coprime.
48+
49+
The MMI of `a` is `x` such that the remainder after dividing `ax` by `m` is `1`:
50+
51+
```text
52+
ax mod m = 1
53+
```
54+
55+
More information regarding how to find a Modular Multiplicative Inverse and what it means can be found in the [related Wikipedia article][mmi].
56+
57+
## General Examples
58+
59+
- Encrypting `"test"` gives `"ybty"` with the key `a = 5`, `b = 7`
60+
- Decrypting `"ybty"` gives `"test"` with the key `a = 5`, `b = 7`
61+
- Decrypting `"ybty"` gives `"lqul"` with the wrong key `a = 11`, `b = 7`
62+
- Decrypting `"kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx"` gives `"thequickbrownfoxjumpsoverthelazydog"` with the key `a = 19`, `b = 13`
63+
- Encrypting `"test"` with the key `a = 18`, `b = 13` is an error because `18` and `26` are not coprime
64+
65+
## Example of finding a Modular Multiplicative Inverse (MMI)
66+
67+
Finding MMI for `a = 15`:
68+
69+
- `(15 * x) mod 26 = 1`
70+
- `(15 * 7) mod 26 = 1`, ie. `105 mod 26 = 1`
71+
- `7` is the MMI of `15 mod 26`
72+
73+
[mmi]: https://en.wikipedia.org/wiki/Modular_multiplicative_inverse
74+
[coprime-integers]: https://en.wikipedia.org/wiki/Coprime_integers
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"authors": [
3+
"glennj"
4+
],
5+
"files": {
6+
"solution": [
7+
"affine_cipher.moon"
8+
],
9+
"test": [
10+
"affine_cipher_spec.moon"
11+
],
12+
"example": [
13+
".meta/example.moon"
14+
]
15+
},
16+
"blurb": "Create an implementation of the Affine cipher, an ancient encryption algorithm from the Middle East.",
17+
"source": "Wikipedia",
18+
"source_url": "https://en.wikipedia.org/wiki/Affine_cipher"
19+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
M = 26 -- size of alphabet
2+
3+
gcd = (a, b) ->
4+
while b != 0
5+
a, b = b, a % b
6+
a
7+
8+
mmi = (a, m) ->
9+
for x = 1, m
10+
return x if (a * x) % m == 1
11+
error 'should not happen: cannot determine MMI of #{a} and #{m}'
12+
13+
ord = (letter) -> letter\byte!
14+
chr = (number) -> string.char number
15+
A = ord 'a'
16+
17+
validate = (a, m) ->
18+
assert gcd(a, m) == 1, 'a and m must be coprime.'
19+
20+
add_spaces = (str, n = 5) ->
21+
str\gsub('.'\rep(n), '%0 ')\gsub(' $', '')
22+
23+
encipher = (text, func) ->
24+
encipherer = (c) -> c\match('%d') and c or chr(A + func(ord(c) - A))
25+
table.concat [encipherer c for c in text\lower!\gmatch '%w']
26+
27+
28+
{
29+
encode: (phrase, key) ->
30+
validate key.a, M
31+
encoder = (x) -> (key.a * x + key.b) % M
32+
add_spaces encipher(phrase, encoder)
33+
34+
decode: (phrase, key) ->
35+
validate key.a, M
36+
a_prime = mmi key.a, M
37+
decoder = (y) -> (a_prime * (y - key.b)) % M
38+
encipher(phrase, decoder)
39+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
module_imports: {'encode', 'decode'},
3+
4+
generate_test: (case, level) ->
5+
local lines
6+
if type(case.expected) == 'string'
7+
lines = {
8+
"result = #{case.property} #{quote case.input.phrase}, {a: #{case.input.key.a}, b: #{case.input.key.b}}",
9+
"expected = #{quote case.expected}",
10+
"assert.are.equal expected, result"
11+
}
12+
else
13+
lines = {
14+
"f = -> #{case.property} #{quote case.input.phrase}, {a: #{case.input.key.a}, b: #{case.input.key.b}}",
15+
"assert.has.error f, #{quote case.expected.error}"
16+
}
17+
table.concat [indent line, level for line in *lines], '\n'
18+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[2ee1d9af-1c43-416c-b41b-cefd7d4d2b2a]
13+
description = "encode -> encode yes"
14+
15+
[785bade9-e98b-4d4f-a5b0-087ba3d7de4b]
16+
description = "encode -> encode no"
17+
18+
[2854851c-48fb-40d8-9bf6-8f192ed25054]
19+
description = "encode -> encode OMG"
20+
21+
[bc0c1244-b544-49dd-9777-13a770be1bad]
22+
description = "encode -> encode O M G"
23+
24+
[381a1a20-b74a-46ce-9277-3778625c9e27]
25+
description = "encode -> encode mindblowingly"
26+
27+
[6686f4e2-753b-47d4-9715-876fdc59029d]
28+
description = "encode -> encode numbers"
29+
30+
[ae23d5bd-30a8-44b6-afbe-23c8c0c7faa3]
31+
description = "encode -> encode deep thought"
32+
33+
[c93a8a4d-426c-42ef-9610-76ded6f7ef57]
34+
description = "encode -> encode all the letters"
35+
36+
[0673638a-4375-40bd-871c-fb6a2c28effb]
37+
description = "encode -> encode with a not coprime to m"
38+
39+
[3f0ac7e2-ec0e-4a79-949e-95e414953438]
40+
description = "decode -> decode exercism"
41+
42+
[241ee64d-5a47-4092-a5d7-7939d259e077]
43+
description = "decode -> decode a sentence"
44+
45+
[33fb16a1-765a-496f-907f-12e644837f5e]
46+
description = "decode -> decode numbers"
47+
48+
[20bc9dce-c5ec-4db6-a3f1-845c776bcbf7]
49+
description = "decode -> decode all the letters"
50+
51+
[623e78c0-922d-49c5-8702-227a3e8eaf81]
52+
description = "decode -> decode with no spaces in input"
53+
54+
[58fd5c2a-1fd9-4563-a80a-71cff200f26f]
55+
description = "decode -> decode with too many spaces"
56+
57+
[b004626f-c186-4af9-a3f4-58f74cdb86d5]
58+
description = "decode -> decode with a not coprime to m"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
encode: (phrase, key) ->
3+
error 'Implement the encode function'
4+
5+
decode: (phrase, key) ->
6+
error 'Implement the decode function'
7+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import encode, decode from require 'affine_cipher'
2+
3+
describe 'affine-cipher', ->
4+
describe 'encode', ->
5+
it 'encode yes', ->
6+
result = encode 'yes', {a: 5, b: 7}
7+
expected = 'xbt'
8+
assert.are.equal expected, result
9+
10+
pending 'encode no', ->
11+
result = encode 'no', {a: 15, b: 18}
12+
expected = 'fu'
13+
assert.are.equal expected, result
14+
15+
pending 'encode OMG', ->
16+
result = encode 'OMG', {a: 21, b: 3}
17+
expected = 'lvz'
18+
assert.are.equal expected, result
19+
20+
pending 'encode O M G', ->
21+
result = encode 'O M G', {a: 25, b: 47}
22+
expected = 'hjp'
23+
assert.are.equal expected, result
24+
25+
pending 'encode mindblowingly', ->
26+
result = encode 'mindblowingly', {a: 11, b: 15}
27+
expected = 'rzcwa gnxzc dgt'
28+
assert.are.equal expected, result
29+
30+
pending 'encode numbers', ->
31+
result = encode 'Testing,1 2 3, testing.', {a: 3, b: 4}
32+
expected = 'jqgjc rw123 jqgjc rw'
33+
assert.are.equal expected, result
34+
35+
pending 'encode deep thought', ->
36+
result = encode 'Truth is fiction.', {a: 5, b: 17}
37+
expected = 'iynia fdqfb ifje'
38+
assert.are.equal expected, result
39+
40+
pending 'encode all the letters', ->
41+
result = encode 'The quick brown fox jumps over the lazy dog.', {a: 17, b: 33}
42+
expected = 'swxtj npvyk lruol iejdc blaxk swxmh qzglf'
43+
assert.are.equal expected, result
44+
45+
pending 'encode with a not coprime to m', ->
46+
f = -> encode 'This is a test.', {a: 6, b: 17}
47+
assert.has.error f, 'a and m must be coprime.'
48+
49+
describe 'decode', ->
50+
pending 'decode exercism', ->
51+
result = decode 'tytgn fjr', {a: 3, b: 7}
52+
expected = 'exercism'
53+
assert.are.equal expected, result
54+
55+
pending 'decode a sentence', ->
56+
result = decode 'qdwju nqcro muwhn odqun oppmd aunwd o', {a: 19, b: 16}
57+
expected = 'anobstacleisoftenasteppingstone'
58+
assert.are.equal expected, result
59+
60+
pending 'decode numbers', ->
61+
result = decode 'odpoz ub123 odpoz ub', {a: 25, b: 7}
62+
expected = 'testing123testing'
63+
assert.are.equal expected, result
64+
65+
pending 'decode all the letters', ->
66+
result = decode 'swxtj npvyk lruol iejdc blaxk swxmh qzglf', {a: 17, b: 33}
67+
expected = 'thequickbrownfoxjumpsoverthelazydog'
68+
assert.are.equal expected, result
69+
70+
pending 'decode with no spaces in input', ->
71+
result = decode 'swxtjnpvyklruoliejdcblaxkswxmhqzglf', {a: 17, b: 33}
72+
expected = 'thequickbrownfoxjumpsoverthelazydog'
73+
assert.are.equal expected, result
74+
75+
pending 'decode with too many spaces', ->
76+
result = decode 'vszzm cly yd cg qdp', {a: 15, b: 16}
77+
expected = 'jollygreengiant'
78+
assert.are.equal expected, result
79+
80+
pending 'decode with a not coprime to m', ->
81+
f = -> decode 'Test', {a: 13, b: 5}
82+
assert.has.error f, 'a and m must be coprime.'

0 commit comments

Comments
 (0)