如何将SecureString转换为System.String?


156

除了通过创建System.String来保护SecureString的不安全之外,还有其他保留意见,该怎么做?

如何将普通的System.Security.SecureString转换为System.String?

我敢肯定,许多熟悉SecureString的人都会做出回应,那就是永远不要将SecureString转换为普通的.NET字符串,因为它会删除所有安全保护措施。 我知道。但是现在,我的程序无论如何都使用普通字符串完成所有操作,并且我试图提高其安全性,尽管我将使用向我返回SecureString的API,但我并没有尝试使用它来提高安全性。

我知道Marshal.SecureStringToBSTR,但是我不知道如何获取该BSTR并从中创建System.String。

对于那些可能想知道我为什么要这样做的人,我从用户处获取密码,并将其以html表单POST的形式提交,以将用户登录到网站。所以...这实际上必须使用托管的未加密缓冲区来完成。如果我什至可以访问非托管,未加密的缓冲区,我想我可以在网络流上逐字节写入流,并希望这样可以使密码始终保持安全。我希望至少对这些情况中的一种有一个答案。

Answers:


192

使用System.Runtime.InteropServices.Marshal类:

String SecureStringToString(SecureString value) {
  IntPtr valuePtr = IntPtr.Zero;
  try {
    valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
    return Marshal.PtrToStringUni(valuePtr);
  } finally {
    Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
  }
}

如果要避免创建托管字符串对象,则可以使用Marshal.ReadInt16(IntPtr, Int32)以下命令访问原始数据:

void HandleSecureString(SecureString value) {
  IntPtr valuePtr = IntPtr.Zero;
  try {
    valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
    for (int i=0; i < value.Length; i++) {
      short unicodeChar = Marshal.ReadInt16(valuePtr, i*2);
      // handle unicodeChar
    }
  } finally {
    Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
  }
}

1
几年后我也投票了,谢谢您的帮助!简要说明一下:这在它自己的内存中也可以用作静态。
约翰·西特

我曾经StopWatchSecureStringToString花4.6sec运行。对我来说太慢了。有谁能获得相同的时间或更快的速度?
radbyx

@radbyx在快速而肮脏的测试设置中,我可以在76ms内将其调用1000次。第一次调用需要0.3毫秒,而随后的调用则需要约0.07毫秒。您的安全字符串有多大?您使用的是哪个版本的框架?
Rasmus Faber

我的secureString的长度是168。如果正在回答您的问题,我正在使用.NET Framework 3.5?我尝试过5-10次总是大约4.5-4.65秒〜我希望能花点时间
radbyx

@RasmusFaber我不好,我已经Database.GetConnectionString()在您的代码中添加了一个以获取我的secureString,这是最糟糕的部分,它花费了将近5秒(是的,我应该调查一下!:)您的代码在秒表中花费了0.00毫秒,因此都好。感谢您指出正确的方向。
radbyx

108

显然,您知道这会破坏SecureString的全部目的,但是无论如何我都会重申一下。

如果需要单线,请尝试以下操作:(仅.NET 4及更高版本)

string password = new System.Net.NetworkCredential(string.Empty, securePassword).Password;

其中securePassword是SecureString。


10
尽管它确实无法达到生产目的,但您的解决方案非常适合单元测试。谢谢。
beterthanlife 2014年

这帮助我弄清楚了SecureString(System.Security.SecureString)没有传递给我的ApiController(webapi)。Thx
granadaCoder

5
请注意,在PowerShell中,这是[System.Net.NetworkCredential]::new('', $securePassword).Password
stijn

1
您可以详细说明@ TheIncorrigible1吗?例如,什么时候''与类型不同[String]::Empty?也New-Object Net.Credential对我不起作用:找不到类型[Net.Credential]:验证是否已加载包含该类型的程序集
stijn

2
它违反了SecureString的目的,因为它会将SecureString内容的未加密副本复制为普通字符串。每次执行此操作时,都将至少一个未加密字符串的副本(可能还有多个垃圾回收)添加到内存中。对于某些对安全性敏感的应用程序,这被认为是一种风险,SecureString是专门为降低风险而实施的。
Steve In CO

49

