Хорошая практика вектора инициализации AES - PullRequest
47 голосов
/ 07 ноября 2011

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

    public static byte[] EncryptString(string toEncrypt, byte[] encryptionKey)
    {
        var toEncryptBytes = Encoding.UTF8.GetBytes(toEncrypt);
        using (var provider = new AesCryptoServiceProvider())
        {
            provider.Key = encryptionKey;
            provider.Mode = CipherMode.CBC;
            provider.Padding = PaddingMode.PKCS7;
            using (var encryptor = provider.CreateEncryptor(provider.Key, provider.IV))
            {
                using (var ms = new MemoryStream())
                {
                    using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                    {
                        cs.Write(toEncryptBytes, 0, toEncryptBytes.Length);
                        cs.FlushFinalBlock();
                    }
                    return ms.ToArray();
                }
            }
        }
    }

, и это дает последовательные результаты;однако я не смогу расшифровать, не зная / не установив вектор инициализации.Я действительно не хочу передавать три значения в этот метод (для IV), что оставляет меня с жестким программированием IV или извлечением его из ключа.Я хотел бы знать, является ли это хорошей практикой, или это сделает зашифрованное значение уязвимым для атаки каким-то образом ... или я действительно задумался над этим и должен просто жестко кодировать IV?

ОБНОВЛЕНИЕ По предложению Iridium, я попробовал что-то вроде этого:

    public static byte[] EncryptString(string toEncrypt, byte[] encryptionKey)
    {
        if (string.IsNullOrEmpty(toEncrypt)) throw new ArgumentException("toEncrypt");
        if (encryptionKey == null || encryptionKey.Length == 0) throw new ArgumentException("encryptionKey");
        var toEncryptBytes = Encoding.UTF8.GetBytes(toEncrypt);
        using (var provider = new AesCryptoServiceProvider())
        {
            provider.Key = encryptionKey;
            provider.Mode = CipherMode.CBC;
            provider.Padding = PaddingMode.PKCS7;
            using (var encryptor = provider.CreateEncryptor(provider.Key, provider.IV))
            {
                using (var ms = new MemoryStream())
                {
                    ms.Write(provider.IV, 0, 16);
                    using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                    {
                        cs.Write(toEncryptBytes, 0, toEncryptBytes.Length);
                        cs.FlushFinalBlock();
                    }
                    return ms.ToArray();
                }
            }
        }
    }

    public static string DecryptString(byte[] encryptedString, byte[] encryptionKey)
    {
        using (var provider = new AesCryptoServiceProvider())
        {
            provider.Key = encryptionKey;
            provider.Mode = CipherMode.CBC;
            provider.Padding = PaddingMode.PKCS7;
            using (var ms = new MemoryStream(encryptedString))
            {
                byte[] buffer;
                ms.Read(buffer, 0, 16);
                provider.IV = buffer;
                using (var decryptor = provider.CreateDecryptor(provider.Key, provider.IV))
                {
                    using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                    {
                        byte[] decrypted = new byte[encryptedString.Length];
                        var byteCount = cs.Read(decrypted, 0, encryptedString.Length);
                        return Encoding.UTF8.GetString(decrypted, 0, byteCount);
                    }
                }
            }
        }
    }

однако, это показывает что-то странное в моем модульном тесте:

    [TestMethod]
    public void EncryptionClosedLoopTest()
    {
        var roundtrip = "This is the data I am encrypting.  There are many like it but this is my encryption.";
        var encrypted = Encryption.EncryptString(roundtrip, encryptionKey);
        var decrypted = Encryption.DecryptString(encrypted, encryptionKey);
        Assert.IsTrue(roundtrip == decrypted);
    }

мой расшифрованный текст отображается как"92ʪ�F" �, hpv0�� Я шифрую.Многим это нравится, но это мое шифрование », которое кажется почти правильным, но, конечно, совершенно неправильным. Похоже, я рядом, хотя. Я пропустил смещение в потоке памяти?

Ответы [ 8 ]

46 голосов
/ 07 ноября 2011

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

При дешифровании IV может быть прочитан со входа перед зашифрованными данными.

