Rfc2898DeriveBytes + PBKDF2 + SecureString是否可以使用安全字符串而不是字符串?

Noo*_*Tom 5 c# cryptography securestring pbkdf2

我有一个函数GetPassword,它返回一个SecureString类型.

当我将此安全字符串传递Rfc2898DeriveBytes给生成密钥时,Visual Studio会显示错误.我有限的知识告诉我,这是因为Rfc2898DeriveBytes只接受一个字符串而不是一个安全的字符串.这有解决方法吗?

//read the password from terminal
Console.Write("Insert password");
securePwd = myCryptography.GetPassword();

//dont know why the salt is initialized like this
byte[] salt = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0xF1, 0xF0, 0xEE, 0x21, 0x22, 0x45 };
 try
 {   //PBKDF2 standard 
     Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(securePwd, salt, iterationsPwd);
Run Code Online (Sandbox Code Playgroud)

eri*_*son 6

显然,您可以通过该功能违反由其提供的保护SecureString暴露其内部状态Marshal.SecureStringToBSTR().

而不是创建String结果,将内容复制Byte[]到传递给Rfc2898DeriveBytes.创建一个String会阻止你破坏密码信息,允许它无限期地挂在堆中,或者被分页到磁盘,这反过来又增加了攻击者找到它的机会.相反,您应该在完成使用后立即销毁密码,方法是使用零填充数组.出于同样的原因,您还应该在BSTR将其复制到的每个元素时为其分配零Byte[].

应为每个散列密码随机选择Salt,而不是固定的可预测值,否则可能会预先计算字典攻击.您应该迭代数万次以防止暴力攻击.


Maa*_*wes 5

在做了一些研究并查看了之前提到的 stackoverflow 上的答案之后SecureString,这个答案几乎肯定是:“不”。只有API的创建者才能SecureString在内部接受并正确处理。他们只能在平台的帮助下做到这一点。

如果您(作为用户)可以检索纯文本,那么您一开始String就否定了使用的大部分优点。SecureString它甚至会有点危险,因为您将创建看起来安全的代码,但实际上根本不安全(编辑:至少在保护内存数据时不安全)。


Der*_*k W 5

我发现有趣的是,Rfc2898DeriveBytes该类不支持SecureString传递用于派生密钥的密码的重载。

WPF允许将密码作为控件SecureString对象处理PasswordBox。似乎是一种浪费,由于我们无法将a传递SecureString给构造函数,因此失去了该控件提供的附加安全性。但是,Erickson提出了使用byte[]替代而不是string重载的优点,因为正确地管理byte[]内存中的内容比使用a 相对容易string

埃里克森的建议为灵感,我想到了以下包装器,该包装器应允许使用受密码保护的值,并SecureString以最小限度地暴露内存中的纯文本值。

private byte[] DeriveKey(SecureString password, byte[] salt, int iterations, int keyByteLength)
{
    IntPtr ptr = Marshal.SecureStringToBSTR(password);
    byte[] passwordByteArray = null;
    try
    {
        int length = Marshal.ReadInt32(ptr, -4);
        passwordByteArray = new byte[length];
        GCHandle handle = GCHandle.Alloc(passwordByteArray, GCHandleType.Pinned);
        try
        {
            for (int i = 0; i < length; i++)
            {
                passwordByteArray[i] = Marshal.ReadByte(ptr, i);
            }

            using (var rfc2898 = new Rfc2898DeriveBytes(passwordByteArray, salt, iterations))
            {
                return rfc2898.GetBytes(keyByteLength);
            }
        }
        finally
        {
            Array.Clear(passwordByteArray, 0, passwordByteArray.Length);  
            handle.Free();
        }
    }
    finally
    {
        Marshal.ZeroFreeBSTR(ptr);
    }
}
Run Code Online (Sandbox Code Playgroud)

这种方法利用了一个事实,即BSTR是一个指针,该指针指向带有四个字节长度前缀的数据字符串的第一个字符。

重要事项:

  • 通过包装Rfc2898DeriveBytes使用语句,可以确保以确定的方式处理它。这一点很重要,因为它有一个内部HMACSHA1对象,KeyedHashAlgorithm并且需要在调用Dispose时将其拥有的密钥(密码)副本清零。有关完整的详细信息,请参见参考源
  • 一旦完成操作,便将BSTR其清零并通过ZeroFreeBSTR释放它。
  • 最后,我们将密码副本清零(清除)。
  • 更新:添加了固定byte[]。如该答案的注释中所述,如果byte[]未固定,则垃圾收集器可以在收集期间重新定位对象,而我们将无法将原始副本归零。

这样应该将纯文本密码在内存中保留的时间最短,并且不会削弱使用SecureString过多密码的好处。但是,如果攻击者可以访问RAM,则可能会遇到更大的问题。还有一点是,我们只能管理自己的密码副本,我们正在使用的API很可能会错误地管理(而不是零清除/清除)其副本。据我所知,情况并非如此Rfc2898DeriveBytes,尽管它们的byte[]密钥(密码)副本未固定,因此,如果在归零之前将其移入堆,则阵列的痕迹可能会徘徊。此处的信息是代码看起来很安全,但可能存在问题。

如果有人在此实现中发现任何严重漏洞,请告诉我。