ang 张贴在此之后,我发现在回答深这篇文章。但是,如果有人知道如何一次访问此方法公开的IntPtr非托管,未加密缓冲区,一次访问一个字节,这样我就不必为了提高安全性而从中创建托管字符串对象,请添加答案。:)

static String SecureStringToString(SecureString value)
{
    IntPtr bstr = Marshal.SecureStringToBSTR(value);

    try
    {
        return Marshal.PtrToStringBSTR(bstr);
    }
    finally
    {
        Marshal.FreeBSTR(bstr);
    }
}

您当然可以使用unsafe关键字和a char*,只需调用bstr.ToPointer()并强制转换。
Ben Voigt

为了安全起见,@ BenVoigt BSTR在字符串数据之后有一个空终止符,但也允许在字符串中嵌入空字符。因此,这要复杂得多,您还需要检索位于该指针之前的长度前缀。docs.microsoft.com/en-us/previous-versions/windows/desktop/...
维姆科嫩

@WimCoenen:是的,但并不重要。存储在BSTR中的长度将是的可用长度的副本SecureString.Length
Ben Voigt

@BenVoigt啊,我不好。我以为SecureString没有公开有关该字符串的任何信息。
Wim Coenen

@WimCoenen:SecureString不是试图隐藏值,而是试图防止将值的副本复制到不能可靠地覆盖的区域中,例如垃圾收集的内存,页面文件等。目的是当SecureString生命周期结束时,绝对秘密的副本不会保留在内存中。它不会阻止您制作和泄漏副本,但绝对不会。
Ben Voigt

15

我认为,扩展方法是解决此问题的最舒适的方法。

我采用了Steve in CO的 出色答案,并将其放入扩展类中,如下所示,同时添加了第二种方法以支持其他方向(字符串->安全字符串),因此您可以创建安全字符串并将其转换为之后是普通字符串:

public static class Extensions
{
    // convert a secure string into a normal plain text string
    public static String ToPlainString(this System.Security.SecureString secureStr)
    {
        String plainStr=new System.Net.NetworkCredential(string.Empty, secureStr).Password;
        return plainStr;
    }

    // convert a plain text string into a secure string
    public static System.Security.SecureString ToSecureString(this String plainStr)
    {
        var secStr = new System.Security.SecureString(); secStr.Clear();
        foreach (char c in plainStr.ToCharArray())
        {
            secStr.AppendChar(c);
        }
        return secStr;
    }
}

这样,您现在可以像这样简单地来回转换字符串

// create a secure string
System.Security.SecureString securePassword = "MyCleverPwd123".ToSecureString(); 
// convert it back to plain text
String plainPassword = securePassword.ToPlainString();  // convert back to normal string

但是请记住,解码方法只能用于测试。


14

我认为最好是将SecureString依赖函数封装在匿名函数中,以更好地控制内存中的解密字符串(一旦固定)。

此代码段中解密SecureString的实现将:

  1. 将字符串固定在内存中(这是您要执行的操作,但此处大多数答案似乎都缺少此字符串)。
  2. 其引用传递给Func / Action委托。
  3. 从内存中清除它,然后释放模块中的GC finally

显然,与依靠不太理想的替代方法相比,“标准化”和维护调用方变得容易得多:

  • string DecryptSecureString(...)帮助函数返回解密的字符串。
  • 将此代码复制到任何需要的地方。

注意这里,您有两个选择:

  1. static T DecryptSecureString<T>这使您可以Func从调用方访问委托的结果(如DecryptSecureStringWithFunctest方法所示)。
  2. static void DecryptSecureString只是一个“无效”版本,Action在您实际上不希望/不需要返回任何内容的情况下(如DecryptSecureStringWithAction测试方法所示)雇用了一个委托。

两者的示例用法可在StringsTest包含的类中找到。

Strings.cs

using System;
using System.Runtime.InteropServices;
using System.Security;

