DPAPI ProtectedMemory.Protect is not encrypting byte array

241 Views Asked by At

I was beginning to explore the DPAPI and my very first sample code does not work. What I expected was for my byte array to change after a call to [ProtectedMemory]::Protect(). However, the byte array was exactly the same before and after the call. So either there is something I don't understand about Powershell (likely) OR there is something I don't understand about using the DPAPI (likely), OR the DPAPI is a scam that does not actually encrypt anything (unlikely). Here is my sample code:

using namespace System.Security.Cryptography
using namespace System.Text

function Protect-String {
    param (
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Secret
    )
    begin {
        Add-Type -AssemblyName System.Security
    }

    process {
        $bytes = [UnicodeEncoding]::UTF8.GetBytes($secret)
        $padding = [byte[]]::new(16 - $bytes.length % 16)
        $bytes += $padding
        Write-host "Original: $bytes"
        $Scope = [MemoryProtectionScope]::SameLogon
        [ProtectedMemory]::Protect($bytes, $Scope)
        Write-host "After   : $bytes"
    }
}

output:

PS C:\> Protect-String something
Original: 115 111 109 101 116 104 105 110 103 0 0 0 0 0 0 0
After   : 115 111 109 101 116 104 105 110 103 0 0 0 0 0 0 0

What I know so far: This code is for Powershell 5 under .Net and will not work with Powershell 7 under .Net Core as the System.Security.Cryptography namespace does not contain the DPAPI classes - these rely upon Windows OS features.

The Protect should encrypt the byte array in place. Since my input and output are the same, I wonder if PowerShell is passing a copy/clone of the byte[]?

1

There are 1 best solutions below

0
nickdmax On

The solution (as given by @TheMadTechnician in the comments) was to specify the type for the $bytes array. Without that specificity, Powershell had to work with ambiguity and when asked to append an array to an array the result was an object[]. When it came time to pass the array to protect() the object[] array was coerced into a new byte[] array which was encrypted and then lost.

My final "play" code was this:

using namespace System.Security.Cryptography
using namespace System.Text
Add-Type -AssemblyName System.Security
# Requires Powershell for Windows

function Protect-MemoryString {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Secret,
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [MemoryProtectionScope]$Scope = [MemoryProtectionScope]::SameLogon

    )

    process {
        [byte[]]$bytes = [UnicodeEncoding]::UTF8.GetBytes($secret)
        $bytes += [byte[]]::new((16 - $bytes.Count % 16) % 16)
        Write-Verbose "Original: $bytes"
        [ProtectedMemory]::Protect($bytes, $Scope)
        Write-Verbose "After   : $bytes"
        [System.Convert]::ToBase64String($bytes)
    }
}

Note that the output is a Base64 encoded string - I did not convert back to a UTF8 string because after the encryption the data is no longer string data and certainly is not UTF8 encoded string data.