From fe592be5d114c85bfe1d85db772155e714435264 Mon Sep 17 00:00:00 2001 From: glaxxie <86179463+glaxxie@users.noreply.github.com> Date: Wed, 13 Dec 2023 02:11:37 -0600 Subject: [PATCH] [New Exercise]: Affine Cipher --- config.json | 8 ++ .../affine-cipher/.docs/instructions.md | 74 +++++++++++ .../.meta/AffineCipher.example.ps1 | 113 +++++++++++++++++ .../practice/affine-cipher/.meta/config.json | 19 +++ .../practice/affine-cipher/.meta/tests.toml | 58 +++++++++ .../practice/affine-cipher/AffineCipher.ps1 | 61 +++++++++ .../affine-cipher/AffineCipher.tests.ps1 | 117 ++++++++++++++++++ 7 files changed, 450 insertions(+) create mode 100644 exercises/practice/affine-cipher/.docs/instructions.md create mode 100644 exercises/practice/affine-cipher/.meta/AffineCipher.example.ps1 create mode 100644 exercises/practice/affine-cipher/.meta/config.json create mode 100644 exercises/practice/affine-cipher/.meta/tests.toml create mode 100644 exercises/practice/affine-cipher/AffineCipher.ps1 create mode 100644 exercises/practice/affine-cipher/AffineCipher.tests.ps1 diff --git a/config.json b/config.json index 1553bde8..4e5ac996 100644 --- a/config.json +++ b/config.json @@ -880,6 +880,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "affine-cipher", + "name": "Affine Cipher", + "uuid": "0db7efd5-2ece-481a-b706-0e011aad52aa", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "markdown", "name": "Markdown", diff --git a/exercises/practice/affine-cipher/.docs/instructions.md b/exercises/practice/affine-cipher/.docs/instructions.md new file mode 100644 index 00000000..2ad6d152 --- /dev/null +++ b/exercises/practice/affine-cipher/.docs/instructions.md @@ -0,0 +1,74 @@ +# Instructions + +Create an implementation of the affine cipher, an ancient encryption system created in the Middle East. + +The affine cipher is a type of monoalphabetic substitution cipher. +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. +Although all monoalphabetic ciphers are weak, the affine cipher is much stronger than the atbash cipher, because it has many more keys. + +[//]: # ( monoalphabetic as spelled by Merriam-Webster, compare to polyalphabetic ) + +## Encryption + +The encryption function is: + +```text +E(x) = (ai + b) mod m +``` + +Where: + +- `i` is the letter's index from `0` to the length of the alphabet - 1 +- `m` is the length of the alphabet. + For the Roman alphabet `m` is `26`. +- `a` and `b` are integers which make the encryption key + +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]). +In case `a` is not coprime to `m`, your program should indicate that this is an error. +Otherwise it should encrypt or decrypt with the provided key. + +For the purpose of this exercise, digits are valid input but they are not encrypted. +Spaces and punctuation characters are excluded. +Ciphertext is written out in groups of fixed length separated by space, the traditional group size being `5` letters. +This is to make it harder to guess encrypted text based on word boundaries. + +## Decryption + +The decryption function is: + +```text +D(y) = (a^-1)(y - b) mod m +``` + +Where: + +- `y` is the numeric value of an encrypted letter, i.e., `y = E(x)` +- it is important to note that `a^-1` is the modular multiplicative inverse (MMI) of `a mod m` +- the modular multiplicative inverse only exists if `a` and `m` are coprime. + +The MMI of `a` is `x` such that the remainder after dividing `ax` by `m` is `1`: + +```text +ax mod m = 1 +``` + +More information regarding how to find a Modular Multiplicative Inverse and what it means can be found in the [related Wikipedia article][mmi]. + +## General Examples + +- Encrypting `"test"` gives `"ybty"` with the key `a = 5`, `b = 7` +- Decrypting `"ybty"` gives `"test"` with the key `a = 5`, `b = 7` +- Decrypting `"ybty"` gives `"lqul"` with the wrong key `a = 11`, `b = 7` +- Decrypting `"kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx"` gives `"thequickbrownfoxjumpsoverthelazydog"` with the key `a = 19`, `b = 13` +- Encrypting `"test"` with the key `a = 18`, `b = 13` is an error because `18` and `26` are not coprime + +## Example of finding a Modular Multiplicative Inverse (MMI) + +Finding MMI for `a = 15`: + +- `(15 * x) mod 26 = 1` +- `(15 * 7) mod 26 = 1`, ie. `105 mod 26 = 1` +- `7` is the MMI of `15 mod 26` + +[mmi]: https://en.wikipedia.org/wiki/Modular_multiplicative_inverse +[coprime-integers]: https://en.wikipedia.org/wiki/Coprime_integers diff --git a/exercises/practice/affine-cipher/.meta/AffineCipher.example.ps1 b/exercises/practice/affine-cipher/.meta/AffineCipher.example.ps1 new file mode 100644 index 00000000..ded2b7cc --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/AffineCipher.example.ps1 @@ -0,0 +1,113 @@ +Function Invoke-Encode() { + <# + .SYNOPSIS + Use the affine cipher, an ancient encryption system created in the Middle East to encrypt text. + + .DESCRIPTION + The encryption function is: E(x) = (ai + b) mod m + + i is the letter's index from 0 to the length of the alphabet - 1 + m is the length of the alphabet. For the Roman alphabet m is 26. + a and b are integers which make the encryption key + Values a and m must be coprime, if not you should throw error. + + .PARAMETER Plaintext + The text to be encrypted. + + .PARAMETER Keys + A hashtable contain the pair of keys `a` and `b`. + + .EXAMPLE + Invoke-Encode -Plaintext "test" -Keys @{a = 5; b = 7} + Returns: "ybty" + #> + [CmdletBinding()] + Param( + [string]$Plaintext, + [hashtable]$Keys + ) + $alphabets = 'a'..'z' + $m = $alphabets.Length + $gcd , $ex, $_ = ExtendedEuclidean $Keys.a $m + + if ($gcd -ne 1) { + Throw "a and m must be coprime" + } + + $charsArray = $Plaintext.ToCharArray() | Where-Object {$_ -match "\w"} | ForEach-Object { + if ($_ -match "[a-z]") { + $i = $alphabets.IndexOf([char]::ToLower($_)) + $x = ($Keys.a * $i + $Keys.b) % $m + $alphabets[$x] + }else { + $_ + } + } + + (-join $charsArray -split '(\w{5})' | Where-Object {$_}) -join " " +} + +Function Invoke-Decode() { + <# + .SYNOPSIS + Use the affine cipher, an ancient encryption system created in the Middle East to decrypt ciphertext. + + .DESCRIPTION + The decryption function is: D(y) = (a^-1)(y - b) mod m + + y is the numeric value of an encrypted letter, i.e., y = E(x) + it is important to note that a^-1 is the modular multiplicative inverse (MMI) of a mod m + the modular multiplicative inverse only exists if a and m are coprime, if they are not you should throw error. + The MMI of a is x such that the remainder after dividing ax by m is 1: ax mod m = 1 + + .PARAMETER Ciphertext + The text to be decrypted. + + .PARAMETER Keys + A hashtable contain the pair of keys `a` and `b`. + + .EXAMPLE + Invoke-Decode -Ciphertext "ybty" -Keys @{a = 5; b = 7} + Returns: "test" + #> + [CmdletBinding()] + Param( + [string]$Ciphertext, + [hashtable]$Keys + ) + $alphabets = 'a'..'z' + $m = $alphabets.Length + + $gcd , $ex, $_ = ExtendedEuclidean $Keys.a $m + + if ($gcd -ne 1) { + Throw "a and m must be coprime" + }else { + $MMI = ($ex % $m + $m) % $m + } + + $charsArray = $Ciphertext.ToCharArray() | ForEach-Object { + if ($_ -match "[a-z]") { + $y = $alphabets.IndexOf($_) + $x = ($MMI * ($y - $Keys.b)) % $m + $alphabets[$x] + } elseif ($_ -match "\d") { + $_ + } + } + + -join $charsArray +} + +function ExtendedEuclidean($a, $b) { + if ($a -eq 0) { + return $b, 0, 1 + } + + $gcd , $x, $y = ExtendedEuclidean ($b % $a) $a + $x1, $y1 = $x, $y + + $x = $y1 - [math]::Floor($b / $a) * $x1 + $y = $x1 + return $gcd , $x, $y +} \ No newline at end of file diff --git a/exercises/practice/affine-cipher/.meta/config.json b/exercises/practice/affine-cipher/.meta/config.json new file mode 100644 index 00000000..44dc1c37 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glaxxie" + ], + "files": { + "solution": [ + "AffineCipher.ps1" + ], + "test": [ + "AffineCipher.tests.ps1" + ], + "example": [ + ".meta/AffineCipher.example.ps1" + ] + }, + "blurb": "Create an implementation of the Affine cipher, an ancient encryption algorithm from the Middle East.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Affine_cipher" +} diff --git a/exercises/practice/affine-cipher/.meta/tests.toml b/exercises/practice/affine-cipher/.meta/tests.toml new file mode 100644 index 00000000..07cce7c7 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/tests.toml @@ -0,0 +1,58 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[2ee1d9af-1c43-416c-b41b-cefd7d4d2b2a] +description = "encode -> encode yes" + +[785bade9-e98b-4d4f-a5b0-087ba3d7de4b] +description = "encode -> encode no" + +[2854851c-48fb-40d8-9bf6-8f192ed25054] +description = "encode -> encode OMG" + +[bc0c1244-b544-49dd-9777-13a770be1bad] +description = "encode -> encode O M G" + +[381a1a20-b74a-46ce-9277-3778625c9e27] +description = "encode -> encode mindblowingly" + +[6686f4e2-753b-47d4-9715-876fdc59029d] +description = "encode -> encode numbers" + +[ae23d5bd-30a8-44b6-afbe-23c8c0c7faa3] +description = "encode -> encode deep thought" + +[c93a8a4d-426c-42ef-9610-76ded6f7ef57] +description = "encode -> encode all the letters" + +[0673638a-4375-40bd-871c-fb6a2c28effb] +description = "encode -> encode with a not coprime to m" + +[3f0ac7e2-ec0e-4a79-949e-95e414953438] +description = "decode -> decode exercism" + +[241ee64d-5a47-4092-a5d7-7939d259e077] +description = "decode -> decode a sentence" + +[33fb16a1-765a-496f-907f-12e644837f5e] +description = "decode -> decode numbers" + +[20bc9dce-c5ec-4db6-a3f1-845c776bcbf7] +description = "decode -> decode all the letters" + +[623e78c0-922d-49c5-8702-227a3e8eaf81] +description = "decode -> decode with no spaces in input" + +[58fd5c2a-1fd9-4563-a80a-71cff200f26f] +description = "decode -> decode with too many spaces" + +[b004626f-c186-4af9-a3f4-58f74cdb86d5] +description = "decode -> decode with a not coprime to m" diff --git a/exercises/practice/affine-cipher/AffineCipher.ps1 b/exercises/practice/affine-cipher/AffineCipher.ps1 new file mode 100644 index 00000000..08abd1ef --- /dev/null +++ b/exercises/practice/affine-cipher/AffineCipher.ps1 @@ -0,0 +1,61 @@ +Function Invoke-Encode() { + <# + .SYNOPSIS + Use the affine cipher, an ancient encryption system created in the Middle East to encrypt text. + + .DESCRIPTION + The encryption function is: E(x) = (ai + b) mod m + + i is the letter's index from 0 to the length of the alphabet - 1 + m is the length of the alphabet. For the Roman alphabet m is 26. + a and b are integers which make the encryption key + Values a and m must be coprime, if not you should throw error. + + .PARAMETER Plaintext + The text to be encrypted. + + .PARAMETER Keys + A hashtable contain the pair of keys `a` and `b`. + + .EXAMPLE + Invoke-Encode -Plaintext "test" -Keys @{a = 5; b = 7} + Returns: "ybty" + #> + [CmdletBinding()] + Param( + [string]$Plaintext, + [hashtable]$Keys + ) + Throw "Please implement this function" +} + +Function Invoke-Decode() { + <# + .SYNOPSIS + Use the affine cipher, an ancient encryption system created in the Middle East to decrypt ciphertext. + + .DESCRIPTION + The decryption function is: D(y) = (a^-1)(y - b) mod m + + y is the numeric value of an encrypted letter, i.e., y = E(x) + it is important to note that a^-1 is the modular multiplicative inverse (MMI) of a mod m + the modular multiplicative inverse only exists if a and m are coprime, if they are not you should throw error. + The MMI of a is x such that the remainder after dividing ax by m is 1: ax mod m = 1 + + .PARAMETER Ciphertext + The text to be decrypted. + + .PARAMETER Keys + A hashtable contain the pair of keys `a` and `b`. + + .EXAMPLE + Invoke-Decode -Ciphertext "ybty" -Keys @{a = 5; b = 7} + Returns: "test" + #> + [CmdletBinding()] + Param( + [string]$Ciphertext, + [hashtable]$Keys + ) + Throw "Please implement this function" +} \ No newline at end of file diff --git a/exercises/practice/affine-cipher/AffineCipher.tests.ps1 b/exercises/practice/affine-cipher/AffineCipher.tests.ps1 new file mode 100644 index 00000000..0b0c7056 --- /dev/null +++ b/exercises/practice/affine-cipher/AffineCipher.tests.ps1 @@ -0,0 +1,117 @@ +BeforeAll { + . "./AffineCipher.ps1" +} + +Describe "AffineCipher test cases" { + Context "encode tests" { + It "encode yes" { + $got = Invoke-Encode -Plaintext "yes" -Keys @{a = 5; b = 7} + $want = "xbt" + + $got | Should -BeExactly $want + } + + It "encode no" { + $got = Invoke-Encode -Plaintext "no" -Keys @{a = 15; b = 18} + $want = "fu" + + $got | Should -BeExactly $want + } + + It "encode OMG" { + $got = Invoke-Encode -Plaintext "OMG" -Keys @{a = 21; b = 3} + $want = "lvz" + + $got | Should -BeExactly $want + } + + It "encode O M G" { + $got = Invoke-Encode -Plaintext "O M G" -Keys @{a = 25; b = 47} + $want = "hjp" + + $got | Should -BeExactly $want + } + + It "encode mindblowingly" { + $got = Invoke-Encode -Plaintext "mindblowingly" -Keys @{a = 11; b = 15} + $want = "rzcwa gnxzc dgt" + + $got | Should -BeExactly $want + } + + It "encode numbers" { + $got = Invoke-Encode -Plaintext "Testing,1 2 3, testing." -Keys @{a = 3; b = 4} + $want = "jqgjc rw123 jqgjc rw" + + $got | Should -BeExactly $want + } + + It "encode deep thought" { + $got = Invoke-Encode -Plaintext "Truth is fiction." -Keys @{a = 5; b = 17} + $want = "iynia fdqfb ifje" + + $got | Should -BeExactly $want + } + + It "encode all the letters" { + $got = Invoke-Encode -Plaintext "The quick brown fox jumps over the lazy dog." -Keys @{a = 17; b = 33} + $want = "swxtj npvyk lruol iejdc blaxk swxmh qzglf" + + $got | Should -BeExactly $want + } + } + + Context "decode tests" { + It "decode exercism" { + $got = Invoke-Decode -Ciphertext "tytgn fjr" -Keys @{a = 3; b = 7} + $want = "exercism" + + $got | Should -BeExactly $want + } + + It "decode a sentence" { + $got = Invoke-Decode -Ciphertext "qdwju nqcro muwhn odqun oppmd aunwd o" -Keys @{a = 19; b = 16} + $want = "anobstacleisoftenasteppingstone" + + $got | Should -BeExactly $want + } + + It "decode numbers" { + $got = Invoke-Decode -Ciphertext "odpoz ub123 odpoz ub" -Keys @{a = 25; b = 7} + $want = "testing123testing" + + $got | Should -BeExactly $want + } + + It "decode all the letters" { + $got = Invoke-Decode -Ciphertext "swxtj npvyk lruol iejdc blaxk swxmh qzglf" -Keys @{a = 17; b = 33} + $want = "thequickbrownfoxjumpsoverthelazydog" + + $got | Should -BeExactly $want + } + + It "decode with no spaces in input" { + $got = Invoke-Decode -Ciphertext "swxtjnpvyklruoliejdcblaxkswxmhqzglf" -Keys @{a = 17; b = 33} + $want = "thequickbrownfoxjumpsoverthelazydog" + + $got | Should -BeExactly $want + } + + It "decode with too many spaces" { + $got = Invoke-Decode -Ciphertext "vszzm cly yd cg qdp" -Keys @{a = 15; b = 16} + $want = "jollygreengiant" + + $got | Should -BeExactly $want + } + } + + Context "invalid inputs" { + It "decode with a not coprime to m" { + { Invoke-Encode -Plaintext "This is a test." -Keys @{a = 6; b = 17} } | Should -Throw "*a and m must be coprime*" + } + + It "encode with a not coprime to m" { + { Invoke-Decode -Ciphertext "Decode test." -Keys @{a = 13; b = 5} } | Should -Throw "*a and m must be coprime*" + } + } +} \ No newline at end of file