namespace SecurityUtils
{
    public partial class Strings
    {
        /// <summary>
        /// Passes decrypted password String pinned in memory to Func delegate scrubbed on return.
        /// </summary>
        /// <typeparam name="T">Generic type returned by Func delegate</typeparam>
        /// <param name="action">Func delegate which will receive the decrypted password pinned in memory as a String object</param>
        /// <returns>Result of Func delegate</returns>
        public static T DecryptSecureString<T>(SecureString secureString, Func<string, T> action)
        {
            var insecureStringPointer = IntPtr.Zero;
            var insecureString = String.Empty;
            var gcHandler = GCHandle.Alloc(insecureString, GCHandleType.Pinned);

            try
            {
                insecureStringPointer = Marshal.SecureStringToGlobalAllocUnicode(secureString);
                insecureString = Marshal.PtrToStringUni(insecureStringPointer);

                return action(insecureString);
            }
            finally
            {
                //clear memory immediately - don't wait for garbage collector
                fixed(char* ptr = insecureString )
                {
                    for(int i = 0; i < insecureString.Length; i++)
                    {
                        ptr[i] = '\0';
                    }
                }

                insecureString = null;

                gcHandler.Free();
                Marshal.ZeroFreeGlobalAllocUnicode(insecureStringPointer);
            }
        }

        /// <summary>
        /// Runs DecryptSecureString with support for Action to leverage void return type
        /// </summary>
        /// <param name="secureString"></param>
        /// <param name="action"></param>
        public static void DecryptSecureString(SecureString secureString, Action<string> action)
        {
            DecryptSecureString<int>(secureString, (s) =>
            {
                action(s);
                return 0;
            });
        }
    }
}

StringsTest.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Security;

namespace SecurityUtils.Test
{
    [TestClass]
    public class StringsTest
    {
        [TestMethod]
        public void DecryptSecureStringWithFunc()
        {
            // Arrange
            var secureString = new SecureString();

            foreach (var c in "UserPassword123".ToCharArray())
                secureString.AppendChar(c);

            secureString.MakeReadOnly();

            // Act
            var result = Strings.DecryptSecureString<bool>(secureString, (password) =>
            {
                return password.Equals("UserPassword123");
            });

            // Assert
            Assert.IsTrue(result);
        }

        [TestMethod]
        public void DecryptSecureStringWithAction()
        {
            // Arrange
            var secureString = new SecureString();

            foreach (var c in "UserPassword123".ToCharArray())
                secureString.AppendChar(c);

            secureString.MakeReadOnly();

            // Act
            var result = false;

            Strings.DecryptSecureString(secureString, (password) =>
            {
                result = password.Equals("UserPassword123");
            });

            // Assert
            Assert.IsTrue(result);
        }
    }
}

显然,这不能防止通过以下方式滥用此功能,因此请注意不要这样做:

[TestMethod]
public void DecryptSecureStringWithAction()
{
    // Arrange
    var secureString = new SecureString();

    foreach (var c in "UserPassword123".ToCharArray())
        secureString.AppendChar(c);

    secureString.MakeReadOnly();

    // Act
    string copyPassword = null;

    Strings.DecryptSecureString(secureString, (password) =>
    {
        copyPassword = password; // Please don't do this!
    });

    // Assert
    Assert.IsNull(copyPassword); // Fails
}

编码愉快!


为什么不使用Marshal.Copy(new byte[insecureString.Length], 0, insecureStringPointer, (int)insecureString.Length);而不是本fixed节?
sclarke81 '19

@ sclarke81,个好主意,但您需要使用[char],而不是[byte]
mklement0

1
总体方法是有希望的,但是我认为您尝试固定包含不安全(纯文本)副本的托管字符串的方法是有效的:您固定的是已初始化为的原始字符串对象String.Empty,不是由创建和返回的新分配实例Marshal.PtrToStringUni()
mklement0

7

我根据rdev5回答创建了以下扩展方法。固定托管字符串非常重要,因为它可以防止垃圾收集器在其周围移动并留下无法擦除的副本。

我认为我的解决方案的优点是不需要不安全的代码。

