C # AES Rijndael - обнаружение недопустимых паролей

Я использую Rijndael для шифрования некоторых конфиденциальных данных в моей программе.

Когда пользователь вводит неправильный пароль, в большинстве случаев выдается CryptographicException с сообщением «Заполнение недействительно и не может быть удалено».

Однако с очень малой вероятностью CryptStream не выбрасывает исключение с неверным паролем, а вместо этого возвращает неверно расшифрованный поток. Другими словами, он расшифровывает в мусор.

Любая идея, как обнаружить/предотвратить это? Самый простой способ, который я могу придумать, - это поместить «магическое число» в начало сообщения при шифровании и проверить, сохраняется ли оно после расшифровки.

Но если есть более простой способ, я хотел бы услышать его!


person Ozzah    schedule 28.04.2011    source источник
comment
Все, что вы делаете, чтобы предотвратить это, облегчит взлом пароля.   -  person SLaks    schedule 28.04.2011
comment
Поскольку я шифрую двоично-сериализованные объекты, я думаю, что могу справиться с этим случаем на уровне SerializationException?   -  person Ozzah    schedule 28.04.2011
comment
@Slaks: Да, но это не обязательно ставит под угрозу само шифрование. Как правило, уже ожидается, что если пользователь угадает пароль, он сможет определить, был ли этот пароль правильным. В некоторой степени это верно для любого шифрования, в котором длина ключа короче длины сообщения.   -  person Brian    schedule 28.04.2011
comment
@Brian: Если у вас есть произвольное тело сообщения, может быть сложно проверить, взломали ли вы правильный пароль. Тем не менее, его тело сообщения представляет собой сериализованный объект .Net, и я предполагаю, что оно имеет стандартный заголовок.   -  person SLaks    schedule 28.04.2011
comment
Также, если у хэша есть слабость, будет проще.   -  person SLaks    schedule 28.04.2011
comment
Есть ли способ надежно воспроизвести эту проблему? (т.е. получить результат мусора вместо CryptographicException)   -  person Andreas Öhlund    schedule 15.08.2014
comment
Не используйте ошибки заполнения или их отсутствие, чтобы определить, была ли расшифровка успешной, это не делает этого. Если вы сообщите об этом вызывающей стороне, вы создали заполняющий оракул, который может быть атакован. Как упоминалось в ответе, решение - HMAC.   -  person zaph    schedule 25.08.2017


Ответы (6)


HMAC - это то, что вам нужно. Он как раз для этого и сделан. Он объединяет ключ и сообщение (которое в данном случае будет вашим паролем) и хэширует их таким образом, чтобы обеспечить подлинность и целостность содержимого, если используемая хэш-функция является безопасной. Вы можете прикрепить HMAC к зашифрованным данным, и его можно использовать позже для проверки правильности расшифровки.

person Can Gencer    schedule 28.04.2011

Контрольные суммы как раз для этой цели. Получите хэш ваших данных перед шифрованием. Зашифруйте данные и поместите их вместе с хешем в хранилище. После расшифровки получите хэш расшифрованных данных и сравните его с прежним. Если вы используете хэш криптографического уровня (например, SHA512), ваши данные будут в безопасности. В конце концов, это именно то, что делает зашифрованное программное обеспечение для сжатия.

Для максимальной безопасности вы можете отдельно зашифровать как хэши, так и данные, а затем расшифровать и сравнить. Если и данные, и хэш расшифровываются в поврежденные данные, вероятность того, что они совпадут, очень мала.

person Teoman Soygul    schedule 28.04.2011
comment
@SLaks: Тогда как он узнает, что расшифровываемое недействительно? Заполнение не случайное? - person BlueRaja - Danny Pflughoeft; 28.04.2011
comment
@Teoman, пока на правильном пути, не до конца. Для лучшей безопасности вам нужно использовать что-то вроде HMAC (см. мой ответ). - person Can Gencer; 28.04.2011
comment
@Can Gencer, спасибо, приятель, я даже не знал о существовании реализации HMAC в библиотеке .NET. - person Teoman Soygul; 29.04.2011
comment
Возможно, лучше зашифровать, а затем MAC. Как указывает Moxie: лучше делать как можно меньше до к аутентификации. Кроме того, для получения ключа HMAC требуется использовать медленную функцию получения ключа, такую ​​как PBKDF2, с процессорным временем ~100 мс. - person zaph; 25.08.2017

Чтобы проверить правильность используемого пароля, вы можете использовать этот код

            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
person Tommaso    schedule 12.07.2018

Хотя я могу частично согласиться с сообщением Теомана Сойгула о 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();
            }
        }
    }
person csharptest.net    schedule 28.04.2011
comment
К сожалению, аутентификации с шифрованием нет, только открытый текст. Почему бы не использовать HMAC для зашифрованных данных (зашифровать затем MAC)? - person zaph; 25.08.2017

Мне нравится Can Gencer answer; вы не можете действительно проверить расшифровку без HMAC.

Но если у вас очень большой открытый текст, расшифровка может быть очень дорогой. Вы можете проделать массу работы только для того, чтобы узнать, что пароль недействителен. Было бы неплохо иметь возможность быстро отклонять неправильные пароли, не проделывая всю эту работу. Есть способ использовать PKCS#5 PBKDF2. (стандартизовано в RFC2898, который доступен для вашей программы C# в Rfc2898DeriveBytes).

Обычно протокол данных требует генерации ключа из пароля и соли с использованием PBKDF2 с частотой 1000 циклов или заданным числом. Затем, возможно, также (необязательно) вектор инициализации через продолжение того же алгоритма.

Чтобы реализовать быструю проверку пароля, сгенерируйте еще два байта через PBKDF2. Если вы не создаете и не используете IV, просто сгенерируйте 32 байта и сохраните последние 2. Сохраните или передайте эту пару байтов рядом с вашим криптотекстом. На стороне расшифровки получите пароль, сгенерируйте ключ и (возможно, одноразовый) IV, затем сгенерируйте 2 дополнительных байта и сравните их с сохраненными данными. Если пары не совпадают, вы знаете, что у вас неправильный пароль без какой-либо расшифровки.

Если они совпадают, это не гарантирует, что пароль правильный. Для этого вам все еще нужен HMAC полного открытого текста. Но вы можете сэкономить массу работы и, возможно, время настенных часов, в большинстве случаев «неправильный пароль», и без ущерба для безопасности всей системы.


ps: вы написали:

Самый простой способ, который я могу придумать, - это поместить «магическое число» в начало сообщения при шифровании и проверить, сохраняется ли оно после расшифровки.

Избегайте помещения открытого текста в криптотекст. Он лишь выставляет другой вектор атаки, облегчает злоумышленнику устранение неправильных поворотов. Проверка пароля, о которой я упоминал выше, — это другое животное, не подвергающее этому риску.

person Cheeso    schedule 23.05.2011
comment
Смысл PBKDF2 в том, что он требует больших вычислительных ресурсов, обычно около 100 мс. Хотя MAC дорог, он может быть дешевле для коротких сообщений и пропорционален размеру сообщения. - person zaph; 25.08.2017

 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), затем удалите выходной файл. Это работает для меня.

person Endri Surawan    schedule 25.08.2017
comment
1. Вы не описываете как это определяет неправильный ключ. 2. возможно, он обнаруживает неправильное заполнение, которое не обнаружит все неправильные клавиши. 3. Использование дополнения для обнаружения неправильного ключа может привести к созданию оракула заполнения, который можно использовать для расшифровки данных без ключа. - person zaph; 27.08.2017