File Decrypting Script

The following PowerShell script can be used to decrypt files encrypted by WinSCP, when you do not have WinSCP available.

The script works both in legacy Windows PowerShell and modern PowerShell [Core], though it runs slower in the latter.

For C# version of the AesCtrTransform function, see Can I use AES in CTR mode in .NET?

Advertisement

param (
    [Parameter(Mandatory = $True)]
    $path,
    [Parameter(Mandatory = $True)]
    $key
)
 
function AesCtrTransform ($key, $salt, $inputStream, $outputStream)
{
    $aes = New-Object System.Security.Cryptography.AesManaged -Property @{
        Mode = [System.Security.Cryptography.CipherMode]::ECB
        Padding = [System.Security.Cryptography.PaddingMode]::None
    }
 
    $blockSize = $aes.BlockSize / 8
 
    $counter = $salt.Clone()
 
    # Particularly in pwsh, it runs much faster with non-generic Queue
    $xorMask = New-Object System.Collections.Queue
 
    $zeroIv = New-Object byte[] $blockSize
    $counterEncryptor = $aes.CreateEncryptor($key, $zeroIv)
 
    while (($b = $inputStream.ReadByte()) -ne -1)
    {
        if ($xorMask.Count -eq 0)
        {
            $counterModeBlock = New-Object byte[] $blockSize
 
            $counterEncryptor.TransformBlock(
                $counter, 0, $counter.Length, $counterModeBlock, 0) | Out-Null
 
            for ($i2 = $counter.Length - 1; $i2 -ge 0; $i2--)
            {
                if ($counter[$i2] -eq 0xFF)
                {
                    $counter[$i2] = 0
                }
                else
                {
                    ++$counter[$i2]
                    break
                }
            }
 
            foreach ($b2 in $counterModeBlock)
            {
                $xorMask.Enqueue($b2)
            }
        }
 
        $mask = $xorMask.Dequeue()
        $outputStream.WriteByte([byte](([byte]$b) -bxor $mask))
    }
}
 
try
{
    $aes = New-Object System.Security.Cryptography.AesManaged
 
    try
    {
        # .NET >= 5
        $key = [System.Convert]::FromHexString($key)
    }
    catch [System.Management.Automation.RuntimeException]
    {
        # .NET Framework
        $key = [System.Runtime.Remoting.Metadata.W3cXsd2001.SoapHexBinary]::Parse($key).Value
    }
    $keySize = $aes.KeySize / 8
    if ($key.Length -ne $keySize)
    {
        throw "Invalid key size (need $keySize bytes)"
    }
    
    $name = Split-Path -Leaf $path
 
    $aesCtrExt = ".aesctr.enc"
    if (-not ($name -like "*$aesCtrExt"))
    {
        throw "$name has not a known encrypted file extension [$aesCtrExt]"
    }
 
    $base64 = $name.SubString(0, $name.Length - $aesCtrExt.Length).Replace("_", "/")
    $padding = 4 - ($base64.Length % 4)
    if (($padding -gt 0) -and ($padding -lt 4))
    {
        $base64 += "=" * $padding
    }
 
    $buffer = [System.Convert]::FromBase64String($base64)
    $blockSize = $aes.BlockSize / 8
    $nameSalt = $buffer[0..($blockSize - 1)]
 
    $nameInputStream =
        New-Object System.IO.MemoryStream(
            $buffer, $nameSalt.Count, ($buffer.Length - $nameSalt.Length))
 
    $nameOutputStream = New-Object System.IO.MemoryStream
 
    AesCtrTransform $key $nameSalt $nameInputStream $nameOutputStream
 
    $name = [System.Text.Encoding]::UTF8.GetString($nameOutputStream.ToArray())
 
    Write-Host "Decrypted filename: $name"
    $parentPath = (Split-Path -Parent $path)
    if ($parentPath)
    {
        $outPath = Join-Path $parentPath $name
    }
    else
    {
        $outPath = $name
    }
 
    if (Test-Path $outPath)
    {
        throw "$outPath already exists"
    }
 
    if ((Get-Item $path).Length -eq 0)
    {
        Write-Host "Creating empty file..."
        New-Item $outPath -Type file | Out-Null
    }
    else
    {
        $fileInputStream = [System.IO.File]::OpenRead($path)
 
        $aesMagic = [System.Text.Encoding]::UTF8.GetBytes("aesctr..........")
 
        $magic = New-Object byte[] $aesMagic.Length
        $fileSalt = New-Object byte[] $blockSize
 
        if (($fileInputStream.Read($magic, 0, $magic.Length) -lt $magic.Length) -or
            (-not [System.Linq.Enumerable]::SequenceEqual($magic, $aesMagic)) -or
            ($fileInputStream.Read($fileSalt, 0, $fileSalt.Length) -lt $fileSalt.Length))
        {
            throw "$path does not have valid header"
        }
 
        $fileOutputStream = [System.IO.File]::Create($outPath)
 
        Write-Host "Decrypting contents..."
 
        AesCtrTransform $key $fileSalt $fileInputStream $fileOutputStream
 
        $fileOutputStream.Dispose()
    }
 
    [System.IO.File]::SetLastWriteTime($outPath, [System.IO.File]::GetLastWriteTime($path))
 
    Write-Host "Done."
 
    exit 0
}
catch
{
    Write-Host "Error: $($_.Exception.Message)"
    exit 1
}

Advertisement

Last modified: by martin