/// <summary>
/// Allows a decrypted secure string to be used whilst minimising the exposure of the
/// unencrypted string.
/// </summary>
/// <typeparam name="T">Generic type returned by Func delegate.</typeparam>
/// <param name="secureString">The string to decrypt.</param>
/// <param name="action">
/// Func delegate which will receive the decrypted password as a string object
/// </param>
/// <returns>Result of Func delegate</returns>
/// <remarks>
/// This method creates an empty managed string and pins it so that the garbage collector
/// cannot move it around and create copies. An unmanaged copy of the the secure string is
/// then created and copied into the managed string. The action is then called using the
/// managed string. Both the managed and unmanaged strings are then zeroed to erase their
/// contents. The managed string is unpinned so that the garbage collector can resume normal
/// behaviour and the unmanaged string is freed.
/// </remarks>
public static T UseDecryptedSecureString<T>(this SecureString secureString, Func<string, T> action)
{
    int length = secureString.Length;
    IntPtr sourceStringPointer = IntPtr.Zero;

    // Create an empty string of the correct size and pin it so that the GC can't move it around.
    string insecureString = new string('\0', length);
    var insecureStringHandler = GCHandle.Alloc(insecureString, GCHandleType.Pinned);

    IntPtr insecureStringPointer = insecureStringHandler.AddrOfPinnedObject();

    try
    {
        // Create an unmanaged copy of the secure string.
        sourceStringPointer = Marshal.SecureStringToBSTR(secureString);

        // Use the pointers to copy from the unmanaged to managed string.
        for (int i = 0; i < secureString.Length; i++)
        {
            short unicodeChar = Marshal.ReadInt16(sourceStringPointer, i * 2);
            Marshal.WriteInt16(insecureStringPointer, i * 2, unicodeChar);
        }

        return action(insecureString);
    }
    finally
    {
        // Zero the managed string so that the string is erased. Then unpin it to allow the
        // GC to take over.
        Marshal.Copy(new byte[length], 0, insecureStringPointer, length);
        insecureStringHandler.Free();

        // Zero and free the unmanaged string.
        Marshal.ZeroFreeBSTR(sourceStringPointer);
    }
}

/// <summary>
/// Allows a decrypted secure string to be used whilst minimising the exposure of the
/// unencrypted string.
/// </summary>
/// <param name="secureString">The string to decrypt.</param>
/// <param name="action">
/// Func delegate which will receive the decrypted password as a string object
/// </param>
/// <returns>Result of Func delegate</returns>
/// <remarks>
/// This method creates an empty managed string and pins it so that the garbage collector
/// cannot move it around and create copies. An unmanaged copy of the the secure string is
/// then created and copied into the managed string. The action is then called using the
/// managed string. Both the managed and unmanaged strings are then zeroed to erase their
/// contents. The managed string is unpinned so that the garbage collector can resume normal
/// behaviour and the unmanaged string is freed.
/// </remarks>
public static void UseDecryptedSecureString(this SecureString secureString, Action<string> action)
{
    UseDecryptedSecureString(secureString, (s) =>
    {
        action(s);
        return 0;
    });
}

尽管您的代码不会泄漏字符串的副本,但它仍然代表着绝望之情。几乎对System.String对象进行的所有操作都将生成未固定和未擦除的副本。这就是为什么它没有内置的原因SecureString
Ben Voigt

不错,尽管您必须使用整个字符串为零new char[length](或length与相乘sizeof(char))。
mklement0

@BenVoigt:只要action委托人不创建临时的,固定的,然后清零的字符串的副本,此方法就应与SecureString自身一样安全或不安全-要使用后者,纯文本表示也必须考虑到安全字符串不是操作系统级别的构造,因此需要在某个时候创建​​它;相对的安全性来自控制该字符串的生存期并确保在使用后将其删除。
mklement0

@ mklement0:SecureString没有成员函数和重载运算符,这些函数会在各处进行复制。 System.String做。
Ben Voigt

1
@ mklement0:考虑到将其传递给NetworkCredential确实接受a 的构造函数,这是相当荒谬的SecureString
Ben Voigt

0

此C#代码就是您想要的。

%ProjectPath%/SecureStringsEasy.cs

using System;
using System.Security;
using System.Runtime.InteropServices;
namespace SecureStringsEasy
{
    public static class MyExtensions
    {
        public static SecureString ToSecureString(string input)
        {
            SecureString secureString = new SecureString();
            foreach (var item in input)
            {
                secureString.AppendChar(item);
            }
            return secureString;
        }
        public static string ToNormalString(SecureString input)
        {
            IntPtr strptr = Marshal.SecureStringToBSTR(input);
            string normal = Marshal.PtrToStringBSTR(strptr);
            Marshal.ZeroFreeBSTR(strptr);
            return normal;
        }
    }
}

