PowerShell Cryptography Library
When playing with cryptography challenges (don’t we all?,) you end up leaning on a bunch of common tasks. For example, substituting all text in a string with a set of replacements (substitution ciphers,) XORing strings together, applying dictionary-based algorithms, investigating word frequency, and more.
PowerShell lends itself really well to these challenges, and I’ve developed a small cryptography library along the way. Here it is. Your job, as a cryptographer, is to uncover the hidden comments :) (Hint: don’t spend very long. Some problems are unbreakable.)
For example, here’s the script I used (interactively) to solve an “XOR Cipher” at http://cryptolib.com/challenges.php.
$cipherHex1 = @"
3526532A122A183D4D0E4A2E3713101954013E2B3E2246235728
573553064832781F164A481F2A2B396350341E2B5E3A5E1C4861
301C11544F1D22363863413F192A4A36520A0D2D2D13044A001A
252025335E2F142F5A355A
"@
$cipherHex2 = @"
32335734163A5136510E41612818114A4912252C2722462F1820
4B7956014E283C180D4D001522312724573416225C794C074C2C
3D0E435A4F063B29383012251B214C315A1C41283618435E5212
2531342D556612205B2B461F592837131019481225212F225B2A
"@
$words = @{}
Get-Content dictfull.txt | % { $words[$_] = $true }
## Turn the cipher text into a single string
$cipherText1 = $cipherHex1 -replace "`r`n",""
$cipherText2 = $cipherHex2 -replace "`r`n",""
## Convert hex to decimal
$cipherBytes1 = $cipherText1 -split '(..)' | ? { $_ } | % { [Convert]::ToInt32($_, 16) }
$cipherBytes2 = $cipherText2 -split "(..)" | ? { $_ } | % { [Convert]::ToInt32($_, 16) }
## Drop the key by XORing the two cipher texts together
$plainTextCombined = XORBytes $cipherBytes1[0..19] $cipherBytes2[0..19]
## Initialize the key
$key = ,0x20*20
## First break comes by scanning through the entire dictionary, guessing the
## first word of one plaintext, then seeing if that corresponds to a key that makes
## the second plaintext look reasonable
$output = $(foreach($word in $words.GetEnumerator() | % { $_.Key })
{
$testString = XORBytes $plainTextCombined[0..($word.Length - 1)] ($word.ToCharArray())
(-join [char[]]$testString) + " : $word"
}
)
## Output the list to a file, then clean the file to include only alpha-numeric
## characters.
$output > first_word.txt
$alphaOnly = gc first_word.txt | select-string '^[a-zA-Z ]+ : .*$'
$alphaOnly > first_word_alpha.txt
## The first break comes from seeing that "healed dr" corresponds to "operation".
## Make a guess of that as the first 9 elements of the key.
$k
ey[0],$key[1],$key[2],$key[3],$key[4],$key[5],$key[6],$key[7],$key[8] = XORBytes ("healed dr".ToCharArray()) $cipherBytes1[0..8]
## These other magic values come from looking at the output and making educated guesses
$key[9],$key[10] = XORBytes ("se".ToCharArray()) $cipherBytes1[49..50]
$key[11],$key[12],$key[13] = XORBytes ("ion".ToCharArray()) $cipherBytes2[91..93]
$key[14],$key[15],$key[16] = XORBytes ("gs ".ToCharArray()) $cipherBytes1[74..76]
$key[17],$key[18],$key[19] = XORBytes ("Fit".ToCharArray()) $cipherBytes2[37..39]
## Display the currently-guessed plain texts alongside eachother,
## including the key and index into the cipher text.
for($counter = 0; $counter -lt $cipherBytes2.Count; $counter++)
{
$keyChar = $key[$counter % 20]
"{0} {1} {2} {3}" -f $counter,([char] $keyChar),([char] ($cipherBytes1[$counter] -bxor $keyChar)),([char] ($cipherBytes2[$counter] -bxor $keyChar))
}
-join [char[]] (XORBytes $key $cipherBytes1)
-join [char[]] (XORBytes $key $cipherBytes2)
-join [char[]] $key
And the library:
unction Get-SubstitutedText($cipherText, $substitutions)
{
$content = $(foreach($char in [char[]] $cipherText)
{
if($substitutions["$char"])
{
$substitutions["$char"]
}
else
{
"."
}
}
)
-join $content
}
function XORBytes($passwordBytes, $cipherBytes2)
{
$combinedBytes = @()
for($counter = 0; $counter -lt $cipherBytes2.Length; $counter++)
{
$combinedBytes += $passwordBytes[$counter % $passwordBytes.Length] -bxor $cipherBytes2[$counter]
}
$combinedBytes
}
function Search-DictionaryForWord($pattern, $floating)
{
if(-not $floating) { $pattern = "^$pattern`$" }
Select-String $pattern wordlist.txt
}
function Search-DictionaryForPattern($pattern, $floating)
{
$substituted = ""
$toFind = ""
if(-not $floating) { $toFind += "^" }
foreach($char in $pattern.ToLower().ToCharArray())
{
if($char -eq ".")
{
$toFind += "."
continue
}
$subIndex = $substituted.IndexOf($char)
if($subIndex -ge 0)
{
$toFind += "\" + ($subIndex + 1)
}
else
{
if(-not $substituted)
{
$toFind += "(.)"
}
else
{
$toFind += "([^$substituted])"
}
$substituted += $char
}
}
if(-not $floating) { $toFind += '$' }
Write-Verbose $toFind
Select-String $toFind wordlist.txt
}
function Measure-LetterFrequency($cipherText)
{
$frequentLetters = " etnoriasfindhdlcumwfgypbvkjxqz"
"`nLetter Frequencies:`n"
$groups = [char[]] $cipherText.ToLower() | group | Sort -Desc Count
$groupNumber = 0
$(foreach($group in $groups)
{
$group | Select Name,
@{
Name = "Replacement";
Expression = { $frequentLetters[$groupNumber] } },
Count,
@{
Name = "Percent";
Expression = { "{0:..%}" -f ($_.Count / $cipherText.Length) } }
$groupNumber++
}) | ft -auto | out-string | % { $_.Trim() }
}
function Measure-BigraphFrequency($cipherText)
{
$frequentBigraphs = "th","he","an","in","er","on","re",
"ed","nd","ha","at","en","es","of","nt","ea","ti",
"to","io","le","is","ou","ar","as","de","rt","ve"
$cipherText = $cipherText -replace " ",""
$cipherText = $cipherText -replace "\W",""
"`nBigraph Frequencies:`n"
$groups =
@(($cipherText -split '(..)') +
($cipherText.Remove(0,1) -split '(..)')) |
? { $_.Trim().Length -eq 2 } | group | Sort -Desc Count
$groupNumber = 0
$(foreach($group in $groups)
{
$group | Select Name,
@{
Name = "Replacement";
Expression = { $frequentBigraphs[$groupNumber] } },
Count
$groupNumber++
}) | ft -auto | out-string | % { $_.Trim() }
}
function Measure-DoubleLetterFrequency($cipherText)
{
$frequentDoubles = "ss","ee","tt","ff","ll","mm","oo"
"`nDouble Frequencies:`n"
$groups = [Regex]::Matches($cipherText, '(.)\1') | % { $_.Value } |
group | Sort -Desc Count
$groupNumber = 0
$(foreach($group in $groups)
{
$group | Select Name,
@{
Name = "Replacement";
Expression = { $frequentDoubles[$groupNumber] } },
Count
$groupNumber++
}) | ft -auto | out-string | % { $_.Trim() }
"`nOther groups:"
"$($frequentDoubles[$groupNumber..($frequentDoubles.Length - 1)])"
}
function Measure-InitialFrequency($cipherText)
{
$frequentInitialLetters = "toawbcdsfmrhiyeglnoujk"
"`nInitial letters:`n"
$groups = -split $cipherText.ToLower() | % { ([char[]] $_)[< /span>0] } | group | Sort -Desc Count
$groupNumber = 0
$(foreach($group in $groups)
{
$group | Select Name,
@{
Name = "Replacement";
Expression = { $frequentInitialLetters[$groupNumber] } },
Count,
@{
Name = "Percent";
Expression = { "{0:..%}" -f ($_.Count / $groups.Count) } }
$groupNumber++
}) | ft -auto | out-string | % { $_.Trim() }
}
function combo($string)
{
if($string.Length -eq 2)
{
$string
$string[-1] + $string[0]
}
else
{
foreach($element in $string.ToCharArray())
{
combo ($string -replace $element,'') | foreach { $element + $_ }
combo ($string -replace $element,'') | foreach { $_ }
}
}
}