【问题标题】:C# AES Rijndael - detecting invalid passwordsC# AES Rijndael - 检测无效密码
【发布时间】:2011-04-28 00:21:49
【问题描述】:

我正在使用 Rijndael 加密程序中的一些敏感数据。

当用户输入错误密码时,大多数情况下会抛出 CryptographicException 并显示消息“填充无效,无法删除。”。

然而,在非常小的概率下,CryptStream 不会使用错误的密码抛出异常,而是返回一个错误解密的流。换句话说,它解密为垃圾。

知道如何检测/预防这种情况吗?我能想到的最简单的方法是在加密时在消息的开头放一个“幻数”,并在解密后检查它是否仍然存在。

但如果有更简单的方法,我很想听听!

【问题讨论】:

  • 您为防止这种情况所做的任何事情都会使破解密码变得更容易。
  • 由于我正在加密二进制序列化对象,我想我可以在 SerializationException 级别处理这种情况?
  • @Slaks:是的,但这并不一定会损害加密本身。通常,如果用户猜到密码,他们将能够确定该密码是否正确。对于密钥长度短于消息长度的任何加密,这在某种程度上是正确的。
  • @Brian:如果您拥有的是任意消息正文,则可能很难验证您是否破解了正确的密码。不过,他的消息正文是一个 .Net 序列化对象,我假设它有一个标准标头。
  • 另外,如果哈希有弱点,那就更容易了。

标签: c# encryption rijndaelmanaged rijndael encryption-symmetric


【解决方案1】:

HMAC 是您所需要的。它正是为此目的而制造的。只要使用的散列函数是安全的,它将密钥和消息(在这种情况下,将是您的密码)结合起来,并以确保内容的真实性和完整性的方式对它们进行散列。您可以将 HMAC 附加到加密数据中,以后可以使用它来验证解密是否正确。

