如何将 SecureString 转换为 System.String?

所有关于通过创建 System.String 来解除 SecureString 的安全保护的保留意见?

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

我相信很多熟悉 SecureString 的人都会回答,永远不要把 SecureString 转换成普通的。NET 字符串,因为它删除了所有的安全保护。我知道.但是现在我的程序用普通的字符串做所有的事情,我正在尝试提高它的安全性,尽管我将使用一个 API 来返回一个 SecureString 给我,我是 没有尝试使用它来提高我的安全性。

我知道马歇尔。SecureStringToBSTR,但是我不知道如何使用这个 BSTR 来创建一个系统。把它们串起来。

对于那些谁可能要求知道为什么我会想要这样做,那么,我从一个用户和提交它作为一个 html 表单 POST 登录用户到一个网站的密码。所以,这真的需要使用托管的、未加密的缓冲区来完成。如果我甚至可以访问非托管的、未加密的缓冲区,我想我就可以在网络流上进行逐字节的流写操作,并希望这样可以在整个过程中保证密码的安全。我希望至少能找到其中一种情况的答案。

158215 次浏览

该死。在发布了这个之后,我在 这篇文章中找到了答案。但是,如果有人知道如何访问这个方法公开的 IntPtr 非托管、未加密的缓冲区,每次访问一个字节,这样我就不必为了保持较高的安全性而创建托管字符串对象,请添加一个答案。:)

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


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

使用 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);
}
}

显然,您知道这会破坏 SecureString 的整个用途,但我还是要重述它。

如果您想要一行程序,请尝试这样做: (仅.NET 4及以上版本)

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

其中 securePassword 是 SecureString。

// 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;
}

我认为 SecureString相关函数最好在匿名函数中将其相关逻辑转换为 封装,以便更好地控制内存中解密的字符串(一旦固定)。

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

  1. 将字符串固定在内存中(这是您想要做的,但在这里的大多数答案中似乎都没有)。
  2. 它的参考传递给 Func/Action 委托。
  3. 从内存中删除它,并在 finally块中释放 GC。

这显然使得“标准化”和维护呼叫者比依赖不那么理想的替代品更容易:

  • string DecryptSecureString(...)助手函数返回解密的字符串。
  • 在需要的地方复制此代码。

注意,这里有两个选项:

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

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

字符串

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
}

编程愉快!

如果使用 StringBuilder而不是 string,则可以在完成后覆盖内存中的实际值。这样,密码就不会在内存中挂起,直到垃圾回收将其拾起。

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

在我看来,扩展方法是解决这个问题最舒适的方法。

我将 指挥官史蒂夫 答得好放入一个扩展类,如下所示,同时还添加了另一个方法来支持另一个方向(string-> security string) ,这样您就可以创建一个安全的字符串,然后将其转换为普通的字符串:

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 (normal string)
String plainPassword = securePassword.ToPlainString();

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


如果您对细节感兴趣: NetworkCredential在内部使用

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


private string MarshalToString(SecureString sstr)
{
if (sstr == null || sstr.Length == 0)
{
return string.Empty;
}
IntPtr intPtr = IntPtr.Zero;
string empty = string.Empty;
try
{
intPtr = Marshal.SecureStringToGlobalAllocUnicode(sstr);
return Marshal.PtrToStringUni(intPtr);
}
finally
{
if (intPtr != IntPtr.Zero)
{
Marshal.ZeroFreeGlobalAllocUnicode(intPtr);
}
}
}


private unsafe SecureString MarshalToSecureString(string str)
{
if (string.IsNullOrEmpty(str))
{
return new SecureString();
}
fixed (char* ptr = str)
{
char* value = ptr;
return new SecureString(value, str.Length);
}
}

SecureString转换为 string,反之亦然。

SecureString内部使用

using System.Runtime.InteropServices;


[DllImport("crypt32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool CryptProtectMemory(SafeBuffer pData, uint cbData, uint dwFlags);


[DllImport("crypt32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool CryptUnprotectMemory(SafeBuffer pData, uint cbData, uint dwFlags);

加密/解密字符串数据(pData缓冲区)的原位(即在适当的位置,而不复制它)。这意味着,您不知道 SecureString 是如何在内部进行加密的。但是由于您可以毫不费力地解密它,所以它并不真正安全——它只是防止查看内存转储的人过于容易地找到纯文本字符串。换句话说,如果不能很容易地在内存转储中找到字符串,显然就无法对其进行解密(隐晦式安全)。

我创建了以下基于 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;
});
}

这个 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;
}
}
}

我从 这个答案来自 sclarke81得到的。我喜欢他的答案,我用的是导数,但是 sclarke81有个 bug。我没有名声,所以我无可奉告。这个问题看起来很小,不需要另一个答案,我可以编辑它。我照做了。被拒绝了。现在我们有了另一个答案。

我希望你看到这个(最后) :

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;
});
}
}

根据 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;
});
}
}

作为答案被接受的代码是正确的,并将在大多数情况下工作,但正如在使用 BSTR 的评论中提到的,将是更好的,并将涵盖所有情况:

private string SecureStringToString(SecureString value) {
IntPtr valuePtr = IntPtr.Zero;
try {
valuePtr = Marshal.SecureStringToBSTR(value);
return Marshal.PtrToStringBSTR(valuePtr);
} finally {
Marshal.ZeroFreeBSTR(valuePtr);
}
}

使用以下方法:

var plaintextPwd = new System.Net.NetworkCredential("", <securestring with your encrypted password>).Password