如何在C#中正确处理密码

16

众所周知,C#的string相当不安全,它无法被固定在RAM中,GC可移动、复制并在RAM中留下多个跟踪,并且RAM可能被交换并作为文件可供阅读,更不用提其他几个已知事实了。

为了减轻这一问题,Microsoft推出了SecureString。然而,问题是:正确使用它的方式是什么?

我之所以问这个问题,是因为迟早会需要提取内部字符串。是的,SecureString确实允许我们每次写入一个char并提供一些其他微小的用途,同时隐藏它的秘密,但有时我们需要一次性获取整个字符串或类似的东西。到目前为止,我看到的所有实现都在最后使用了一个常规的string,其中包含从SecureString中提取和解密的内容,尽管它可能涉及一些复杂的代码来绕过.NET的string

在我的情况下,我使用了一个密码哈希库叫做BCrypt。它使用常规的string来计算哈希值,我认为这远非理想。我没有找到任何可以直接接受SecureString的库,所以我别无选择,只能使用它。

目前我运行以下代码:

SecureString password; // This usually comes from a PasswordBox Property
IntPtr passwordBSTR = Marshal.SecureStringToBSTR(password);

string insecurePassword = Marshal.PtrToStringBSTR(passwordBSTR);
Marshal.ZeroFreeBSTR(passwordBSTR);
passwordBSTR = default(IntPtr);
password.Clear();

// Use the insecurePassword....usually as:
bool result = BCrypt.Net.BCrypt.Verify(insecurePassword, user.Password); // This hashes the provided password and compares it with another BCrypt hash.

insecureString = string.Empty; // hoping for the GC to act now, fingers crossed.

// use result and etc...

这种方式会使得密码原始的stringBCrypt方法多次复制,甚至在其中更多次。

这让我有一些问题:

1 - BSTR字符串是否像C字符串一样具有内存管理的意义?它们是否固定在RAM中,我可以根据自己的需要操纵和删除它们,从而更安全地在C#中使用它们或者将其与C++代码进行交互,以便消除大量的.NET string操作(如验证密码字符串是否为空或空白,长度是否正确,密码强度是否符合要求)?

2 - 如果问题1的答案是“是”,那么我是否应该花费一些时间将BSTR字符串传递给Interop C++代码来计算其哈希值和验证?

3 - 我的后续代码是否正确,或者我在SecureString操作方面漏掉了什么?

只是为了明确:我提出这个问题的主要目的是评估是否应该将所有密码处理代码从我的C#代码转移到C++中(可能使用C++/CLI或Interop),以实现比C#提供更安全的代码。 在密码字符串哈希方面,将创建多个字符串并将用户的秘密信息分散在RAM中,这种情况让我非常关注。

例如:我可以编写如下的方法:

public string SecureHash(SecureString password)
这种方法将提取 SecureString 值并将其作为 BSTR 字符串(或其他提取选项,我真的不知道它们之间的优劣)传递给 C++ 哈希方法,以便我们可以控制密码在内存中的所有位置。
我这样做是为了避免大量重构我的代码,最后发现我一直错误地使用了 SecureStrings,而且已经有针对此问题的解决方案了,或者说存在虚假的安全感,有时会发生这种情况,因为我不知道如何在内存中实现 BSTR 字符串或其他 SecureStrings提取选项(Unicode、ANSI 等)。

你可以重写BCrypt,使其接受一个安全字符串作为参数。根据https://msdn.microsoft.com/en-us/library/system.security.securestring(v=vs.110).aspx#HowSecure的说明,如果需要频繁修改字符串等操作,则其实并不是那么安全。 - Matt Clark
@MattClark 这正是我的想法。我的问题是它是否值得这样做。我不了解BSTR字符串的内存行为,所以我可能会因为虚假的安全感而遇到所有麻烦。 - Michel Feinstein
BSTR实际上并没有比SecureString更有优势,特别是因为BSTR只是未加密的非托管内存中的字符串。在安全字符串和BSTR的情况下,您都可以手动进行垃圾回收。 - Matt Clark
我并不打算从SecureString中获得任何东西,而是想将SecureString传递给一个方法来哈希它的秘密值。在方法内部,只会进行BSTR操作,因为SecureString的值将被提取到BSTR中。但我不确定这比简单字符串更安全,因为我不了解BSTR在内存中的行为,即使BSTR是最好的选择,也有其他选项可以解密,如CoTaskMemAnsiCoTaskMemUnicodeGlobalAllocAnsiGlobalAllocUnicode - Michel Feinstein
1
如果你在Windows上运行,我会简单地与本机Windows API进行交互,例如BCryptHashData函数(有点hacky):http://codereview.stackexchange.com/questions/93111/hashing-a-securestring-using-cryptography-next-generation - Simon Mourier
显示剩余2条评论
3个回答