【讨论】:

    【解决方案2】:

    校验和正是为此目的。在加密之前获取数据的哈希值。加密数据并将其与哈希一起存储。解密后,得到解密后数据的哈希值,并与前者进行比较。如果您使用加密级哈希(即 SHA512),您的数据将是安全的。毕竟,这正是加密压缩软件所做的。

    为了最终安全,您可以分别加密哈希和数据,然后解密和比较。如果数据和哈希都解密为损坏的数据,那么它们匹配的机会非常小。

    【讨论】:

    • @SLaks:那它怎么知道什么时候被解密的东西是无效的呢?填充不是随机的吗?
    • @Teoman,虽然在正确的轨道上,但不是一直到那里。为了获得最佳安全性,您需要使用 HMAC 之类的东西(请参阅我的回答)。
    • @Can Gencer,谢谢伙计,我什至不知道 .NET 库中存在 HMAC 实现。
    • 可以说先加密然后 MAC 更好。如Moxie points out:最好在认证之前尽量少做。此外,还需要使用慢速密钥导出函数,例如 PBKDF2,CPU 时间约为 100 毫秒,以导出 HMAC 密钥。
    【解决方案3】:

    要检查您使用的密码是否正确,您可以使用此代码

                Dim decryptedByteCount As Integer
                Try
                    decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length)
                Catch exp As System.Exception
                    Return "Password Not Correct"
                End Try
    

    本质上是检查解密过程中是否产生错误信息。

    我把下面所有的解码代码报告出来

        Public Shared Function Decrypt(ByVal cipherText As String) As String
    
        If System.Web.HttpContext.Current.Session("Crypto") = "" Then
            HttpContext.Current.Response.Redirect("http://yoursite.com")
        Else
            If cipherText <> "" Then
                'Setto la password per criptare il testo
                Dim passPhrase As String = System.Web.HttpContext.Current.Session("Crypto")
    
                'Ottieni lo stream completo di byte che rappresentano: [32 byte di Salt] + [32 byte di IV] + [n byte di testo cifrato]
                Dim cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText)
    
                'Ottieni i Salt bytes estraendo i primi 32 byte dai byte di testo cifrato forniti
                Dim saltStringBytes = cipherTextBytesWithSaltAndIv.Take((Keysize)).ToArray
    
                'Ottieni i IV byte estraendo i successivi 32 byte dai byte testo cifrato forniti.
                Dim ivStringBytes = cipherTextBytesWithSaltAndIv.Skip((Keysize)).Take((Keysize)).ToArray
    
                'Ottieni i byte del testo cifrato effettivo rimuovendo i primi 64 byte dal testo cifrato.
                Dim cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip(((Keysize) * 2)).Take((cipherTextBytesWithSaltAndIv.Length - ((Keysize) * 2))).ToArray
    
                Dim password = New Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations)
                Dim keyBytes = password.GetBytes((Keysize))
                Dim symmetricKey = New RijndaelManaged
                symmetricKey.BlockSize = 256
                symmetricKey.Mode = CipherMode.CBC
                symmetricKey.Padding = PaddingMode.PKCS7
    
                Dim decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes)
                Dim memoryStream = New MemoryStream(cipherTextBytes)
                Dim cryptoStream = New CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)
                Dim plainTextBytes = New Byte((cipherTextBytes.Length) - 1) {}
    
                Dim decryptedByteCount As Integer
                Try
                    decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length)
                Catch exp As System.Exception
                    Return "La password di Cryptazione non è corretta"
                End Try
    
                memoryStream.Close()
                cryptoStream.Close()
                Return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount)
            Else
                Decrypt = ""
            End If
        End If
    
    End Function
    

    【讨论】:

      【解决方案4】:

      虽然我在某种程度上同意 Teoman Soygul 关于 CRC/Hash 的帖子,但有一件非常重要的事情需要注意。永远不要加密散列,因为这样可以更容易找到生成的密钥。即使不加密哈希,您仍然可以为他们提供一种简单的方法来测试他们是否成功获得了正确的密码;但是,让我们假设这已经是可能的。因为我知道你加密了什么样的数据,无论是文本,还是序列化的对象,或者其他什么,我很可能可以编写代码来识别它。

      也就是说,我使用以下代码的派生来加密/解密数据:

          static void Main()
          {
              byte[] test = Encrypt(Encoding.UTF8.GetBytes("Hello World!"), "My Product Name and/or whatever constant", "password");
              Console.WriteLine(Convert.ToBase64String(test));
              string plain = Encoding.UTF8.GetString(Decrypt(test, "My Product Name and/or whatever constant", "passwords"));
              Console.WriteLine(plain);
          }
          public static byte[] Encrypt(byte[] data, string iv, string password)
          {
              using (RijndaelManaged m = new RijndaelManaged())
              using (SHA256Managed h = new SHA256Managed())
              {
                  m.KeySize = 256;
                  m.BlockSize = 256;
                  byte[] hash = h.ComputeHash(data);
                  byte[] salt = new byte[32];
                  new RNGCryptoServiceProvider().GetBytes(salt);
                  m.IV = h.ComputeHash(Encoding.UTF8.GetBytes(iv));
                  m.Key = new Rfc2898DeriveBytes(password, salt) { IterationCount = 10000 }.GetBytes(32);
      
                  using (MemoryStream ms = new MemoryStream())
                  {
                      ms.Write(hash, 0, hash.Length);
                      ms.Write(salt, 0, salt.Length);
                      using (CryptoStream cs = new CryptoStream(ms, m.CreateEncryptor(), CryptoStreamMode.Write))
                      {
                          cs.Write(data, 0, data.Length);
                          cs.FlushFinalBlock();
                          return ms.ToArray();
                      }
                  }
              }
          }
      
          public static byte[] Decrypt(byte[] data, string iv, string password)
          {
              using (MemoryStream ms = new MemoryStream(data, false))
              using (RijndaelManaged m = new RijndaelManaged())
              using (SHA256Managed h = new SHA256Managed())
              {
                  try
                  {
                      m.KeySize = 256;
                      m.BlockSize = 256;
      
                      byte[] hash = new byte[32];
                      ms.Read(hash, 0, 32);
                      byte[] salt = new byte[32];
                      ms.Read(salt, 0, 32);
      
                      m.IV = h.ComputeHash(Encoding.UTF8.GetBytes(iv));
                      m.Key = new Rfc2898DeriveBytes(password, salt) { IterationCount = 10000 }.GetBytes(32);
                      using (MemoryStream result = new MemoryStream())
                      {
                          using (CryptoStream cs = new CryptoStream(ms, m.CreateDecryptor(), CryptoStreamMode.Read))
                          {
                              byte[] buffer = new byte[1024];
                              int len;
                              while ((len = cs.Read(buffer, 0, buffer.Length)) > 0)
                                  result.Write(buffer, 0, len);
                          }
      
                          byte[] final = result.ToArray();
                          if (Convert.ToBase64String(hash) != Convert.ToBase64String(h.ComputeHash(final)))
                              throw new UnauthorizedAccessException();
      
                          return final;
                      }
                  }
                  catch
                  {
                      //never leak the exception type...
                      throw new UnauthorizedAccessException();
                  }
              }
          }
      

      【讨论】:

      • 遗憾的是没有加密认证,只有明文。为什么不对加密数据进行 HMAC(加密然后 MAC)?
      【解决方案5】:

      我喜欢Can Gencer's answer;如果没有 HMAC,您将无法真正验证解密。

      但是,如果您有一个非常大的明文,那么解密可能会非常昂贵。您可能会做大量工作才能发现密码无效。能够快速拒绝错误密码,而无需完成所有工作,这将是一件好事。有一种方法可以使用PKCS#5 PBKDF2。 (在RFC2898 中标准化,您的c# 程序可以在Rfc2898DeriveBytes 中访问它)。

      通常,数据协议要求使用 PBKDF2 从密码和盐生成密钥,周期为 1000 次或某个指定的次数。然后也可以(可选地)通过相同算法的延续来初始化向量。

      要实现快速密码检查,请通过 PBKDF2.再生成两个字节。如果您不生成和使用 IV,则只需生成 32 个字节并保留最后 2 个字节。存储或传输与您的密文相邻的这对字节。在解密方面,获取密码,生成密钥和(可能是一次性的)IV,然后生成另外 2 个字节,并根据存储的数据检查它们。如果对不匹配你知道你有一个错误的密码,没有任何解密。

      如果它们匹配,则不能保证密码正确。为此,您仍然需要完整明文的 HMAC。但是,在大多数“密码错误”的情况下,您可以为自己节省大量工作,也许还可以节省挂钟时间,而且不会影响整个系统的安全性。


      ps:你写的:

      我能想到的最简单的方法是在加密时在消息的开头放一个“幻数”,并在解密后检查它是否仍然存在。

      避免将明文放入密文中。它只暴露了另一个攻击向量,使攻击者更容易消除错误的转弯。我上面提到的密码验证是另一种动物,不会暴露这个风险。

      【讨论】:

      • PBKDF2 的计算成本很高,一般在 100 毫秒左右。虽然 MAC 很昂贵,但它对于短消息可能更便宜,并且与消息大小成正比。
      【解决方案6】:
       Public Sub decryptFile(ByVal input As String, ByVal output As String)
      
              inputFile = New FileStream(input, FileMode.Open, FileAccess.Read)
              outputFile = New FileStream(output, FileMode.OpenOrCreate, FileAccess.Write)
              outputFile.SetLength(0)
      
              Dim buffer(4096) As Byte
              Dim bytesProcessed As Long = 0
              Dim fileLength As Long = inputFile.Length
              Dim bytesInCurrentBlock As Integer
              Dim rijandael As New RijndaelManaged
              Dim cryptoStream As CryptoStream = New CryptoStream(outputFile, rijandael.CreateDecryptor(encryptionKey, encryptionIV), CryptoStreamMode.Write)
      
              While bytesProcessed < fileLength
                  bytesInCurrentBlock = inputFile.Read(buffer, 0, 4096)
                  cryptoStream.Write(buffer, 0, bytesInCurrentBlock)
                  bytesProcessed = bytesProcessed + CLng(bytesInCurrentBlock)
              End While
              Try
                  cryptoStream.Close() 'this will raise error if wrong password used
                  inputFile.Close()
                  outputFile.Close()
                  File.Delete(input)
                  success += 1
              Catch ex As Exception
                  fail += 1
                  inputFile.Close()
                  outputFile.Close()
                  outputFile = Nothing
                  File.Delete(output)
              End Try
      

      我使用该代码解密任何文件。在cryptostream.close() 上检测到错误密码。当使用错误的密钥解密文件时,将此行视为错误。发生错误时,只需关闭输出流并释放它(将outputFile 设置为Nothing),然后删除输出文件。它对我有用。

      【讨论】:

      • 1.您没有描述如何检测到不正确的密钥。 2.它可能检测到不正确的填充,它不会检测到所有不正确的键。 3.使用padding检测不正确的key会导致创建一个Padding Oracle,可以在没有key的情况下解密数据。
      猜你喜欢
      • 2023-03-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-03
      • 2012-05-02
      • 2016-05-26
      • 1970-01-01
      • 2012-07-20
      相关资源
      最近更新 更多