如何使用口令生成Rijndael密钥和向量?

33

如何使用口令生成Rijndael KEY和IV?密钥长度必须为256位。


3
你使用的是什么语言?哪个库? - uvesten
2
感谢所有的回复。我会给大家点赞的。 - Predator
1
不要这样做;学习一下IV实际上是如何工作的。请查看我在所选答案中的回复,了解为什么这是一个糟糕的想法。 - Rushyo
3
“@Spongeboy Nope. The fundamental premise of the question is wrong. You don't generate an IV from the same source as the key. You generate a unique IV with no relationship to the key every time you encrypt a message, then include that IV with that message. It is impossible to both answer this question and provide a working solution as the question is based on false premises.”翻译为中文为:“@Spongeboy 不对。问题的基本前提是错误的。你不应该从与密钥相同的来源生成IV。每次加密消息时,您都应该生成一个与密钥无关的唯一IV,然后将该IV包含在该消息中。由于问题基于错误的前提,因此无法同时回答这个问题和提供有效的解决方案。” - Rushyo
1
@Spongeboy 在 C# 中,你可以调用 GenerateIV() 方法来获取一个合适的 IV。然后,在对明文进行加密之后,将 IV 添加为密文的前 16 个字节。当你解密密文时,取出前 16 个字节,并将其作为解密时所使用的 IV。 - Rushyo
显示剩余2条评论
6个回答

62

我认为你正在寻找基于密码的密钥派生方法。有一个实现它的Rfc2898DeriveBytes类。

Rfc2898DeriveBytes需要一个密码、一个盐和一个迭代计数,然后通过调用GetBytes方法生成密钥。

RFC 2898包括从密码和盐创建密钥和初始化向量(IV)的方法。您可以使用基于密码的密钥派生函数PBKDF2,通过允许生成虚拟上限长度的密钥来导出密钥。 Rfc2898DeriveBytes类可用于从基键和其他参数生成派生键。在基于密码的密钥派生函数中,基键是密码,而其他参数是盐值和迭代计数。

有关PBKDF2的更多信息,请参见RFC 2898,“PKCS#5:基于密码的加密规范版本2.0”。

示例:

public static byte[] CreateKey(string password)
{
    var salt = new byte[] { 1, 2, 23, 234, 37, 48, 134, 63, 248, 4 };

    const int Iterations = 9872;
    using (var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, salt, Iterations))
        return rfc2898DeriveBytes.GetBytes(32);
}

您可以在任何对称算法中使用DeriveBytes,而不仅仅是Rijndael
例如:

public static SymmetricAlgorithm InitSymmetric(SymmetricAlgorithm algorithm, string password, int keyBitLength)
{
    var salt = new byte[] { 1, 2, 23, 234, 37, 48, 134, 63, 248, 4 };

    const int Iterations = 234;
    using (var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, salt, Iterations))
    {
        if (!algorithm.ValidKeySize(keyBitLength))
            throw new InvalidOperationException("Invalid size key");

        algorithm.Key = rfc2898DeriveBytes.GetBytes(keyBitLength / 8);
        algorithm.IV = rfc2898DeriveBytes.GetBytes(algorithm.BlockSize / 8);
        return algorithm;
    }
}

private static byte[] Transform(byte[] bytes, Func<ICryptoTransform> selectCryptoTransform)
{
    using (var memoryStream = new MemoryStream())
    {
        using (var cryptoStream = new CryptoStream(memoryStream, selectCryptoTransform(), CryptoStreamMode.Write))
            cryptoStream.Write(bytes, 0, bytes.Length);
        return memoryStream.ToArray();
    }
}

使用方法:

public static void Main()
{
    using (var rijndael = InitSymmetric(Rijndael.Create(), "TestPassword", 256))
    {
        var text = "Some text to encrypt";
        var bytes = Encoding.UTF8.GetBytes(text);

        var encryptedBytes = Transform(bytes, rijndael.CreateEncryptor);
        var decryptedBytes = Transform(encryptedBytes, rijndael.CreateDecryptor);

        var decryptedText = Encoding.UTF8.GetString(decryptedBytes);
        Debug.Assert(text == decryptedText);
    }
}

请确保更改saltiterations参数。


你需要为每次使用此方法传入一个随机盐吗?你可以这样做:像你的示例一样使用一个常量盐,但是单独派生IV,使用不同的密码。例如,如果你有一个用户记录,你可以从一个常量盐和用户ID中派生IV吗?这将避免在某处存储盐,并且你可以依赖现有数据(在这种情况下)。 - John B
正如 @Rushyo 在问题的评论中所提到的,IV 不应该从密码派生,而是应该调用 GenerateIV,然后将其放在加密字符串的前面。 - Spongeboy

44

这是我在互联网上找到的即插即用代码。它只需要运行:

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

private static readonly byte[] SALT = new byte[] { 0x26, 0xdc, 0xff, 0x00, 0xad, 0xed, 0x7a, 0xee, 0xc5, 0xfe, 0x07, 0xaf, 0x4d, 0x08, 0x22, 0x3c };

