Шифрование / дешифрование файлов Rijndael

Я потратил последние несколько дней на создание класса шифрования / дешифрования файлов на основе стандарта шифрования Rijndael, доступного через класс RijndaelManaged, и просмотрел все ресурсы и примеры, которые смог найти. Примеры были либо устаревшими, либо неработающими, либо ограниченными, но, по крайней мере, им удалось многому научиться, и я подумал, что опубликую их обновленную версию, убедившись, что она надежна и выдержит вашу критику.

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

Я также не совсем уверен, хватит ли одной соли как для ключа, так и для вектора инициализации, или две лучше по соображениям безопасности?

Любые другие наблюдения и / или оптимизации также были бы весьма признательны.

class FileEncDec
{
    private int keySize;
    private string passPhrase;

    internal FileEncDec( int keySize = 256, string passPhrase = @"This is pass phrase key to use for testing" )
    {
        this.keySize = keySize;
        this.passPhrase = passPhrase; // Can be user selected and must be kept secret
    }

    private static byte[] GenerateSalt( int length )
    {
        byte[] salt = new byte[ length ];

        // Populate salt with cryptographically strong bytes.
        RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

        rng.GetNonZeroBytes( salt );

        // Split salt length (always one byte) into four two-bit pieces and store these pieces in the first four bytes 
        // of the salt array.
        salt[ 0 ] = (byte)( ( salt[ 0 ] & 0xfc ) | ( length & 0x03 ) );
        salt[ 1 ] = (byte)( ( salt[ 1 ] & 0xf3 ) | ( length & 0x0c ) );
        salt[ 2 ] = (byte)( ( salt[ 2 ] & 0xcf ) | ( length & 0x30 ) );
        salt[ 3 ] = (byte)( ( salt[ 3 ] & 0x3f ) | ( length & 0xc0 ) );

        return salt;
    }

    internal bool EncryptFile( string inputFile, string outputFile )
    {
        try
        {
            byte[] salt = GenerateSalt( 16 ); // Salt needs to be known for decryption (can be safely stored in the file)
            Rfc2898DeriveBytes derivedBytes = new Rfc2898DeriveBytes( passPhrase, salt, 10000 );
            int bytesRead, bufferSize = keySize / 8;
            byte[] data = new byte[ bufferSize ];
            RijndaelManaged cryptor = new RijndaelManaged();
            cryptor.Key = derivedBytes.GetBytes( keySize / 8 );
            cryptor.IV = derivedBytes.GetBytes( cryptor.BlockSize / 8 );

            using ( var fsIn = new FileStream( inputFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan ) )
            {
                using ( var fsOut = new FileStream( outputFile, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.SequentialScan ) )
                {
                    // Add the salt to the file
                    fsOut.Write( salt, 0, salt.Length );

                    using ( CryptoStream cs = new CryptoStream( fsOut, cryptor.CreateEncryptor(), CryptoStreamMode.Write ) )
                    {
                        while ( ( bytesRead = fsIn.Read( data, 0, bufferSize ) ) > 0 )
                        {
                            cs.Write( data, 0, bytesRead );
                        }
                    }
                }
            }

            return true;
        }
        catch ( Exception )
        {
            return false;
        }
    }

    internal bool DecryptFile( string inputFile, string outputFile )
    {
        try
        {
            int bytesRead = 0, bufferSize = keySize / 8, saltLen;
            byte[] data = new byte[ bufferSize ], salt;
            Rfc2898DeriveBytes derivedBytes;
            RijndaelManaged cryptor = new RijndaelManaged();    // Create new cryptor so it's thread safe and don't need to use locks

            using ( var fsIn = new FileStream( inputFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan ) )
            {
                // Retrieve the salt length from the file
                fsIn.Read( data, 0, 4 );

                saltLen =   ( data[ 0 ] & 0x03 ) |
                            ( data[ 1 ] & 0x0c ) |
                            ( data[ 2 ] & 0x30 ) |
                            ( data[ 3 ] & 0xc0 );

                salt = new byte[ saltLen ];
                Array.Copy( data, salt, 4 );

                // Retrieve the remaining salt from the file and create the cryptor
                fsIn.Read( salt, 4, saltLen - 4 );
                derivedBytes = new Rfc2898DeriveBytes( passPhrase, salt, 10000 );
                cryptor.Key = derivedBytes.GetBytes( keySize / 8 );
                cryptor.IV = derivedBytes.GetBytes( cryptor.BlockSize / 8 );

                using ( var fsOut = new FileStream( outputFile, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.SequentialScan ) )
                {
                    using ( var cs = new CryptoStream( fsIn, cryptor.CreateDecryptor(), CryptoStreamMode.Read ) )
                    {
                        while ( ( bytesRead = cs.Read( data, 0, bufferSize ) ) > 0 )
                        {
                            fsOut.Write( data, 0, bytesRead );
                        }
                    }
                }
            }

            return true;
        }
        catch ( Exception )
        {
            return false;
        }
    }
}

