I'm using Rijndael to encrypt some sensitive data in my program.
When the user enters an incorrect password, most of the time a CryptographicException
is thrown with the message "Padding is invalid and cannot be removed.".
However, with very small probability, the CryptStream does not throw an exception with the wrong password, but instead gives back an incorrectly decrypted stream. In other words, it decrypts to garbage.
Any idea how to detect/prevent this? The simplest way I can think of would be to put a "magic number" at the start of the message when encrypting, and check if it's still there after decrypting.
But if there's an easier way, I'd love to hear it!
HMAC is what you need. It is exactly made for this purpose. It combines the key and the message (which in this case, will be your password) and hashes them in a way that it will ensure the authenticity and integrity of the content, as long as the hash function used is secure. You can attach the HMAC to the encrypted data, and it can be used later to validate if the decryption was made correctly.
Checksums are exactly for this purpose. Get a hash of your data before encrypting. Encrypt the data and put it along with the hash into storage. After decrypting, get the hash of the decrypted data and compare it with the former. If you use a crypto grade hash (i.e. SHA512) your data will be safe. After all, this is exactly what encrypted compression software does.
For ultimate security, you can encrypt both the hashes and data separately then decrypt and compare. If both data and hash decrypts to corrupted data, there is very minuscule chances that they will match.
To check if the password you are using is correct, you can use this code
Dim decryptedByteCount As Integer
Try
decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length)
Catch exp As System.Exception
Return "Password Not Correct"
End Try
in essence, check if an error message is generated during decryption.
I report all the decoding code below
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
Though I can agree somewhat with Teoman Soygul post about CRC/Hash there is one very important thing to note. Never encrypt the hash as this can make it easier to find the resulting key. Even without encrypting the hash you still gave them an easy way to test if they have successfully gained the correct password; however, let's assume that is already possible. Since I know what kind of data you encrypted, be it text, or serialized objects, or whatever, it's likely I can write code to recognize it.
That said, I've used derivations of the following code to encrypt/decrypt data:
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();
}
}
}
I like Can Gencer's answer; you cannot really verify a decryption without the HMAC.
But, if you have a very a very large plaintext, then the decrypting can be very expensive. You might do a ton of work just to find out that the password was invalid. It would be nice to be able to do a quick rejection of wrong passwords, without going through all that work. There is a way using the PKCS#5 PBKDF2. (standardized in RFC2898, which is accessible to your c# program in Rfc2898DeriveBytes).
Normally the data protocol calls for generation of the key from a password and salt using PBKDF2, at 1000 cycles or some specified number. Then maybe also (optionally) the initialization vector, via a contniuation of the same algorithm.
To implement the quick password check, generate two more bytes via the PBKDF2. If you don't generate and use an IV, then just generate 32 bytes and keep the last 2. Store or transmit this pair of bytes adjacent to your cryptotext. On the decrypting side, get the password, generate the key and (maybe throwaway) IV, then generate the 2 additional bytes, and check them against the stored data. If the pairs don't match you know you have a wrong password, without any decryption.
If they match, it is not a guarantee that the password is correct. You still need the HMAC of the full plaintext for that. But you can save yourself a ton of work, and maybe wall clock time, in most cases of "wrong password", and without compromising the security of the overall system.
ps: you wrote:
The simplest way I can think of would be to put a "magic number" at the start of the message when encrypting, and check if it's still there after decrypting.
Avoid putting plaintext into the cryptotext. It only exposes another attack vector, makes it easier for an attacker to eliminate wrong turns. The password verification thing I mentioned above is a different animal, does not expose this risk.
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
I use that code to decrypt any file. Wrong password detected on cryptostream.close()
. Catch this line as error when a wrong key is used to decrypt file. When error happens, just close the output stream and release it (set outputFile
to Nothing
), then delete output file. It's working for me.
来源:https://stackoverflow.com/questions/5812232/c-sharp-aes-rijndael-detecting-invalid-passwords