C#中SecureString转换为Byte[]

11
我怎样才能获得一个SecureStringbyte[]等效物(我从PasswordBox中获取)?
我的目标是使用CryptoStream将这些字节写入文件,而该类的Write方法需要一个byte[]输入,因此我想将SecureString转换为byte[],以便可以用它与CryptoStream一起使用。
编辑:我不想使用字符串,因为这样就失去了使用SecureString的意义。
5个回答

13

假设你想使用字节数组,并在完成后将其删除,那么你应该封装整个操作,以便在完成后进行清理:

public static T Process<T>(this SecureString src, Func<byte[], T> func)
{
    IntPtr bstr = IntPtr.Zero;
    byte[] workArray = null;
    GCHandle? handle = null; // Hats off to Tobias Bauer
    try
    {
        /*** PLAINTEXT EXPOSURE BEGINS HERE ***/
        bstr = Marshal.SecureStringToBSTR(src);
        unsafe
        {
            byte* bstrBytes = (byte*)bstr;
            workArray = new byte[src.Length * 2];
            handle = GCHandle.Alloc(workArray, GCHandleType.Pinned); // Hats off to Tobias Bauer
            for (int i = 0; i < workArray.Length; i++)
                workArray[i] = *bstrBytes++;
        }

        return func(workArray);
    }
    finally
    {
        if (workArray != null)
            for (int i = 0; i < workArray.Length; i++)
                workArray[i] = 0;
        handle.Free();
        if (bstr != IntPtr.Zero)
            Marshal.ZeroFreeBSTR(bstr);
        /*** PLAINTEXT EXPOSURE ENDS HERE ***/
    }
}

以下是一个用例的样例:

private byte[] GetHash(SecureString password)
{
    using (var h = new SHA256Cng()) // or your hash of choice
    {
        return password.Process(h.ComputeHash);
    }
}

无需繁琐的步骤,无需在内存中留下明文。

请注意,传递给func()的字节数组包含纯Unicode格式的明文,对于大多数加密应用程序来说,这不应该是一个问题。


3
Eric - 我觉得你在使用 workArray 上有一个小问题。虽然归零是好的,但如果垃圾回收器决定移动它,那么你的敏感数据就会变成内存中的“垃圾”。在放置敏感数据之前,你需要将 byte 数组固定在内存中。 - ArielB
1
不错,@ArielB。我已经添加了GCHandle.Alloc()和handle.Free()来固定workArray,直到它被清除。迟做总比不做好。谢谢! - Eric Lloyd
根据@tobias-bauer的建议,我已经更新了这个版本(除了被忽略的异常)。干得好,Tobias! - Eric Lloyd

3

我从原始答案进行修改,以处理Unicode

IntPtr unmanagedBytes = Marshal.SecureStringToGlobalAllocUnicode(password);
byte[] bValue = null;
try
{
    byte* byteArray = (byte*)unmanagedBytes.GetPointer();

    // Find the end of the string
    byte* pEnd = byteArray;
    char c='\0';
    do
    {
        byte b1=*pEnd++;
        byte b2=*pEnd++;
        c = '\0';
        c= (char)(b1 << 8);                 
        c += (char)b2;
    }while (c != '\0');

    // Length is effectively the difference here (note we're 2 past end) 
    int length = (int)((pEnd - byteArray) - 2);
    bValue = new byte[length];
    for (int i=0;i<length;++i)
    {
        // Work with data in byte array as necessary, via pointers, here
        bValue[i] = *(byteArray + i);
    }
}
finally
{
    // This will completely remove the data from memory
    Marshal.ZeroFreeGlobalAllocUnicode(unmanagedBytes);
}

2
与 ANSI C 字符串不同,BSTR 可以包含空字符,因此您的空字符扫描无效。只需使用源 SecureString 的 Length 成员(乘以 2 以获取字节计数)。 - Eric Lloyd

1

