Solving the Caesar Cipher
It is time to review a solution to last month's scripting challenge. I really hope you are at least starting the challenge. My challenges are intended to get you thinking about how you would use PowerShell. What scripting techniques or concepts can you take advantage of? Feeling unsure about something? Then you'll need to learn something new. Obviously, the challenge solution isn't of practical use. The learning experience and the process is the real take-away.
The challenge was a variation of the classic caesar cipher which is a simple character substitution algorithm. The difference is that you take the letters from a key work and insert them at the beginning the alphabet. This has the effect of "shifting" the letters and changing the order of the letters.
For example, I'll use these variables.
$plaintext = "I am the walrus"
$key = "powershell"
$alpha = "abcdefghijklmnopqrstuvwxyz"
The unique key letters in the key are 'powershl'. These letters are removed from $alpha
and inserted at the beginning. Your challenge was to write a set of PowerShell functions to encode and decode a short string. The basic challenge only required encoding/decoding a string with letters only and ignoring maintaining case. Experienced scripters should be able to write functions that support numbers, punctuation, and maintains casing.
How did you do?
Basic Solution
As is true with just about everything in PowerShell, my approach is not the only way.
I'll begin with my plain text phrase and the encoding key word. Feel free to follow along and run the code yourself.
$plainText = "I am the walrus with a secret."
$key = "powershell"
I need to get the unique letters from the key. I can turn the string into an array of [char]
objects and select the unique elements.
$keyUnique = ($key.ToCharArray() | Select-Object -Unique ) -join ''
I'll eventually insert this in front of the alphabet to create my encoding key.
I don't want to rely on static or hard-coded variables so I'll create a generic list and populate with the alphabet using the [char]
equivalents.
$alphaList = [System.Collections.Generic.List[char]]::new()
97..122 | ForEach-Object { $alphaList.Add([char]$_) }
I need to remove the elements from the alphabet list. I can't remove matching items by index number because each removal shifts the index number. I'll create a parallel lookup variable.
$alphaList | ForEach-Object -Begin {
$alphaLookup = [ordered]@{}
$i = 1
} -Process {
$alphaLookup.Add($_, $i++)
}
The $alphaLookup
variable is a hashtable with an index number as the value. Now I can update the alpha list by removing the unique key characters.
$keyUnique | ForEach-Object { [void]($alphaList.Remove($_)) }
Now, I can insert the unique letters to the beginning of the list.
$alphaList.InsertRange(0, [char[]]$keyUnique)
For the encoding, since I'm not concerned about case I'll convert the string to all lower case, get the characters and then find the matching character index in the lookup hashtable. Once I know the index, I can find the corresponding character in the list.
$encoded = $plainText.ToLower().ToCharArray() | ForEach-Object {
$charIndex = $alphaLookup[[char]$_]
if ($charIndex -gt 0) {
$alphaList[$charIndex - 1]
}
else {
#return the non-alpha character or space
$_
}
}
$Encoded
is an array that I can join together.
$out = -join $encoded
To decode all I need to do is run the process in reverse.
#create a reverse lookup table for the alphabet
$alphaLookupReverse = @{}
$alphaLookup.GetEnumerator() | ForEach-Object {
$alphaLookupReverse.Add($_.Value, $_.Key)
}
$in = $out.ToCharArray() | ForEach-Object {
$charIndex = $alphaList.IndexOf($_)
if ($charIndex -ge 0) {
$alphaLookupReverse[$charIndex + 1]
}
else {
#return the non-alpha character or space
$_
}
}
-join $in
I wrapped all of this into a set of basic functions.
Function Protect-Text {
[cmdletbinding()]
Param(
[Parameter(Mandatory, ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string]$PlainText,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Key
)
Begin {
$alphaList = [System.Collections.Generic.List[char]]::new()
97..122 | ForEach-Object { $alphaList.Add([char]$_) }
#create a lookup table for the alphabet
$alphaList | ForEach-Object -Begin {
$alphaLookup = [ordered]@{}
$i = 1
} -Process {
$alphaLookup.Add($_, $i++)
}
}
Process {
#get unique characters in key
$keyUnique = $key.ToCharArray() | Select-Object -Unique
#remove key characters from alphabet
$keyUnique | ForEach-Object { [void]($alphaList.Remove($_)) }
#insert the unique key characters at the beginning of the alphabet
$alphaList.InsertRange(0, [char[]]$keyUnique)
#encode the plain text by matching the alphabet index of each character
$encoded = $plainText.ToLower().ToCharArray() | ForEach-Object {
$charIndex = $alphaLookup[[char]$_]
if ($charIndex -gt 0) {
$alphaList[$charIndex - 1]
}
else {
#return the non-alpha character or space
$_
}
}
#join together
-join $encoded
}
End {
#not used
}
}
Function Unprotect-Text {
[cmdletbinding()]
Param(
[Parameter(Mandatory, ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string]$EncodedText,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Key
)
Begin {
$alphaList = [System.Collections.Generic.List[char]]::new()
97..122 | ForEach-Object { $alphaList.Add([char]$_) }
#create a lookup table for the alphabet
$alphaList | ForEach-Object -Begin {
$alphaLookup = [ordered]@{}
$i = 1
} -Process {
$alphaLookup.Add($_, $i++)
}
#create a reverse lookup table for the alphabet
$alphaLookupReverse = @{}
$alphaLookup.GetEnumerator() | ForEach-Object {
$alphaLookupReverse.Add($_.Value, $_.Key)
}
#get unique characters in key
$keyUnique = $key.ToCharArray() | Select-Object -Unique
#remove key characters from alphabet
$keyUnique | ForEach-Object { [void]($alphaList.Remove($_)) }
#insert the unique key characters at the beginning of the alphabet
$alphaList.InsertRange(0, [char[]]$keyUnique)
}
Process {
$in = $EncodedText.ToCharArray() | ForEach-Object {
$charIndex = $alphaList.IndexOf($_)
if ($charIndex -ge 0) {
$alphaLookupReverse[$charIndex + 1]
}
else {
#return the non-alpha character or space
$_
}
}
-join $in
}
End {
#not used
}
}
Because I'm using pipelined parameter values I can test by piping from one function to the other.
PS C:\> Protect-Text -PlainText "I am the PowerShell walrus" -Key "powershell123" | Unprotect-Text -Key powershell123
i am the powershell walrus