Хорошая практика вектора инициализации AES
на мой вопрос Шифрование 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();
}
}
}
}
и это дает последовательные результаты; однако я не смогу расшифровать, не зная/ не устанавливая вектор инициализации. Я действительно не хочу передавать три значения в этот метод (on для IV), что оставляет меня с жестким кодированием IV или выводом его из ключ. Я хотел бы знать, является ли это хорошей практикой или это сделает зашифрованное значение уязвимым для атаки... или я действительно слишком много думаю об этом и должен просто жестко закодировать IV?
обновление По предложению иридия я попробовал что-то вроде этого:
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ʪ�Ф"�,hpv0�� шифровании. Таких много, но это мой шифр.- который кажется почти правильно, но, конечно, совершенно неправильно. Похоже, я уже близко. Я пропустил смещение в потоке памяти?
6 ответов
IV должен быть случайным и уникальным для каждого запуска вашего метода шифрования. Вывод его из ключа / сообщения или жесткого кодирования недостаточно безопасен. IV может быть сгенерирован в рамках этого метода, а не передан в него и записан в выходной поток до зашифрованных данных.
при дешифровании, IV можно после этого прочитать от входного сигнала перед зашифрованными данными.
при шифровании создайте свой 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 байт и использовать его в crypto stream
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());
}
Я изменил ваш метод дешифрования следующим образом, и он работает:
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
упрощает это, так как вам больше не нужно беспокоиться о смещениях в любом месте.
чтобы разрешить настройку IV на провайдере (как указал Iridium):
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 генерируется случайным образом от поставщика для каждого шифрования.
в моем случае, чтобы создать 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 и использую его для генерации производных байтов, в тот момент, когда дело доходит до intializing шифрования/дешифрования, я просто использую что-то вроде этого
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 размеры
для генерации случайного IV вам понадобится действительно случайное число. Какой бы язык API вы ни использовали для генерации случайного числа, он должен генерировать истинное случайное число. Как android, так и ios имеют API, которые генерируют случайные числа на основе данных датчика.
недавно я реализовал AES 256 со случайным IV (сгенерированным с использованием действительно случайных чисел) и хэшированным ключом. Для более безопасной (случайный IV + хэшированный ключ) кросс-платформенной (android, ios, c#) реализации AES см. Мой ответ здесь - https://stackoverflow.com/a/24561148/2480840