misc/dpapi-working.ps1
function Lock-DPAPIMessage { <# .SYNOPSIS Encrypts a message using DPAPI-NG and a target SID .DESCRIPTION DPAPI-NG allows encrypting messages using a target SID, which can then be decrypted only by them. .NOTES This is not directly supported in Linux, but there are python implementations (see LAPS4Linux) .LINK https://learn.microsoft.com/en-us/windows/win32/seccng/cng-dpapi .EXAMPLE Test-MyTestFunction -Verbose Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines #> [CmdletBinding(DefaultParameterSetName = 'SIDString')] param ( # Security Principal to use for encrypting the message [Parameter(Mandatory, ParameterSetName = "SIDString")] [Parameter(Mandatory, ParameterSetName = "SIDBytes")] [String]$SID, # Message to Encrypt, in String format [Parameter(Mandatory, ParameterSetName = "SIDString")] [String]$String, # Message to encrypt, in Byte array format [Parameter(Mandatory, ParameterSetName = "SIDBytes")] [Byte[]]$Bytes ) begin { } process { if ($String) { write-loghandler -level "Verbose" -message "Converting message to Byte[]" [Byte[]]$Bytes = [system.text.encoding]::UTF8.getBytes($String) } if ($SID) { $Descriptor = "SID=$SID" write-loghandler -level "Verbose" -message "Setting descriptor as $Descriptor" } $protected = [DpapiNgUtil]::Protect($descriptor,$Bytes) if ($AsBase64) { [system.convert]::ToBase64String($protected) } else { $Protected } <# $secretJSON = $secret | ConvertTo-Json $protected = [DpapiNgUtil]::Protect($descriptor,[system.text.encoding]::UTF8.getBytes($SecretJSON)) $protectedB64 = [system.convert]::ToBase64String($protected) "{0:-25} - {1}" -f "Original String", $SecretText "{0:-25} - {1}" -f "DPAPI String", $ProtectedB64 $unprotected = $null $unprotected = [system.text.encoding]::UTF8.GetString([DpapiNgUtil]::Unprotect($protected)) $Unprotected #> } end { } } function new-LAPSPasswordJSON { <# .SYNOPSIS Creates a plaintext LAPS JSON string .DESCRIPTION LAPS Passwords need to be in JSON format before being protected by DPAPI. This takes a username and a password and converts it to the correct format. .NOTES This functionality on Linux can be seen with LAPS4Linux. For reference, see LAPS Tech ref: https://learn.microsoft.com/en-us/windows-server/identity/laps/laps-technical-reference .LINK Specify a URI to a help page, this will show when Get-Help -Online is used. .EXAMPLE Test-MyTestFunction -Verbose Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines #> [CmdletBinding(DefaultParameterSetName="Prompt")] param ( [Parameter(ParameterSetName = "Credential", Mandatory )] [ValidateScript( { -not [String]::IsNullOrEmpty($_.UserName) -and -not [String]::IsNullOrEmpty($_.GetNetworkCredential().Password) })] [System.Management.Automation.PSCredential]$Credential, [Parameter(ParameterSetName = "UserWithPass", Mandatory )] [Parameter(ParameterSetName = "UserGenPass", Mandatory )] [Parameter(ParameterSetName = "Prompt" )] [ValidateNotNullOrEmpty()] [String]$Username, [Parameter(ParameterSetName = "UserWithPass", Mandatory)] [ValidateNotNullOrEmpty()] [securestring]$Password, [Parameter(ParameterSetName = "UserGenPass", Mandatory)] [Switch]$GeneratePassword, [ValidateNotNullOrEmpty()] [string]$LastSet, # Output as plain JSON instead of UTF-16-LE with trailing nulls [Switch]$AsPlainText, # Output as plain JSON instead of UTF-16-LE with trailing nulls [Switch]$IncludeUnencryptedPassword ) begin { } process { write-loghandler -level "Verbose" -message "Function Invocation Details" write-loghandler -level "Verbose" -message ("{0,-15} ; {1,-15} ; {2,-15} ; {3,-15}" -f "ParameterSet", "WhatIf", "Confirm", "Verbose") write-loghandler -level "Verbose" -message ("{0,-15} ; {1,-15} ; {2,-15} ; {3,-15}" -f $PsCmdlet.ParameterSetName, $WhatIfPreference, $ConfirmPreference, $VerbosePreference) if ($GeneratePassword) { [SecureString]$Password = get-randomPassword -forceComplex } if (-not $credential) { if ($username -and $Password) { $Credential = New-Object System.Management.Automation.PSCredential($username, $Password) } else { $CredParams = @{ Message = "Enter the LAPS credential to be stored" } if ($username) { $CredParams.Username = $Username } $Credential = Get-Credential @CredParams } } if ( [String]::IsNullOrEmpty($Credential.UserName) -or [String]::IsNullOrEmpty($Credential.GetNetworkCredential().Password) ) { Throw "Credential username or a password was either null, blank, or not provided." } if (-not $LastSet) { $LastSet = [datetime]::now } $LastSetFileTime = [datetime]::Parse($lastSet).ToFileTimeUtc() $LastSetLAPSFormat = $LastSetFileTime.ToString("X").toLower() if ($IncludeUnencryptedPassword) { $LapsPw = $Credential.GetNetworkCredential().password } else { write-loghandler -level "Verbose" -message "Blinding password because 'IncludeUnencryptedPassword' not specified" $LAPSPw = $cred.password.GetHashCode() } $LAPSHashTable = @{ 'n' = $Credential.UserName 'p' = $LapsPw 't' = "$LastSetLAPSFormat" } $LAPSJSON = $LAPSHashTable | convertto-JSON -Compress if ($AsPlainText) { $Output = $LAPSJSON } else { write-loghandler -level "Verbose" -message "UTF-16-LE + NUL NUL" $LAPSPadding=[system.text.encoding]::UTF8.GetBytes(@(0x00, 0x00) -as [Char[]]) $Output = [system.text.encoding]::Unicode.GetBytes($LAPSJSON) + $LAPSPadding } $Output } end { } } function get-randomPassword { [CmdletBinding()] param ( [Parameter()] [ValidateRange(6,128)] [Int] $PasswordLength = 14, [switch] $forceComplex, [Switch] $AsPlainText ) BEGIN { [char[]]$Exclusions = 'vwuOmnil1-.,%' } PROCESS{ $GeneratedCharacters = @{ Uppercase = (97..122) | get-random -count 32 | where-object {$Exclusions -notContains $_} | foreach-object {[Char]$_} Lowercase = (65..90) | get-random -count 128 | where-object {$Exclusions -notContains $_} | foreach-object {[Char]$_} Numeric = (48..57) | get-random -count 16 | where-object {$Exclusions -notContains $_} | foreach-object {[Char]$_} SpecialChar = [Char[]]('!!@#$%&*()=?}][{') | get-random -count 4 | where-object {$Exclusions -notContains $_} | foreach-object {[Char]$_} } $StringSet = $GeneratedCharacters.Uppercase + $GeneratedCharacters.Lowercase + $GeneratedCharacters.Numeric + $GeneratedCharacters.SpecialChar $Complexity = get-random -count 1 -inputObject $GeneratedCharacters.Lowercase $Complexity += get-random -count 1 -inputObject $GeneratedCharacters.Uppercase $Complexity += get-random -count 1 -inputObject $GeneratedCharacters.Numeric $Complexity += get-random -count 1 -inputObject $GeneratedCharacters.SpecialChar $PreScramble = -join(get-random -count ($passwordLength-4) -InputObject $StringSet) $GeneratedPassword = $($PreScramble + $Complexity) | sort-object {get-random} if ($AsPlainText) { $GeneratedPassword } else { $GeneratedPassword | ConvertTo-SecureString -AsPlainText -force } } } $DPAPI_TypeDef = @" using System; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; public static class DpapiNgUtil { public static string ProtectBase64(string protectionDescriptor, string input) { byte[] output = Protect(protectionDescriptor, Encoding.UTF8.GetBytes(input)); return Convert.ToBase64String(output); } public static string UnprotectBase64(string input) { byte[] bytes = Convert.FromBase64String(input); byte[] output = Unprotect(bytes); return Encoding.UTF8.GetString(output); } public static byte[] Protect(string protectionDescriptor, byte[] data) { using (NCryptProtectionDescriptorHandle handle = NCryptProtectionDescriptorHandle.Create(protectionDescriptor)) { return Protect(handle, data); } } internal static byte[] Protect(NCryptProtectionDescriptorHandle descriptor, byte[] data) { uint cbProtectedBlob; LocalAllocHandle protectedBlobHandle; int status = NativeMethods.NCryptProtectSecret(descriptor, NativeMethods.NCRYPT_SILENT_FLAG, data, (uint)data.Length, IntPtr.Zero, IntPtr.Zero, out protectedBlobHandle, out cbProtectedBlob); if(status != 0) { throw new CryptographicException(status); } using (protectedBlobHandle) { byte[] retVal = new byte[cbProtectedBlob]; Marshal.Copy(protectedBlobHandle.DangerousGetHandle(), retVal, 0, retVal.Length); return retVal; } } public static byte[] Unprotect(byte[] protectedData) { uint cbData; LocalAllocHandle dataHandle; int status = NativeMethods.NCryptUnprotectSecret(IntPtr.Zero, NativeMethods.NCRYPT_SILENT_FLAG, protectedData, (uint)protectedData.Length, IntPtr.Zero, IntPtr.Zero, out dataHandle, out cbData); if (status != 0) { throw new CryptographicException(status); } using (dataHandle) { byte[] retVal = new byte[cbData]; Marshal.Copy(dataHandle.DangerousGetHandle(), retVal, 0, retVal.Length); return retVal; } } } internal class LocalAllocHandle : SafeHandle { // Called by P/Invoke when returning SafeHandles private LocalAllocHandle() : base(IntPtr.Zero, ownsHandle: true) { } // Do not provide a finalizer - SafeHandle's critical finalizer will // call ReleaseHandle for you. public override bool IsInvalid { get { return handle == IntPtr.Zero; } } protected override bool ReleaseHandle() { IntPtr retVal = NativeMethods.LocalFree(handle); return (retVal == IntPtr.Zero); } } internal class NCryptProtectionDescriptorHandle : SafeHandle { // Called by P/Invoke when returning SafeHandles private NCryptProtectionDescriptorHandle() : base(IntPtr.Zero, ownsHandle: true) { } // Do not provide a finalizer - SafeHandle's critical finalizer will // call ReleaseHandle for you. public override bool IsInvalid { get { return handle == IntPtr.Zero; } } public static NCryptProtectionDescriptorHandle Create(string protectionDescriptor) { NCryptProtectionDescriptorHandle descriptorHandle; int status = NativeMethods.NCryptCreateProtectionDescriptor(protectionDescriptor, 0, out descriptorHandle); if (status != 0) { throw new CryptographicException(status); } return descriptorHandle; } protected override bool ReleaseHandle() { int retVal = NativeMethods.NCryptCloseProtectionDescriptor(handle); return (retVal == 0); } } internal static class NativeMethods { private const string KERNEL32LIB = "kernel32.dll"; private const string NCRYPTLIB = "ncrypt.dll"; internal const uint NCRYPT_SILENT_FLAG = 0x00000040; // http://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx [DllImport(KERNEL32LIB, SetLastError = true)] internal static extern IntPtr LocalFree( [In] IntPtr handle); // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706799(v=vs.85).aspx [DllImport(NCRYPTLIB)] internal extern static int NCryptCloseProtectionDescriptor( [In] IntPtr hDescriptor); // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706800(v=vs.85).aspx [DllImport(NCRYPTLIB, CharSet = CharSet.Unicode)] internal extern static int NCryptCreateProtectionDescriptor( [In] string pwszDescriptorString, [In] uint dwFlags, [Out] out NCryptProtectionDescriptorHandle phDescriptor); // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706802(v=vs.85).aspx [DllImport(NCRYPTLIB)] internal extern static int NCryptProtectSecret( [In] NCryptProtectionDescriptorHandle hDescriptor, [In] uint dwFlags, [In] byte[] pbData, [In] uint cbData, [In] IntPtr pMemPara, [In] IntPtr hWnd, [Out] out LocalAllocHandle ppbProtectedBlob, [Out] out uint pcbProtectedBlob); // http://msdn.microsoft.com/en-us/library/windows/desktop/hh706811(v=vs.85).aspx [DllImport(NCRYPTLIB)] internal extern static int NCryptUnprotectSecret( [In] IntPtr phDescriptor, [In] uint dwFlags, [In] byte[] pbProtectedBlob, [In] uint cbProtectedBlob, [In] IntPtr pMemPara, [In] IntPtr hWnd, [Out] out LocalAllocHandle ppbData, [Out] out uint pcbData); } "@ add-type -TypeDefinition $DPAPI_TypeDef |