C#AES Rijndael – 检测无效密码

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

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

但是,如果概率非常小,CryptStream不会使用错误的密码抛出exception,而是会返回错误解密的流。 换句话说,它解密为垃圾。

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

但如果有一种更简单的方法,我很乐意听到它!

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

  • HMAC,维基百科
  • System.Security.Cryptography.HMAC

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

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

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

  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 Byte 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 

虽然我对Teoman Soygul关于CRC / Hash的post有所了解,但有一点非常重要。 永远不要加密哈希,因为这样可以更容易地找到结果密钥。 即使没有加密哈希,你仍然给他们一个简单的方法来测试他们是否已成功获得正确的密码; 但是,让我们假设这已经成为可能了。 由于我知道您加密了哪种数据,无论是文本还是序列化对象,或者其他什么,我都可以编写代码来识别它。

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

  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(); } } } 

我喜欢Can Gencer的回答 ; 如果没有HMAC,你无法真正validation解密。

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

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

要实现快速密码检查,请通过PBKDF2生成两个以上的字节 。 如果您不生成并使用IV,则只生成32个字节并保留最后一个2.将这对字节存储或传输到您的cryptotext附近。 在解密端,获取密码,生成密钥和(可能一次性)IV,然后生成2个额外字节,并根据存储的数据进行检查。 如果对不匹配,则表示您的密码错误,没有任何解密。

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


ps :你写道:

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

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

  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 ),然后删除输出文件。 它对我有用。