C# AES Rijndael - 检测无效密码

8
我正在使用Rijndael算法加密程序中的一些敏感数据。当用户输入错误密码时,大多数情况下会抛出“填充无效且无法移除”的CryptographicException异常信息。
然而,有很小的概率CryptStream不会用错误密码抛出异常,而是返回一个解密错误的流,换句话说,它解密成垃圾。
有什么方法可以检测/预防这种情况吗?我能想到的最简单的方法是,在加密时在消息开头放置一个“魔术数字”,并在解密后检查它是否仍然存在。
但如果有更简单的方法,我很愿意听取建议!

3
你要是采取任何措施来防止这种情况,都会让密码更容易被破解。 - SLaks
既然我正在加密二进制序列化对象,那么我可以在 SerializationException 级别上处理这种情况,对吗? - Ozzah
@Brian:如果你拥有的是任意消息体,那么验证你是否破解了正确的密码可能会很困难。但他的消息体是一个 .Net 序列化对象,我假设它有一个标准头部。 - SLaks
另外,如果哈希函数存在弱点,那么它将更容易被攻破。 - SLaks
不要使用填充错误或缺乏填充来确定解密是否成功,它并不会这样做。如果您将此报告给调用者,则创建了一个填充预言机攻击,可以被攻击。正如答案中提到的那样,解决方案是使用HMAC。 - zaph
显示剩余2条评论
6个回答

7
HMAC是你需要的技术,它专门为此目的而设计。它将密钥和消息(在本例中为您的密码)结合起来,并以一种方式对它们进行哈希处理,以确保内容的真实性和完整性,只要所使用的哈希函数是安全的。您可以将HMAC附加到加密数据上,并且稍后可以用它来验证解密是否正确。
参考链接:

3
校验和正是为此目的而存在。在加密数据之前获取数据的哈希值。加密数据并将其与哈希值一起存储。解密后,获取解密数据的哈希值并与原始哈希值进行比较。如果使用加密级别的哈希(例如SHA512),则您的数据将是安全的。毕竟,这正是加密压缩软件所做的事情。
对于终极安全性,您可以分别加密哈希和数据,然后进行解密和比较。如果数据和哈希都解密为损坏的数据,则它们匹配的可能性非常小。

@SLaks:那么它如何知道被解密的内容是否无效?填充不是随机的吗? - BlueRaja - Danny Pflughoeft
@Teoman,虽然你的方向是正确的,但还没有完全到位。为了获得最佳安全性,您需要使用类似HMAC的东西(请参见我的答案)。 - Can Gencer
@Can Gencer,谢谢老兄,我甚至不知道.NET库中存在HMAC实现。 - Teoman Soygul
加密后再进行消息认证可能更好。正如Moxie指出的:在身份验证之前尽可能少地进行操作是更好的选择。此外,需要使用慢速密钥派生函数(例如PBKDF2),以大约100毫秒的CPU时间来派生HMAC密钥。 - zaph

1

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

            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

0

虽然我在 Teoman Soygul 的帖子中对 CRC/哈希有些同意,但有一件非常重要的事情需要注意。永远不要加密哈希,因为这可能会使寻找结果密钥更容易。即使没有加密哈希,您仍然为他们提供了一种轻松的方法来测试他们是否成功获得了正确的密码;然而,让我们假设这已经是可能的。由于我知道您加密的数据类型,无论是文本、序列化对象还是其他类型,我都可以编写代码来识别它。

话虽如此,我曾使用以下代码的变形来加密 / 解密数据:

    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(先加密再进行消息认证码)呢? - zaph

-1

我喜欢Can Gencer的回答; 你不能真正验证没有HMAC的解密。

但是,如果你有一个非常大的明文,那么解密可能会非常昂贵。你可能需要做很多工作才能发现密码无效。能够快速拒绝错误密码而不必经历所有这些工作将是很好的。有一种方法可以使用PKCS#5 PBKDF2。(在RFC2898中标准化,可通过Rfc2898DeriveBytes在您的C#程序中访问)。

通常,数据协议要求使用PBKDF2从密码和盐生成密钥,进行1000个周期或某个指定的数字。然后,也可以(可选地)通过同一算法的继续来初始化向量。
为了实现快速密码检查,通过PBKDF2生成另外两个字节。如果您不生成并使用IV,则只需生成32个字节并保留最后2个字节。将这对字节存储或传输到加密文本旁边。在解密方面,获取密码,生成密钥和(可能丢弃的)IV,然后生成2个附加字节,并将其与存储的数据进行比较。如果这些对不匹配,则知道您有一个错误的密码,而无需进行任何解密。
如果它们匹配,这并不能保证密码是正确的。您仍然需要全文本的HMAC才能确定。但是,在大多数“错误密码”的情况下,您可以节省大量工作和墙钟时间,而不会影响整个系统的安全性。

附言:你写道:

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

避免将明文放入加密文本中。这只会暴露另一个攻击向量,使攻击者更容易消除错误的方向。我上面提到的密码验证是一个不同的问题,不会暴露这种风险。


PBKDF2 的目的是使计算变得昂贵,通常在 100ms 左右。虽然 MAC 是比较昂贵的,但对于短消息来说可能更便宜,并且与消息大小成比例。 - zaph

-1
 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
  1. 您没有描述如何检测错误的密钥。
  2. 它可能会检测到不正确的填充,这将无法检测到所有不正确的密钥。
  3. 使用填充来检测不正确的密钥可能会导致创建一个填充预言机,该预言机可用于在没有密钥的情况下解密数据。
- zaph

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