0

我是从sclarke81得出的。我喜欢他的答案,并且使用派生词,但sclarke81的错误。我没有声誉,所以我无法发表评论。这个问题似乎很小,无法保证没有其他答案,我可以对其进行编辑。所以我做了。被拒绝了。所以现在我们有了另一个答案。

sclarke81我希望您终于看到了:

Marshal.Copy(new byte[length], 0, insecureStringPointer, length);

应该:

Marshal.Copy(new byte[length * 2], 0, insecureStringPointer, length * 2);

和完整的答案与错误修复:


    /// 
    /// Allows a decrypted secure string to be used whilst minimising the exposure of the
    /// unencrypted string.
    /// 
    /// Generic type returned by Func delegate.
    /// The string to decrypt.
    /// 
    /// Func delegate which will receive the decrypted password as a string object
    /// 
    /// Result of Func delegate
    /// 
    /// This method creates an empty managed string and pins it so that the garbage collector
    /// cannot move it around and create copies. An unmanaged copy of the the secure string is
    /// then created and copied into the managed string. The action is then called using the
    /// managed string. Both the managed and unmanaged strings are then zeroed to erase their
    /// contents. The managed string is unpinned so that the garbage collector can resume normal
    /// behaviour and the unmanaged string is freed.
    /// 
    public static T UseDecryptedSecureString(this SecureString secureString, Func action)
    {
        int length = secureString.Length;
        IntPtr sourceStringPointer = IntPtr.Zero;

        // Create an empty string of the correct size and pin it so that the GC can't move it around.
        string insecureString = new string('\0', length);
        var insecureStringHandler = GCHandle.Alloc(insecureString, GCHandleType.Pinned);

        IntPtr insecureStringPointer = insecureStringHandler.AddrOfPinnedObject();

        try
        {
            // Create an unmanaged copy of the secure string.
            sourceStringPointer = Marshal.SecureStringToBSTR(secureString);

            // Use the pointers to copy from the unmanaged to managed string.
            for (int i = 0; i < secureString.Length; i++)
            {
                short unicodeChar = Marshal.ReadInt16(sourceStringPointer, i * 2);
                Marshal.WriteInt16(insecureStringPointer, i * 2, unicodeChar);
            }

            return action(insecureString);
        }
        finally
        {
            // Zero the managed string so that the string is erased. Then unpin it to allow the
            // GC to take over.
            Marshal.Copy(new byte[length * 2], 0, insecureStringPointer, length * 2);
            insecureStringHandler.Free();

            // Zero and free the unmanaged string.
            Marshal.ZeroFreeBSTR(sourceStringPointer);
        }
    }

    /// 
    /// Allows a decrypted secure string to be used whilst minimising the exposure of the
    /// unencrypted string.
    /// 
    /// The string to decrypt.
    /// 
    /// Func delegate which will receive the decrypted password as a string object
    /// 
    /// Result of Func delegate
    /// 
    /// This method creates an empty managed string and pins it so that the garbage collector
    /// cannot move it around and create copies. An unmanaged copy of the the secure string is
    /// then created and copied into the managed string. The action is then called using the
    /// managed string. Both the managed and unmanaged strings are then zeroed to erase their
    /// contents. The managed string is unpinned so that the garbage collector can resume normal
    /// behaviour and the unmanaged string is freed.
    /// 
    public static void UseDecryptedSecureString(this SecureString secureString, Action action)
    {
        UseDecryptedSecureString(secureString, (s) =>
        {
            action(s);
            return 0;
        });
    }
}

好点子; 我对所引用的答案发表了评论,该评论应通知OP。
mklement0

0

