加密用户密码的正确方法

信息安全 加密 密码
2021-08-19 14:14:53

我有一个场景,用户输入的密码需要存储以备后用。我不能使用散列,因为我需要获取原始密码以供以后使用。

例如,考虑一个电子邮件发件人应用程序,其中用户最初会为他的电子邮件帐户输入密码,应用程序将以加密形式存储它,当应用程序需要发送电子邮件时,它会解密密码并使用它来发送邮件.

现在,这就是我正在做的事情:

  1. 应用程序在数据库中存储一个主密钥(使用 RNGCryptoServiceProvider 生成)
  2. AES 的密钥和 IV 都是使用 Rfc2898DeriveBytes 从主密钥派生的(对于 Rfc2898DeriveBytes,密码=masterkey 和 salt=user_id)

    // the initial, one time masterkey generation, which will be used for all passwords
    byte[] masterKeyBytes = new byte[32];
    new RNGCryptoServiceProvider().GetBytes(masterKeyBytes);
    // masterkey is saved in database
    string masterKey = Convert.ToBase64String(masterKeyBytes);
    
    // password encryption of a user's password
    var derivative = new Rfc2898DeriveBytes(masterKey, 128_bit_guid_of_user_id, numberOfIterations)
    var aes = new AesCryptoServiceProvider();
    aes.Key = derivative.GetBytes(32);
    aes.IV = derivative.GetBytes(16);
    var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
    // get the encrypted password from encryptor
    .....
    .....
    

我这样做对吗?是否有任何更正?此外,所使用的尺寸(用于密钥大小、masterkeysize 等)是否足够好?

还有几个问题(只是为了了解更多):

  1. 从高熵主密钥派生密钥(每个用户的单独 AES 密钥)是否被认为不值得(或更糟)?(即仅使用主密钥作为 AES 密钥就足够了)。如果是这样,为什么?,我的论点是——如果有人为特定用户推断出 AES 密钥,他/她仍然无法推断出主密钥,因此其他用户是安全的。我遇到了 - 在一些智能卡应用程序中使用的“密钥多样化”,它们从每个实例的主密钥派生一个单独的 AES 密钥(这里不适用吗?)

  2. 在这种情况下我们可以只使用 PBKDF 来派生密钥,还是应该使用 KBKDF?如果是这样,您能否提及提供 KBKDF 的库(最好来自 .NET 框架)?我知道 RfcDerivativeBytes 使用 PBKDF 派生密钥,但我不知道如何使用 KBKDF 派生密钥。

1个回答

当你加密密码时,是因为你害怕被窃听;您设想攻击者可以查看您的数据库内容。加密是正确的工具,但是如果您将加密密钥放在同一个数据库中,那么您所做的就相当于将门钥匙隐藏在门垫下。可以瞥见数据库的攻击者通常也可以通过这种方式获得密钥。通常,当数据库内容泄漏时,它是通过影响整个数据库的方法(大多数成功的 SQL 注入攻击允许任意数据提取;同样适用于丢失/被盗的数据库备份)。因此,将主密钥存储在与数据库不同的位置会更有意义,以限制密钥和加密密码的泄露风险。

由于您显然使用的是 C# 和 .NET,因此我推断出一个 Windows 系统,因此您可能希望通过ProtectedData使用DPAPI


如果您使用正确的Key Derivation Function,则从同一个主密钥以及特定于实例的值中派生出实际的加密密钥IV 是一件好事但是,您使用的额外“随机化”值是用户特定的标识符;这意味着如果您更改给定用户的加密数据(例如,用户更改了他的密码),那么旧密码和新密码都将使用相同的密钥和相同的 IV 进行加密。对于大多数加密系统,IV 重用是一种罪过。

相反,您应该使用在加密新数据时重新生成的随机值,并与加密值一起存储。可以有几种方法;最简单的方法是使用“主密钥”作为 AES 的实际加密密钥,并在每次要加密时生成一个随机 IV;然后,加密的结果将是 IV 和加密数据的串联,按该顺序。

请记住,IV 并不是秘密。否则,我们将其称为key因此,如果它可供攻击者使用,这不是问题。但是,每个加密系统对 IV 都有自己的要求;特别是, CBC 模式下的分组密码(例如 AES)需要使用强随机源(AesCryptoServiceProvider默认为 CBC)随机生成 IV 。RNGCryptoServiceProvider因此,使用实例生成 IV 。


说到 KDF,您使用Rfc2898DeriveBytes,这是矫枉过正。该类实现了PBKDF2,这是一个基于密码的KDF。“密码”之所以特殊,是因为它们与人脑兼容,这暗示了其与生俱来的弱点。蛮力攻击适用于密码(我们称之为“字典攻击”)。为了提高抵抗力,基于密码的 KDF 通过多次重复迭代变得缓慢。不幸的是,这也使得 KDF 的使用成本很高。

由于您的主密钥不是密码,而是由强随机源生成的适当长且随机的密钥,因此它不易受到暴力破解。因此,PBKDF2 的成本是对 CPU 周期的浪费。如果您仍想使用 PBKDF2 导出加密密钥和/或 IV,则应将迭代次数设置为 1。或者,如上所述,根本不要使用 KDF:使用主密钥加密数据“as是”,但带有一个特定于实例的随机 IV,您将其存储在加密结果中。


AesCryptoServiceProvider依赖于底层的原生代码实现。RijndaelManaged用“纯.NET”编写。对于加密短元素,RijndaelManaged实际上可能更有效;Dispose()如果您忘记在必要时调用该方法,它也会降低手柄泄漏的风险。通常,性能实际上并不重要(加密和解密时间相对于您的服务器所做的其他事情可以忽略不计),但如果在您的情况下确实如此,那么请记住您有几个实现选项。

用于 AES 的 32 字节(256 位)密钥有点矫枉过正,但不会造成太大伤害。与 128 位密钥相比,256 位密钥意味着加密和解密的 CPU 成本 +40%,但对于加密的小元素,它应该可以忽略不计。