2
  1. Marshal.SecureStringToBSTR()方法将创建一个BSTR字符串(即字节数组)在非托管内存中并返回指向它的指针;这意味着底层值被固定。该值本身是代表您的字符串的Unicode字符序列,前缀为字符串长度,后跟两个连续的空字符 (https://msdn.microsoft.com/en-us/library/windows/desktop/ms221069(v=vs.85).aspx)。 另一方面,Marshal.PtrToStringBSTR()将返回一个不安全的.NET System.String对象。
  2. 我认为你有以下几种选择:

    • 将SecureString对象转换为非托管对象,然后传递给任何计算哈希的非托管库
    • 在整个堆栈中使用SecureString,在您的情况下,这意味着重写BCrypt.Net以使用SecureString对象(该库是开源且非常小:https://bcrypt.codeplex.com/SourceControl/latest#BCrypt.Net/BCrypt.cs),或查找替代的托管实现。但由于SecureString没有提供读取其数据的方法,因此您最终需要将其转换为,很可能是非托管的,字符串(您可以从托管代码中使用该非托管数据)。
  3. 您错过了SecureString的正确处理(在“using”或try/finally内)。


1- 如果我将哈希转换为BSTR或Unicode,是否会对哈希产生影响?Marshal有一些转换选项,我不确定哪个是最好的。2- 我认为使用托管实现无法保证安全,无论如何,我都必须在C#中提取SecureString的内容,这可能会导致由托管代码引起的其他问题,我仍在考虑使用非托管代码或库是最好的选择,并且可能加入两种实现,一个使用SecureString作为输入并在内部处理为非托管代码的托管库,返回.NET字符串。 - Michel Feinstein
3- 是的,我没有提到它,因为这部分已经有很好的文档说明了。我正在使用Clear()方法,一旦我从PasswordBox中获取完数据后就会使用它。 - Michel Feinstein
#1:如果您的算法将pwd的哈希计算为一系列“字符”,则无论您使用什么表示(BSTR,UTF-8等),都不应有任何区别。另一方面,如果您从pwd的字节“表示”计算哈希,则会有区别。虽然我不会选择后者,因为您可能会引入微妙的错误(例如,您的密码当前在BSTR中,您计算了哈希并将其存储在数据库中,然后2年后您的前端更改并开始接收UTF-8中的pwds,这会导致不同的哈希值,没有人可以再登录)。 - johnnyjob
你是对的,我关于SecureString API的理解有误。我以为可以逐个字符读取,但我已经更新了回答。 - johnnyjob
#1: 那么,当我将.NET字符串转换为BSTR或其他类型时,它们的字节都会相同吗?只有终止符('\0''\0')会改变吗? - Michel Feinstein
显示剩余2条评论

0

给SecureString添加一个扩展方法怎么样:

using System;
using System.Runtime.InteropServices;

public static class SecureStringExtensions 
{
    public static T Decrypt<T>(this SecureString ss, Func<string, T> handler) 
    {
        if (handler == null)
        {
            throw new ArgumentNullException(nameof(handler));
        }

        var ptr = Marshal.SecureStringToBSTR(ss);
        try {
            return handler(Marshal.PtrToStringBSTR(ptr));
        } finally {
            Marshal.ZeroFreeBSTR(ptr);
        }
    }
}

这种方式,解密后的字符串会留在栈上,并且很快就会被未来的栈变量覆盖。


这不是问题所在...问题在于所有 C# 的 bcrypt 框架都使用不安全的字符串...你的代码只是一种将 BSTR 字符串从 SecureString 中提取出来的封装方式,供一个已经能够处理 BSTR 字符串的函数使用...只是一个辅助方法,不会对任何密码字符串进行哈希或执行任何特殊操作...我无法使用它获取一个 bcrypt 哈希,我只能获得底层的 BSTR 字符串,这不是问题的全部内容。 - Michel Feinstein

-1
为什么不使用单向加密(哈希)呢?将随机盐与加密后的密码保存并推送这两个值。这样,您可以使用相同的盐加密要检查的密码并比较加密后的值。我建议使用SHA-256或更强大的算法。
至于安全性,这基本上是Microsoft在C# AspNet Membership中使用的方法。由于这基本上是Dot Net的行业标准,因此这种安全级别对您也应该适用。
以下是一个好的源代码来了解如何实现这一点: http://www.obviex.com/samples/hash.aspx

1
你提供的是2002年的代码形式。到了2016年,情况已经完全改变了。BCrypt是当前用于密码哈希存储的标准(PBKDF2是第二好的选择,scrypt也是一个不错的解决方案,只是太新了,大多数人不信任)。SHA256太快了,现在可以使用在GPU上运行的字典攻击找到匹配项。参考链接:http://blog.ircmaxell.com/2011/08/rainbow-table-is-dead.html - Michel Feinstein

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