根据sclarke81解决方案和John Flaherty修复的最终工作解决方案是:

    public static class Utils
    {
        /// <remarks>
        /// This method creates an empty managed string and pins it so that the garbage collector
        /// cannot move it around and create copies. An unmanaged copy of the the secure string is
        /// then created and copied into the managed string. The action is then called using the
        /// managed string. Both the managed and unmanaged strings are then zeroed to erase their
        /// contents. The managed string is unpinned so that the garbage collector can resume normal
        /// behaviour and the unmanaged string is freed.
        /// </remarks>
        public static T UseDecryptedSecureString<T>(this SecureString secureString, Func<string, T> action)
        {
            int length = secureString.Length;
            IntPtr sourceStringPointer = IntPtr.Zero;

            // Create an empty string of the correct size and pin it so that the GC can't move it around.
            string insecureString = new string('\0', length);
            var insecureStringHandler = GCHandle.Alloc(insecureString, GCHandleType.Pinned);

            IntPtr insecureStringPointer = insecureStringHandler.AddrOfPinnedObject();

            try
            {
                // Create an unmanaged copy of the secure string.
                sourceStringPointer = Marshal.SecureStringToBSTR(secureString);

                // Use the pointers to copy from the unmanaged to managed string.
                for (int i = 0; i < secureString.Length; i++)
                {
                    short unicodeChar = Marshal.ReadInt16(sourceStringPointer, i * 2);
                    Marshal.WriteInt16(insecureStringPointer, i * 2, unicodeChar);
                }

                return action(insecureString);
            }
            finally
            {
                // Zero the managed string so that the string is erased. Then unpin it to allow the
                // GC to take over.
                Marshal.Copy(new byte[length * 2], 0, insecureStringPointer, length * 2);
                insecureStringHandler.Free();

                // Zero and free the unmanaged string.
                Marshal.ZeroFreeBSTR(sourceStringPointer);
            }
        }

        /// <summary>
        /// Allows a decrypted secure string to be used whilst minimising the exposure of the
        /// unencrypted string.
        /// </summary>
        /// <param name="secureString">The string to decrypt.</param>
        /// <param name="action">
        /// Func delegate which will receive the decrypted password as a string object
        /// </param>
        /// <returns>Result of Func delegate</returns>
        /// <remarks>
        /// This method creates an empty managed string and pins it so that the garbage collector
        /// cannot move it around and create copies. An unmanaged copy of the the secure string is
        /// then created and copied into the managed string. The action is then called using the
        /// managed string. Both the managed and unmanaged strings are then zeroed to erase their
        /// contents. The managed string is unpinned so that the garbage collector can resume normal
        /// behaviour and the unmanaged string is freed.
        /// </remarks>
        public static void UseDecryptedSecureString(this SecureString secureString, Action<string> action)
        {
            UseDecryptedSecureString(secureString, (s) =>
            {
                action(s);
                return 0;
            });
        }
    }

-5
// using so that Marshal doesn't have to be qualified
using System.Runtime.InteropServices;    
//using for SecureString
using System.Security;
public string DecodeSecureString (SecureString Convert) 
{
    //convert to IntPtr using Marshal
    IntPtr cvttmpst = Marshal.SecureStringToBSTR(Convert);
    //convert to string using Marshal
    string cvtPlainPassword = Marshal.PtrToStringAuto(cvttmpst);
    //return the now plain string
    return cvtPlainPassword;
}

此答案有内存泄漏。
Ben Voigt

@BenVoigt您能否进一步说明这是怎么发生内存泄漏的?
El Ronnoco '16

4
@ElRonnoco:没有任何东西可以BSTR显式释放它,而且它不是.NET对象,因此垃圾收集器也不会处理它。与5年前发布且不会泄漏的stackoverflow.com/a/818709/103167进行比较。
Ben Voigt

此答案不适用于非Windows平台。的PtrToStringAuto是错误的解释,请参阅:github.com/PowerShell/PowerShell/issues/...
K.弗兰克

-5

如果使用a StringBuilder而不是a string,则可以在完成后覆盖内存中的实际值。这样一来,密码就不会在内存中徘徊,直到垃圾回收将其清除为止。

StringBuilder.Append(plainTextPassword);
StringBuilder.Clear();
// overwrite with reasonably random characters
StringBuilder.Append(New Guid().ToString());

2
尽管这是事实,但是垃圾回收器可能仍会在世代压缩期间在内存中四处移动StringBuilder缓冲区,这会使“覆盖实际值”失败,因为存在另一个(或更多)未被破坏的剩余副本。
Ben Voigt

4
这甚至无法远程回答问题。
杰伊·沙利文
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.