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

Posted

技术标签:

【中文标题】Rfc2898DeriveBytes + PBKDF2 + SecureString 是不是可以使用安全字符串而不是字符串?【英文标题】:Rfc2898DeriveBytes + PBKDF2 + SecureString is it possible to use a secure string instead of a string?Rfc2898DeriveBytes + PBKDF2 + SecureString 是否可以使用安全字符串而不是字符串? 【发布时间】:2012-04-01 19:20:27 【问题描述】:

我有一个函数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);

【问题讨论】:

...哦,你,我不知道为什么盐是这样的:D 我现在在做什么,是我使用securePwd.ToString 问题是它使使用secureString的整个过程毫无价值:D 就是,以//read the password from terminal开头的 ToString() 返回SecureString的内容?你确定吗?文档不是这么说的。 【参考方案1】:

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

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

erickson 的 建议为灵感,我想出了以下包装器,该包装器应该允许使用受 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);
    

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

要点:

通过将Rfc2898DeriveBytes 包装在 using 语句中,可确保以确定的方式处理它。这很重要,因为它有一个内部 HMACSHA1 对象,它是一个 KeyedHashAlgorithm,并且需要在调用 Dispose 时将其拥有的密钥(密码)的副本清零。详情请参阅Reference Source。 一旦我们完成了BSTR,我们会将其归零并通过ZeroFreeBSTR 释放它。 最后,我们将密码副本归零(清除)。 更新:添加了byte[] 的固定。正如 answer 的 cmets 中所讨论的,如果 byte[] 未固定,那么垃圾收集器可以在收集期间重新定位对象,我们将无法将原始副本归零。

这应该将明文密码保留在内存中的时间最短,并且不会过多地削弱使用SecureString 的收益。虽然,如果攻击者可以访问 RAM,您可能会遇到更大的问题。另一点是我们只能管理我们自己的密码副本,我们使用的 API 很可能管理不善(不是清零/清除)他们的副本。据我所知,Rfc2898DeriveBytes 的情况并非如此,尽管他们的byte[] 密钥(密码)副本没有被固定,因此如果在归零之前在堆中移动数组的痕迹可能会残留出去。这里的信息是代码看起来很安全,但问题可能隐藏在下面。

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

【讨论】:

这正是我希望找到的,而且我相信这是最安全的。在我的简短研究中,我相信你已经涵盖了每一点。因为Rfc2898DeriveBytes 的实现没有固定byte[],如果垃圾收集器重新定位它,内存可能会在更长的时间内可用,从而暴露攻击向量。但是,我们知道如果攻击者可以访问 RAM,这是我们最不关心的问题。您的解决方案将未固定敏感数据的外观限制为 API 的行为。 我想知道我们是否可以直接返回rfc2898,同时仍然清除passwordByteArray。然后,您可以在 DeriveKey 函数调用上使用 using。似乎你可以从Rfc2898DeriveBytes uses HMAC 开始执行此操作,它会创建一个.Clone() of your byte array。 它也是clears the array on disposalcalling the base method。阵列虽然没有固定。通过返回DeriveKey,您可以根据需要生成任意数量的字节。我在powershell中测试了返回rfc2898,它似乎工作。使用固定盐更改密码会提供不同的密钥,这意味着密码被克隆并且不会被归零。【参考方案2】:

显然,您可以违反SecureString 和expose its internal state via the Marshal.SecureStringToBSTR() function. 提供的保护

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

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

【讨论】:

谢谢,但我需要在一台机器上用密码加密文件,然后在另一台机器上用相同的密码解密。因此我认为盐必须相同,否则第二台机器将无法解密它。我说的对吗? 我选择另一个作为答案,因为它更“正式”,但我真的很感谢你的棘手建议 @NoobTom 是的,当然两台机器上的盐必须相同。当使用密码散列进行身份验证时,应存储散列、盐和迭代次数,以便在出现密码时重新计算散列,以查看新散列是否与存储的散列匹配。永远不要使用硬编码的哈希进行密码验证。 注意:这也取决于 API 负责管理和归零它自己的潜在密钥副本。查看Reference Source:只要密码字节数组为64 字节或更少,那么Rfc2898DeriveBytes 对象内部的HMACSHA1 对象就有它自己的密码字节数组的克隆副本。值得庆幸的是,HMACSHA1 是一个 KeyedHashAlgorithm,它在对 Dispose 的调用中提供了它的密钥副本的归零。 我还要补充一点,固定byte[] 也很重要。如果byte[] 没有固定,那么 GC 可以在收集期间重新定位它,让我们无法将原始数组归零。【参考方案3】:

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

如果您 - 作为用户 - 可以检索纯文本 String,那么您首先会否定使用 SecureString 的大部分优势。这甚至会有点危险,因为您会创建看起来安全的代码,实际上根本不安全(编辑:至少在保护内存数据方面不是)。

【讨论】:

以上是关于Rfc2898DeriveBytes + PBKDF2 + SecureString 是不是可以使用安全字符串而不是字符串?的主要内容,如果未能解决你的问题,请参考以下文章

.NET:PasswordDeriveBytes 和 Rfc2898DeriveBytes 之间的区别

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

使用 Rfc2898DeriveBytes 在 C# 中实现 PBKDF2

Crypto++ pbkdf2 输出不同于 Rfc2898DeriveBytes (C#) 和 crypto.pbkdf2 (JavaScript)

如何散列密码

PBKDF2 Python 密钥与 .NET Rfc2898