PowerShell Cryptography Library

Mon, Oct 19, 2009 5-minute read

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 { $_ }
        }
    }
   
}