由于我没有足够的声望在Eric的答案下评论,所以我不得不发布这篇文章来发表我的观点。

在我看来,Eric的代码存在问题,因为GCHandle.Alloc(workArray, ...)的操作不正确。它不应该固定workArraynull值,而是应该固定稍后将创建的实际数组。

此外,handle.Free()可能会抛出InvalidOperationException异常,因此我建议将其放在Marshal.ZeroFreeBSTR(...)之后,保证至少二进制字符串bstr指向的位置被清零。

修改后的代码应该是这样的:

public static T Process<T>(this SecureString src, Func<byte[], T> func)
{
    IntPtr bstr = IntPtr.Zero;
    byte[] workArray = null;
    GCHandle? handle = null; // Change no. 1
    try
    {
        /*** PLAINTEXT EXPOSURE BEGINS HERE ***/
        bstr = Marshal.SecureStringToBSTR(src);
        unsafe
        {
            byte* bstrBytes = (byte*)bstr;
            workArray = new byte[src.Length * 2];
            handle = GCHandle.Alloc(workArray, GCHandleType.Pinned); // Change no. 2

            for (int i = 0; i < workArray.Length; i++)
                workArray[i] = *bstrBytes++;
        }

        return func(workArray);
    }
    finally
    {
        if (workArray != null)
            for (int i = 0; i < workArray.Length; i++)
                workArray[i] = 0;
        
        if (bstr != IntPtr.Zero)
            Marshal.ZeroFreeBSTR(bstr);

        handle?.Free(); // Change no. 3 (Edit: no try-catch but after Marshal.ZeroFreeBSTR)

        /*** PLAINTEXT EXPOSURE ENDS HERE ***/
    }
}

这些修改确保正确的byte数组被固定在内存中(更改1和2)。此外,它们避免了在handle?.Free()引发异常的情况下仍在内存中加载未加密的二进制字符串(更改3)。

GCHandle.Alloc(...) 的使用很好。如果您没有对其运行单元测试,会发生什么?我将更新我的答案以匹配。但是,除非您确实要处理失败,否则永远不要吞咽异常。 - Eric Lloyd
@EricLloyd 感谢您更新答案!关于异常吞噬,我还有一件事:我完全同意您的观点。但是,我建议在Marshal.ZeroFreeBSTR(...)之后放置handle?.Free(),因为后者在失败时不会抛出异常,并确保bstr数组被清零。我会相应地调整我的答案。 - Tobias Bauer

0

这个100%托管代码对我来说似乎有效:

var pUnicodeBytes = Marshal.SecureStringToGlobalAllocUnicode(secureString);
try
{
    byte[] unicodeBytes = new byte[secureString.Length * 2];

    for( var idx = 0; idx < unicodeBytes.Length; ++idx )
    {
        bytes[idx] = Marshal.ReadByte(pUnicodeBytes, idx);
    }

    return bytes;
}
finally
{
    Marshal.ZeroFreeGlobalAllocUnicode(pUnicodeBytes);
}

-2
根据此文,http://www.microsoft.com/indonesia/msdn/credmgmt.aspx,您可以将其转换为标准的C#字符串,然后将其转换为字节数组:
static string SecureStringToString( SecureString value )
{
  string s ;
  IntPtr p = Marshal.SecureStringToBSTR( value );
  try
  {
    s = Marshal.PtrToStringBSTR( p ) ;
  }
  finally
  {
    Marshal.FreeBSTR( p ) ;
  }
  return s ;
}

根据这个答案或者如何将SecureString转换为System.String?,你可以使用Marshal.ReadByteMarshal.ReadInt16IntPtr上获取所需内容。

2
虽然这样做会打破 SecureString 的目的,即允许安全地在内存中存储敏感字符串数据,例如密码。 - Adrian
我不想使用string,那么是否有其他方法可以保持密码安全并完成相同的事情? - inixsoftware

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接