Enable SecureString cmdlets for non-Windows (#9199)

On Unix systems, fallback to plaintext manipulation instead of using the DPAPI which is not available.

## PR Context

Currently, existing scripts that use SecureString cmdlets fail with an error complaining about crypt32.dll not being available.  This change allows these cmdlets to be used, but there is no encryption of the string.
.Net already [states](https://docs.microsoft.com/en-us/dotnet/api/system.security.securestring?view=netcore-2.1#remarks) that the contents of a SecureString are not encrypted on .Net Core.

Fix https://github.com/PowerShell/PowerShell/issues/1654

Co-authored-by: Travis Plunk <travis.plunk@microsoft.com>
This commit is contained in:
Steve Lee
2019-03-25 10:01:11 -07:00
committed by Travis Plunk
co-authored by Travis Plunk
parent d4eed9ea2a
commit 9e608ac55f
3 changed files with 49 additions and 48 deletions
@@ -155,12 +155,17 @@ namespace Microsoft.PowerShell
byte[] protectedData = null;
data = GetData(input);
#if UNIX
// DPAPI doesn't exist on UNIX so we simply use the string as a byte-array
protectedData = data;
#else
protectedData = ProtectedData.Protect(data, null,
DataProtectionScope.CurrentUser);
for (int i = 0; i < data.Length; i++)
{
data[i] = 0;
}
#endif
output = ByteArrayToString(protectedData);
@@ -189,9 +194,14 @@ namespace Microsoft.PowerShell
protectedData = ByteArrayFromString(input);
#if UNIX
// DPAPI isn't supported in UNIX, so we just translate the byte-array back to a string
data = protectedData;
#else
data = ProtectedData.Unprotect(protectedData, null,
DataProtectionScope.CurrentUser);
#endif
s = New(data);
return s;
@@ -400,7 +410,7 @@ namespace Microsoft.PowerShell
internal string IV { get; }
}
#if CORECLR
#if !UNIX
// The DPAPIs implemented in this section are temporary workaround.
// CoreCLR team will bring 'ProtectedData' type to Project K eventually.
@@ -421,7 +431,9 @@ namespace Microsoft.PowerShell
public static byte[] Protect(byte[] userData, byte[] optionalEntropy, DataProtectionScope scope)
{
if (userData == null)
{
throw new ArgumentNullException("userData");
}
GCHandle pbDataIn = new GCHandle();
GCHandle pOptionalEntropy = new GCHandle();
@@ -446,13 +458,14 @@ namespace Microsoft.PowerShell
dwFlags |= CAPI.CRYPTPROTECT_LOCAL_MACHINE;
unsafe
{
if (!CAPI.CryptProtectData(new IntPtr(&dataIn),
string.Empty,
new IntPtr(&entropy),
IntPtr.Zero,
IntPtr.Zero,
dwFlags,
new IntPtr(&blob)))
if (!CAPI.CryptProtectData(
pDataIn: new IntPtr(&dataIn),
szDataDescr: string.Empty,
pOptionalEntropy: new IntPtr(&entropy),
pvReserved: IntPtr.Zero,
pPromptStruct: IntPtr.Zero,
dwFlags: dwFlags,
pDataBlob: new IntPtr(&blob)))
{
int lastWin32Error = Marshal.GetLastWin32Error();
@@ -473,7 +486,9 @@ namespace Microsoft.PowerShell
// In some cases, the API would fail due to OOM but simply return a null pointer.
if (blob.pbData == IntPtr.Zero)
{
throw new OutOfMemoryException();
}
byte[] encryptedData = new byte[(int)blob.cbData];
Marshal.Copy(blob.pbData, encryptedData, 0, encryptedData.Length);
@@ -483,9 +498,13 @@ namespace Microsoft.PowerShell
finally
{
if (pbDataIn.IsAllocated)
{
pbDataIn.Free();
}
if (pOptionalEntropy.IsAllocated)
{
pOptionalEntropy.Free();
}
if (blob.pbData != IntPtr.Zero)
{
CAPI.ZeroMemory(blob.pbData, blob.cbData);
@@ -499,11 +518,10 @@ namespace Microsoft.PowerShell
/// </summary>
public static byte[] Unprotect(byte[] encryptedData, byte[] optionalEntropy, DataProtectionScope scope)
{
#if UNIX
throw new PlatformNotSupportedException(Serialization.DeserializeSecureStringNotSupported);
#else
if (encryptedData == null)
{
throw new ArgumentNullException("encryptedData");
}
GCHandle pbDataIn = new GCHandle();
GCHandle pOptionalEntropy = new GCHandle();
@@ -525,22 +543,30 @@ namespace Microsoft.PowerShell
uint dwFlags = CAPI.CRYPTPROTECT_UI_FORBIDDEN;
if (scope == DataProtectionScope.LocalMachine)
{
dwFlags |= CAPI.CRYPTPROTECT_LOCAL_MACHINE;
}
unsafe
{
if (!CAPI.CryptUnprotectData(new IntPtr(&dataIn),
IntPtr.Zero,
new IntPtr(&entropy),
IntPtr.Zero,
IntPtr.Zero,
dwFlags,
new IntPtr(&userData)))
if (!CAPI.CryptUnprotectData(
pDataIn: new IntPtr(&dataIn),
ppszDataDescr: IntPtr.Zero,
pOptionalEntropy: new IntPtr(&entropy),
pvReserved: IntPtr.Zero,
pPromptStruct: IntPtr.Zero,
dwFlags: dwFlags,
pDataBlob: new IntPtr(&userData)))
{
throw new CryptographicException(Marshal.GetLastWin32Error());
}
}
// In some cases, the API would fail due to OOM but simply return a null pointer.
if (userData.pbData == IntPtr.Zero)
{
throw new OutOfMemoryException();
}
byte[] data = new byte[(int)userData.cbData];
Marshal.Copy(userData.pbData, data, 0, data.Length);
@@ -550,16 +576,19 @@ namespace Microsoft.PowerShell
finally
{
if (pbDataIn.IsAllocated)
{
pbDataIn.Free();
}
if (pOptionalEntropy.IsAllocated)
{
pOptionalEntropy.Free();
}
if (userData.pbData != IntPtr.Zero)
{
CAPI.ZeroMemory(userData.pbData, userData.cbData);
CAPI.LocalFree(userData.pbData);
}
}
#endif
}
}
@@ -5,12 +5,6 @@ Describe "SecureString conversion tests" -Tags "CI" {
$string = "ABCD"
$secureString = [System.Security.SecureString]::New()
$string.ToCharArray() | foreach-object { $securestring.AppendChar($_) }
$defaultParamValues = $PSdefaultParameterValues.Clone()
$PSdefaultParameterValues = @{}
if ( ! $IsWindows ) { $PSdefaultParameterValues["it:pending"] = $true }
}
AfterAll {
$global:PSdefaultParameterValues = $defaultParamValues
}
It "using null arguments to ConvertFrom-SecureString produces an exception" {
@@ -170,29 +170,7 @@ Describe "CliXml test" -Tags "CI" {
$testPath | Should -Not -Exist
}
It "should fail to import PSCredential on non-Windows" -Skip:$IsWindows {
$cliXml = @"
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<Obj RefId="0">
<TN RefId="0">
<T>System.Management.Automation.PSCredential</T>
<T>System.Object</T>
</TN>
<ToString>System.Management.Automation.PSCredential</ToString>
<Props>
<S N="UserName">foo</S>
<!-- this is just an empty password, but needs to be a valid hash -->
<SS N="Password">01000000d08c9ddf0115d1118c7a00c04fc297eb01000000977756228672474da9bfbe7b6acc02cb0000000002000000000003660000c0000000100000002529194617877bbbc952c6aacb6535f00000000004800000a000000010000000703ca974777a8335d5e2f8b3e592ff4208000000635d9fcda035734e140000001b6940e3222117a1014c67377e8786549f3dca29</SS>
</Props>
</Obj>
</Objs>
"@
$path = "$testdrive/cred.xml"
Set-Content -Path $path -Value $cliXml
{ Import-Clixml -Path $path } | Should -Throw -ErrorId "System.PlatformNotSupportedException,Microsoft.PowerShell.Commands.ImportClixmlCommand"
}
It "should import PSCredential" -Skip:(!$IsWindows) {
It "should import PSCredential" {
$UserName = "Foo"
$pass = ConvertTo-SecureString (New-RandomHexString) -AsPlainText -Force
$cred = [PSCredential]::new($UserName, $pass)