public static byte[] Encrypt(byte[] plain, string password)
{
    MemoryStream memoryStream;
    CryptoStream cryptoStream;
    Rijndael rijndael = Rijndael.Create();
    Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(password, SALT);
    rijndael.Key = pdb.GetBytes(32);
    rijndael.IV = pdb.GetBytes(16);
    memoryStream = new MemoryStream();
    cryptoStream = new CryptoStream(memoryStream, rijndael.CreateEncryptor(), CryptoStreamMode.Write);
    cryptoStream.Write(plain, 0, plain.Length);
    cryptoStream.Close();
    return memoryStream.ToArray();
}

public static byte[] Decrypt(byte[] cipher, string password)
{
    MemoryStream memoryStream;
    CryptoStream cryptoStream;
    Rijndael rijndael = Rijndael.Create();
    Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(password, SALT);
    rijndael.Key = pdb.GetBytes(32);
    rijndael.IV = pdb.GetBytes(16);
    memoryStream = new MemoryStream();
    cryptoStream = new CryptoStream(memoryStream, rijndael.CreateDecryptor(), CryptoStreamMode.Write);
    cryptoStream.Write(cipher, 0, cipher.Length);
    cryptoStream.Close();
    return memoryStream.ToArray();
}

19
@Gens - 我一直相信应该选择最佳答案,即对所有用户最有用的答案。我花了很多时间制作可重用的 API 和示例。虽然你现在让新用户感到鼓舞,但这对老用户来说是绝对不公平和令人沮丧的。 - Alex Aza
5
关于这两行代码: rijndael.Key = pdb.GetBytes(32); rijndael.IV = pdb.GetBytes(16); 这是否意味着IV只是Key的前一半?这会对安全性产生影响吗? 这两行代码的意思是将pdb(PasswordDeriveBytes)生成的32字节密钥设置为rijndael加密算法的密钥,同时将pdb生成的16字节密钥设置为rijndael加密算法的初始化向量(IV)。这并不意味着IV是Key的前一半,也不会对安全性产生影响。 - Spongeboy
1
@Spongeboy 是的。我怀疑可以通过攻击来确定IV,从而建立原始密钥(请记住Rijndael不将IV视为机密),这将允许对原始密码/盐组合进行暴力攻击(只需不断尝试值,直到获得该IV)。 - Rushyo
9
重复使用相同的初始向量(IV)存在严重问题,这只是冰山一角。请不要这样做。每次重复使用相同的IV,都会使加密强度急剧下降,存在极高的安全风险。 - Rushyo
1
此外,如果它是密钥的前半部分(稍后将进行检查),那么恢复IV将使得获取其余密钥的问题减少1700万亿倍。编辑:顺便说一下,不是前半部分。然而,这并不意味着您不能从中潜在地恢复一半的密钥。 - Rushyo
显示剩余4条评论

9

IV必须是随机的(不需要是无法预测的随机,只需要足够随机以避免被重用)。

关于从密码生成密钥,您需要寻找一个key derivation function,现在至少有三个好选择(PBKDF2,bcrypt,scrypt),使用先前某位帖子建议的非迭代哈希往往会导致不安全的系统。

另外不要使用AES或Rijndael,它们并不完全相同。使用不属于AES的Rijndael组合可能会在以后造成互操作性问题,而这些函数组合的安全性也没有得到很好的研究。


4

IV必须是随机的(通常与加密数据一起传递),而密钥可以通过多种方式派生:如果密码长度小于32个字符,只需将密码填充到密钥长度即可(这种方法不够可靠),或者使用SHA2哈希算法派生密钥(更可靠),也可以使用一些更复杂的方式。


2
不要在密码中填充任何内容。不要使用字符串输入作为密钥。不要使用SHA2。使用专门为此问题设计的密钥派生算法,例如PKBDF2。此外,在使用密钥派生算法之前,请务必阅读标签(RTFM)。 - Rushyo
2
@Rushyo,不要发表过于笼统的言论。 - Eugene Mayevski 'Callback
因为玩猜谜游戏并希望加密算法能够正常工作已被证明是一个好主意?与其这样,还不如向机器之神祈祷并忏悔所有的工程感觉。有非常明确的密钥派生方法。人们使用自制的无稽之谈事实上让我作为黑客得到了薪水,但这并不代表它是良好的工程实践。 - Rushyo
嗯,刚看了一下“BlackBox”。我可以看出你是DIY方法的忠实粉丝。祝你好运! - Rushyo

1

使用 Rfc2898DeriveBytes Class

话虽如此,你的安全级别会受到口令长度/强度的影响而下降/受限。所以最好不要这样做。


0
其他答案包含更多信息。对于您只想根据口令生成密钥和初始向量的问题,可以使用以下方法:
        // use something more random in real life
        var salt =  new byte[]{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};

        string password = "my-password";
        Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(password, salt);
        var key = pdb.GetBytes(32);
        var iv = pdb.GetBytes(16);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接