Редактировать: 1. Добавлен генератор соли. 2. Сделал рефакторинг для одиночных salt и Rfc2898DerivedBytes и теперь выводит IV из password + salt. 3. Сделано шифрование / дешифрование потокобезопасным (если я сделал это неправильно, дайте мне знать).

Изменить 2: 1. Реорганизован так, что чтение / запись использует буферы вместо однобайтовых операций чтения / записи. 2. Встроенная соль в зашифрованный файл и очищенные переменные (но по-прежнему разрешает passPhrase значение по умолчанию для примера «копировать / вставить». 3. Реорганизованные дескрипторы файлов.


person Storm    schedule 11.06.2015    source источник
comment
Проверки кода следует размещать на codereview.stackexchange.com. SO требует, чтобы у вас был конкретный вопрос.   -  person Maarten Bodewes    schedule 11.06.2015
comment
@MaartenBodewes У меня был конкретный вопрос, на самом деле у меня было множество вопросов, которые, поскольку usr ответил на них, я реализовал его ответы / предложения (и обеспечил, чтобы я ссылался на изменения в правках), чтобы любой, кто ищет то же самое, мог получите наглядный пример. Спасибо за ссылку, вспомню в будущем   -  person Storm    schedule 11.06.2015


Ответы (1)


Вероятно, вам следует каждый раз использовать другой IV. Если вы используете один и тот же IV с одинаковыми данными, результат будет таким же. Теперь злоумышленники могут сделать вывод, что файлы (частично) совпадают, что является утечкой. Вы можете сгенерировать 16 строго случайных байтов и использовать их как соль для Rfc2898DeriveBytes. Добавьте эти байты в файл. Используйте только один Rfc2898DeriveBytes для генерации IV и ключа. В качестве альтернативы вы можете вообще не использовать соль для ключа и случайным образом сгенерировать IV. Соль можно использовать, чтобы сделать вывод ключа уникальным для вашего варианта использования или, например, чтобы дать каждому пользователю вашего приложения другой алгоритм получения ключа.

Обратите внимание, что побайтовая обработка потоков выполняется очень медленно. Используйте буферы. Наверное, стоит использовать Stream.Copy.

person usr    schedule 11.06.2015
comment
Спасибо за эту ценную информацию. Я обновлю источник, чтобы отразить единую соль и Rfc2898DeriveBytes. Что касается соли, я понимаю, что она помогает, когда вводы key / iv слишком короткие, поэтому я думаю, что это всегда должно быть? - person Storm; 11.06.2015
comment
Это только помогает сделать вывод уникальным. Если пароль состоит из одного символа, остается только 256 возможных паролей, независимо от того, какую соль вы используете. - person usr; 11.06.2015
comment
Так безопасно ли хранить salt в зашифрованном файле? Вы можете вывести IV из Rfc2898DeriveBytes, если вы знаете password и можете получить salt из заголовка зашифрованного файла. - person Storm; 11.06.2015
comment
Да, только ключ является секретным, и его нельзя получить. Соль не обязательна. Предупреждение: злоумышленники могут изменить ваши данные без вашего ведома. Используйте аутентифицированное шифрование, чтобы убедиться, что содержимое не было изменено. - person usr; 11.06.2015
comment
Я реализовал ваши предложения за исключением аутентифицированного шифрования, поскольку есть несколько способов сделать это и, следовательно, выходят за рамки OP. Цените вашу ценную информацию! - person Storm; 11.06.2015
comment
В качестве альтернативы вы можете вообще не использовать соль для ключа и случайным образом сгенерировать IV. Неправильно, это всегда приводит к одному и тому же ключу. Это упрощает выполнение атаки по словарю при наличии нескольких паролей. - person Maarten Bodewes; 11.06.2015
comment
@MaartenBodewes, это правда, но это не делает ответ неверным. Если это нормально для OP, пусть будет так. Обновление: как бы вы провели атаку по словарю? Я никак не могу придумать. Я не думаю, что это возможно. - person usr; 11.06.2015
comment
Скажем так, соль не является дополнительным компонентом PBKDF2 по уважительной причине. - person Maarten Bodewes; 11.06.2015
comment
@MaartenBodewes Я не вижу причин, по которым это не будет необязательным, и пока вы его не указали. - person usr; 11.06.2015
comment
С криптовалютой вы сами должны быть достаточно уверены. Но хорошо, поскольку вы настаиваете: если у вас есть несколько паролей без соли, злоумышленник может выполнить атаку по словарю для всех зашифрованных файлов. Хотя злоумышленнику по-прежнему придется вычислять ключ с помощью PBKDF2, теперь для проверки всех паролей требуется только одна AES-расшифровка всего зашифрованного текста. Это ускорение на x, где x - количество паролей. - person Maarten Bodewes; 11.06.2015
comment
Хм. Похоже, ему потребуется одна полная расшифровка файла для каждого возможного пароля, который он пытается использовать. Каждый пароль приводит к другому ключу, который необходимо проверить. Кроме того, случайный IV делает каждый файл уникальным. Я не понимаю, что вы предлагаете. - person usr; 11.06.2015
comment
Вам не нужно тестировать весь файл, чтобы убедиться, что ключ правильный. - person Maarten Bodewes; 12.06.2015