11 голосов
/ 23 мая 2013

При шифровании генерируйте ваш IV и предварительно ожидайте его в зашифрованном тексте (что-то вроде этого)

        using (var aes= new AesCryptoServiceProvider()
        {
            Key = PrivateKey,
            Mode = CipherMode.CBC,
            Padding = PaddingMode.PKCS7
        })
        {

            var input = Encoding.UTF8.GetBytes(originalPayload);
            aes.GenerateIV();
            var iv = aes.IV;
            using (var encrypter = aes.CreateEncryptor(aes.Key, iv))
            using (var cipherStream = new MemoryStream())
            {
                using (var tCryptoStream = new CryptoStream(cipherStream, encrypter, CryptoStreamMode.Write))
                using (var tBinaryWriter = new BinaryWriter(tCryptoStream))
                {
                    //Prepend IV to data
                    //tBinaryWriter.Write(iv); This is the original broken code, it encrypts the iv
                    cipherStream.Write(iv);  //Write iv to the plain stream (not tested though)
                    tBinaryWriter.Write(input);
                    tCryptoStream.FlushFinalBlock();
                }

                string encryptedPayload = Convert.ToBase64String(cipherStream.ToArray());
            }

        }

При дешифровании обратно выведите первые 16 байтов и используйте его в криптопотоке

var aes= new AesCryptoServiceProvider()
                    {
                        Key = PrivateKey,
                        Mode = CipherMode.CBC,
                        Padding = PaddingMode.PKCS7
                    };

                    //get first 16 bytes of IV and use it to decrypt
                    var iv = new byte[16];
                    Array.Copy(input, 0, iv, 0, iv.Length);

                    using (var ms = new MemoryStream())
                    {
                        using (var cs = new CryptoStream(ms, aes.CreateDecryptor(aes.Key, iv), CryptoStreamMode.Write))
                        using (var binaryWriter = new BinaryWriter(cs))
                        {
                            //Decrypt Cipher Text from Message
                            binaryWriter.Write(
                                input,
                                iv.Length,
                                input.Length - iv.Length
                            );
                        }

                        return Encoding.Default.GetString(ms.ToArray());
                    }
5 голосов
/ 06 июня 2013

Я изменил ваш метод расшифровки следующим образом, и он работает:

public static string DecryptString(byte[] encryptedString, byte[] encryptionKey)
{
    using (var provider = new AesCryptoServiceProvider())
    {
        provider.Key = encryptionKey;
        using (var ms = new MemoryStream(encryptedString))
        {
            // Read the first 16 bytes which is the IV.
            byte[] iv = new byte[16];
            ms.Read(iv, 0, 16);
            provider.IV = iv;

            using (var decryptor = provider.CreateDecryptor())
            {
                using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                {
                    using (var sr = new StreamReader(cs))
                    {
                        return sr.ReadToEnd();
                    }
                }
            }
        }
    }
}

Проблема с вашей реализацией заключается в том, что вы читаете слишком много байтов в CryptoStream.Вам действительно нужно прочитать encryptedText.Length - 16.Использование StreamReader упрощает это, поскольку вам больше не нужно беспокоиться о смещениях.

2 голосов
/ 04 апреля 2012

Чтобы разрешить настройку IV для провайдера (как указал Иридиум):

ms.Read(provider.IV, 0, 16);

Я добавил следующее в ваш код:

