66

根据我的问题Aes Encryption... missing a important piece,我现在了解到我对字符串创建可逆加密的假设有点偏离。我现在有

    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�� 我正在加密。有很多喜欢它,但这是我的加密。”这似乎几乎是正确的,但当然完全错误。看起来我很接近了。我错过了内存流上的偏移量吗?

4

8 回答 8

65

对于您的加密方法的每次运行,IV 应该是随机且唯一的。从密钥/消息派生或硬编码它不够安全。IV 可以在此方法中生成,而不是传递给它,并在加密数据之前写入输出流。

解密时,可以在加密数据之前从输入中读取 IV。

于 2011-11-07T19:38:36.880 回答
19

加密时,生成您的 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());
                    }
于 2013-05-23T15:55:45.957 回答
8

人们的大力投入。我从 ankurpatel 和 Konstantin 那里得到了综合答案,并对其进行了清理,并添加了一些方便的方法覆盖。这在 .NET Core 2.2 中自 2019 年 6 月起有效。

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.UTF8.GetBytes(key));
}

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

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

static string AesDecrypt(string data, byte[] key)
{
    return Encoding.UTF8.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;
        }
    }
}
于 2019-06-07T05:43:23.187 回答
6

我将您的解密方法修改如下,它可以工作:

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。使用 a 可以StreamReader简化这一点,因为您不再需要担心任何地方的偏移量。

于 2013-06-06T13:32:56.417 回答
6

接受的答案是正确的,但没有提供如何获得随机 IV 的好例子。

事实证明,这比人们试图做到的要容易得多每次构建时,.NET 中的 AesCryptoServiceProvider 都会自动生成一个加密随机 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 并在解密时获取它。我还稍微重构了这个例子]

于 2018-10-08T17:04:06.017 回答
2

为了解决提供者上的 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 是从每个加密的提供者中随机生成的。

于 2012-04-04T16:33:39.027 回答
-1

就我而言,要生成 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 大小

于 2013-10-04T10:38:06.377 回答
-1

要生成随机 IV,您需要一个真正的随机数。无论您使用哪种语言特定的 API 来生成随机数,都应该生成真正的随机数。android 和 ios 都有基于传感器数据生成随机数的 api。

我最近使用随机 IV(使用真正随机数生成)和散列密钥实现了 AES 256。对于 AES 的更安全(随机 IV + 散列密钥)跨平台(android、ios、c#)实现,请参见我的答案 - https://stackoverflow.com/a/24561148/2480840

于 2014-07-03T19:42:19.520 回答