1

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[]?

nickdmax
  • 539
  • 2
  • 4
  • 11
  • if I use a static byte array then it works: `$bytes = [byte[]]@(1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4)` so it must have something to do with how I created the byte array -- perhaps that += is the issue... – nickdmax Mar 18 '22 at 18:39
  • yes after the += the type is no longer Byte[] but is now an Object[] -- strange that I didn't get an error. So what must have happened is the Object[] was converted to a Byte[] (i.e. making a copy) and then passed to the function. – nickdmax Mar 18 '22 at 18:42
  • 3
    yeah, `+=` destroys and rebuilds arrays, so it probably rebuilds it as `[object[]]`. I'm guessing you could specify the type at `[byte[]]$bytes += $padding` and it would likely solve the issue. – TheMadTechnician Mar 18 '22 at 18:50
  • @TheMadTechnician -- That is much better than my whole Array.Copy() business. I like it. -- Yes it works (although I just declared `$bytes` as `[byte[]]$bytes` when it was first used. – nickdmax Mar 18 '22 at 18:57
  • @TheMadTechnician -- Based upon your comment I just change: ```[byte[]]$bytes = [UnicodeEncoding]::UTF8.GetBytes($secret) $bytes += [byte[]]::new((16 - $bytes.Count % 16) % 16)``` The extra `%` there is to fix a little bug with the padding logic for when the string was already a %16 long. -- Add your comment as an answer and I will mark it as the answer. – nickdmax Mar 18 '22 at 19:02
  • Then `[UnicodeEncoding]::UTF8.GetString($bytes[0..($bytes.Length - $padding)])` after unprotecting it :) – Santiago Squarzon Mar 18 '22 at 19:16
  • The problem with using the `GetString` is that after encryption the byte array no longer represents a Unicode string. Additionally, this is a block cipher so when you chop off the padding bytes you are removing data - it would no longer work with `unprotect` the function. - At this point the best thing to do is conver to a base64 encoded string. – nickdmax Mar 18 '22 at 19:22
  • My bad, I didnt notice `Unicode`, it should work if you get bytes and get string using `[Encoding]::UTF8.GetBytes( )` and `[Encoding]::UTF8.GetString( )` – Santiago Squarzon Mar 18 '22 at 19:24

1 Answers1

0

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.

nickdmax
  • 539
  • 2
  • 4
  • 11