var iv = new byte[provider.IV.Length];
memoryStream.Read(iv, 0, provider.IV.Length);
using (var decryptor = provider.CreateDecryptor(key, iv);

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

1 голос
/ 07 июня 2019

Отличный вклад от людей.Я взял объединенные ответы от Анкурпателя и Константина, почистил их и добавил некоторые удобные переопределения методов.Это работает с июня 2019 года в .NET Core 2.2.

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

private const int AesKeySize = 16;

public static void Main()
{
    // the data to encrypt
    var message = "Here is some data to encrypt!";

    // create KeySize character key
    var key = "g(KMDu(EEw63.*V`";

    // encrypt the string to a string
    var encrypted = AesEncrypt(message, key);

    // decrypt the string to a string.
    var decrypted = AesDecrypt(encrypted, key);

    // display the original data and the decrypted data
    Console.WriteLine($"Original:   text: {encrypted}");
    Console.WriteLine($"Round Trip: text: {decrypted}");
}

static string AesEncrypt(string data, string key)
{
    return AesEncrypt(data, Encoding.Default.GetBytes(key));
}

static string AesDecrypt(string data, string key)
{
    return AesDecrypt(data, Encoding.Default.GetBytes(key));
}

static string AesEncrypt(string data, byte[] key)
{
    return Convert.ToBase64String(AesEncrypt(Encoding.Default.GetBytes(data), key));
}

static string AesDecrypt(string data, byte[] key)
{
    return Encoding.Default.GetString(AesDecrypt(Convert.FromBase64String(data), key));
}

static byte[] AesEncrypt(byte[] data, byte[] key)
{
    if (data == null || data.Length <= 0)
    {
        throw new ArgumentNullException($"{nameof(data)} cannot be empty");
    }

    if (key == null || key.Length != AesKeySize)
    {
        throw new ArgumentException($"{nameof(key)} must be length of {AesKeySize}");
    }

    using (var aes = new AesCryptoServiceProvider
    {
        Key = key,
        Mode = CipherMode.CBC,
        Padding = PaddingMode.PKCS7
    })
    {
        aes.GenerateIV();
        var iv = aes.IV;
        using (var encrypter = aes.CreateEncryptor(aes.Key, iv))
        using (var cipherStream = new MemoryStream())
        {
            using (var tCryptoStream = new CryptoStream(cipherStream, encrypter, CryptoStreamMode.Write))
            using (var tBinaryWriter = new BinaryWriter(tCryptoStream))
            {
                // prepend IV to data
                cipherStream.Write(iv);
                tBinaryWriter.Write(data);
                tCryptoStream.FlushFinalBlock();
            }
            var cipherBytes = cipherStream.ToArray();

            return cipherBytes;
        }
    }
}

static byte[] AesDecrypt(byte[] data, byte[] key)
{
    if (data == null || data.Length <= 0)
    {
        throw new ArgumentNullException($"{nameof(data)} cannot be empty");
    }

    if (key == null || key.Length != AesKeySize)
    {
        throw new ArgumentException($"{nameof(key)} must be length of {AesKeySize}");
    }

    using (var aes = new AesCryptoServiceProvider
    {
        Key = key,
        Mode = CipherMode.CBC,
        Padding = PaddingMode.PKCS7
    })
    {
        // get first KeySize bytes of IV and use it to decrypt
        var iv = new byte[AesKeySize];
        Array.Copy(data, 0, iv, 0, iv.Length);

        using (var ms = new MemoryStream())
        {
            using (var cs = new CryptoStream(ms, aes.CreateDecryptor(aes.Key, iv), CryptoStreamMode.Write))
            using (var binaryWriter = new BinaryWriter(cs))
            {
                // decrypt cipher text from data, starting just past the IV
                binaryWriter.Write(
                    data,
                    iv.Length,
                    data.Length - iv.Length
                );
            }

            var dataBytes = ms.ToArray();

            return dataBytes;
        }
    }
}
1 голос
/ 08 октября 2018

Принятый ответ правильный, но не дает хорошего примера того, как получить случайный IV.

Оказывается, это лот проще, чем люди пытаются это сделать. AesCryptoServiceProvider в .NET автоматически генерирует криптографически случайный IV каждый раз, когда вы его создаете. И если вам нужно использовать один и тот же экземпляр для нескольких шифрований, вы можете вызвать GenerateIV ()

Вы также можете добавить IV к зашифрованному значению, прежде чем возвращать его, и дешифрующий конец снимает его

private static void Main(string[] args) {
    var rnd = new Random(); 
    var key = new byte[32];  // For this example, I'll use a random 32-byte key.
    rnd.NextBytes(key);
    var message = "This is a test";

    // Looping to encrypt the same thing twice just to show that the IV changes.
    for (var i = 0; i < 2; ++i) {
        var encrypted = EncryptString(message, key);
        Console.WriteLine(encrypted);
        Console.WriteLine(DecryptString(encrypted, key));
    }
}

public static string EncryptString(string message, byte[] key) {
    var aes = new AesCryptoServiceProvider();
    var iv = aes.IV;
    using (var memStream = new System.IO.MemoryStream()) {
        memStream.Write(iv, 0, iv.Length);  // Add the IV to the first 16 bytes of the encrypted value
        using (var cryptStream = new CryptoStream(memStream, aes.CreateEncryptor(key, aes.IV), CryptoStreamMode.Write)) {
            using (var writer = new System.IO.StreamWriter(cryptStream)) {
                writer.Write(message);
            }
        }
        var buf = memStream.ToArray();
        return Convert.ToBase64String(buf, 0, buf.Length);
    }
}

public static string DecryptString(string encryptedValue, byte[] key) {
    var bytes = Convert.FromBase64String(encryptedValue);
    var aes = new AesCryptoServiceProvider();
    using (var memStream = new System.IO.MemoryStream(bytes)) {
        var iv = new byte[16];
        memStream.Read(iv, 0, 16);  // Pull the IV from the first 16 bytes of the encrypted value
        using (var cryptStream = new CryptoStream(memStream, aes.CreateDecryptor(key, iv), CryptoStreamMode.Read)) {
            using (var reader = new System.IO.StreamReader(cryptStream)) {
                return reader.ReadToEnd();
            }
        }
    }  
}

[РЕДАКТИРОВАТЬ: я изменил свой ответ, чтобы включить, как передать IV в зашифрованном значении и получить его при расшифровке. Я также немного переработал пример]

0 голосов
/ 03 июля 2014

Для генерации случайного IV вам понадобится действительно случайное число. Независимо от языка API, который вы используете для генерации случайного числа, должно генерироваться истинное случайное число. У android и ios есть apis, которые генерируют случайные числа на основе данных датчика.

Я недавно внедрил AES 256 со случайным IV (генерируется с использованием действительно случайных чисел) и хэшированный ключ. Для более безопасной (случайной IV + хешированной) кросс-платформенной (android, ios, c #) реализации AES смотрите мой ответ здесь - https://stackoverflow.com/a/24561148/2480840

0 голосов
/ 04 октября 2013

В моем случае, чтобы сгенерировать IV, я использую что-то вроде этого

    /// <summary>
    /// Derives password bytes
    /// </summary>
    /// <param name="Password">password</param>
    /// <returns>derived bytes</returns>
    private Rfc2898DeriveBytes DerivePass(string Password)
    {
        byte[] hash = CalcHash(Password);
        Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(Password, hash, _KEY_ITER);
        return pdb;
    }

    /// <summary>
    /// calculates the hash of the given string
    /// </summary>
    /// <param name="buffer">string to hash</param>
    /// <returns>hash value (byte array)</returns>
    private byte[] CalcHash(string buffer)
    {
        RIPEMD160 hasher = RIPEMD160.Create();
        byte[] data = Encoding.UTF8.GetBytes(buffer);
        return hasher.ComputeHash(data);
    }

, то есть я вычисляю хэш пароля, используя RIPEMD160, и использую его для генерации производных байтов в тот момент, когдаэто касается инициализации шифрования / дешифрования. Я просто использую что-то вроде этого

        Rfc2898DeriveBytes pdb = DerivePass(Password);
        SymmetricAlgorithm alg = _engine;
        alg.Key = pdb.GetBytes(_keySize);
        alg.IV = pdb.GetBytes(_IVSize);

Я не знаю, правильно ли это (вероятно, крипто-гуру здесь будут стрелять в меня: D), но, по крайней мере,это дает мне приличный IV, и мне не нужно хранить его «где-то», поскольку простой ввод правильного пароля вернет необходимое значение IV;как примечание, _engine в приведенном выше примере объявлен как «SymmetricAlgorithm» и инициализируется с использованием чего-то вроде этого

_engine = Rijndael.Create();
_keySize = (_engine.KeySize / 8);
_IVSize = (_engine.BlockSize / 8);

, который создает требуемые крипто-объекты и инициализирует размеры ключа и IV

...