Skip to content

Commit 07539a7

Browse files
authored
[affine-cipher] Add new practice exercise (#2787)
1 parent d622504 commit 07539a7

10 files changed

Lines changed: 580 additions & 0 deletions

File tree

config.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,19 @@
11781178
"strings"
11791179
]
11801180
},
1181+
{
1182+
"slug": "affine-cipher",
1183+
"name": "Affine Cipher",
1184+
"uuid": "cf62a3c5-af1a-4d05-a004-11b35eb79442",
1185+
"practices": [],
1186+
"prerequisites": [],
1187+
"difficulty": 4,
1188+
"topics": [
1189+
"algorithms",
1190+
"cryptography",
1191+
"strings"
1192+
]
1193+
},
11811194
{
11821195
"slug": "all-your-base",
11831196
"name": "All Your Base",
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"authors": [
3+
"KasimKaizer"
4+
],
5+
"files": {
6+
"solution": [
7+
"affine_cipher.go"
8+
],
9+
"test": [
10+
"affine_cipher_test.go"
11+
],
12+
"example": [
13+
".meta/example.go"
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+
}
20+
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Package affinecipher contains tools that implement affine-cipher.
2+
package affinecipher
3+
4+
import (
5+
"errors"
6+
"strings"
7+
)
8+
9+
type operation int
10+
11+
const (
12+
encode operation = iota + 1
13+
decode
14+
)
15+
16+
const _totalAlphabets = 26 // total number of letters in alphabet.
17+
18+
// Encode encodes the provided message with the provided keys, using affine-cipher.
19+
func Encode(text string, a, b int) (string, error) {
20+
if gcd(a, _totalAlphabets) != 1 {
21+
return "", errors.New("affinecipher.Encode: a and b must be co-prime")
22+
}
23+
return cipher(encode, text, a, b)
24+
}
25+
26+
// Decode decodes the provided encoded message with the provided keys, using affine-cipher.
27+
func Decode(text string, a, b int) (string, error) {
28+
if gcd(a, _totalAlphabets) != 1 {
29+
return "", errors.New("affinecipher.Decode: a and b must be co-prime")
30+
}
31+
mmi := multiInv(a, _totalAlphabets)
32+
return cipher(decode, text, mmi, b)
33+
}
34+
35+
// cipher functions takes a operation, the text and as well as key values, and performs the
36+
// operation on the text using the provided keys.
37+
func cipher(op operation, text string, a, b int) (string, error) {
38+
text = strings.ToLower(text)
39+
var output strings.Builder
40+
accum := 0
41+
opFunc := operationFunc(op)
42+
for _, char := range []byte(text) {
43+
if !(char >= 'a' && char <= 'z') && // if not a letter and
44+
!(char >= '1' && char <= '9') { // if not a number
45+
continue
46+
}
47+
48+
// if its encoding then write a space every 5 letters.
49+
if op == encode && accum != 0 && (accum%5) == 0 {
50+
output.WriteByte(' ')
51+
}
52+
accum++
53+
54+
if char >= '1' && char <= '9' { // write numbers as it is.
55+
output.WriteByte(char)
56+
continue
57+
}
58+
output.WriteByte(opFunc(char, a, b))
59+
}
60+
return output.String(), nil
61+
}
62+
63+
// gcd function finds the greatest common divisor for the numbers passed into it.
64+
func gcd(a, b int) int {
65+
if a < b {
66+
a, b = b, a
67+
}
68+
for b > 0 {
69+
a, b = b, (a % b)
70+
}
71+
return a
72+
}
73+
74+
// multiInv finds the modular multiple inverse of the passed num and mod.
75+
func multiInv(num, mod int) int {
76+
t1, t2 := 0, 1
77+
a, b := num, mod
78+
79+
if a < b {
80+
a, b = b, a
81+
}
82+
83+
for b > 0 {
84+
t := t1 - (t2 * (a / b))
85+
a, b = b, (a % b)
86+
t1, t2 = t2, t
87+
}
88+
89+
return t1
90+
}
91+
92+
// operationFunc function returns the appropriate function for the passed operation.
93+
func operationFunc(op operation) func(byte, int, int) byte {
94+
if op == encode {
95+
return encryptionFunc
96+
}
97+
return decryptionFunc
98+
}
99+
100+
// encryptionFunc function is for encrypting text.
101+
func encryptionFunc(letter byte, num1, num2 int) byte {
102+
temp := ((num1 * int(letter-'a')) + num2) % _totalAlphabets
103+
return byte(temp) + 'a'
104+
}
105+
106+
// decryptionFunc function is for decrypting text.
107+
func decryptionFunc(letter byte, mmi, num2 int) byte {
108+
temp := (mmi * (int(letter-'a') - num2)) % _totalAlphabets
109+
if temp < 0 {
110+
temp += _totalAlphabets
111+
}
112+
return byte(temp) + 'a'
113+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"text/template"
6+
7+
"../../../../gen"
8+
)
9+
10+
func main() {
11+
t, err := template.New("").Parse(tmpl)
12+
if err != nil {
13+
log.Fatal(err)
14+
}
15+
j := map[string]interface{}{
16+
"encode": &[]testCase{},
17+
"decode": &[]testCase{},
18+
}
19+
if err := gen.Gen("affine-cipher", j, t); err != nil {
20+
log.Fatal(err)
21+
}
22+
}
23+
24+
type testCase struct {
25+
Description string `json:"description"`
26+
Input struct {
27+
Phrase string `json:"phrase"`
28+
Key struct {
29+
Num1 int `json:"a"`
30+
Num2 int `json:"b"`
31+
} `json:"key"`
32+
} `json:"input"`
33+
Expected interface{} `json:"expected"`
34+
}
35+
36+
func (t testCase) ExpectedString() string {
37+
m, ok := t.Expected.(string)
38+
if !ok {
39+
return ""
40+
}
41+
return m
42+
}
43+
44+
func (t testCase) Error() bool {
45+
m, ok := t.Expected.(map[string]interface{})
46+
if !ok {
47+
return ok
48+
}
49+
_, ok = m["error"].(string)
50+
return ok
51+
}
52+
53+
// Template to generate encode and decode test cases.
54+
var tmpl = `{{.Header}}
55+
56+
type testCase struct {
57+
description string
58+
inputPhrase string
59+
inputA int
60+
inputB int
61+
expectError bool
62+
expected string
63+
}
64+
65+
var encodeTests = []testCase{ {{range .J.encode}}
66+
{
67+
description: {{printf "%q" .Description}},
68+
inputPhrase: {{printf "%q" .Input.Phrase}},
69+
inputA: {{.Input.Key.Num1}},
70+
inputB: {{.Input.Key.Num2}},
71+
expectError: {{.Error}},
72+
expected: {{printf "%q" .ExpectedString}},
73+
},
74+
{{end}}
75+
}
76+
77+
var decodeTests = []testCase{ {{range .J.decode}}
78+
{
79+
description: {{printf "%q" .Description}},
80+
inputPhrase: {{printf "%q" .Input.Phrase}},
81+
inputA: {{.Input.Key.Num1}},
82+
inputB: {{.Input.Key.Num2}},
83+
expectError: {{.Error}},
84+
expected: {{printf "%q" .ExpectedString}},
85+
},
86+
{{end}}
87+
}
88+
`
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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package affinecipher
2+
3+
func Encode(text string, a, b int) (string, error) {
4+
panic("Please implement the Encode function")
5+
}
6+
7+
func Decode(text string, a, b int) (string, error) {
8+
panic("Please implement the Decode function")
9+
}

0 commit comments

Comments
 (0)