RijndaelManaged.CreateEncryptor密钥扩展



有两种方法可以为RijndaelManaged对象指定键和IV。一种是调用CreateEncryptor:

var encryptor = rij.CreateEncryptor(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(iv)));

另一种是直接设置KeyIV属性:

rij.Key = "1111222233334444";
rij.IV = "1111222233334444";

只要KeyIV的长度是16字节,两种方法都会产生相同的结果。但是,如果您的密钥短于16个字节,第一种方法仍然允许您对数据进行编码,而第二种方法会因异常而失败。

这听起来可能是一个绝对抽象的问题,但我必须使用PHP&只有10字节长的密钥,以便向使用第一种方法的服务器发送加密消息。

所以问题是:CreateEncryptor如何扩展密钥,是否有PHP实现?我无法更改C#代码,所以我不得不在PHP中复制这种行为。

我将不得不从一些假设开始。(TL;DR-解决方案大约有三分之二的时间,但旅程更凉爽)。

首先,在您的示例中,您将IV和Key设置为字符串。这是不可能的。因此,我假设我们对字符串调用GetBytes(),顺便说一句,这是一个可怕的想法,因为可用ASCII空间中的潜在字节值比一个字节中所有256个值中的值都少;这就是GenerateIV()和GenerateKey()的作用。我最后会讲到这一点。

接下来,我将假设您使用RijndaelManaged的默认块、密钥和反馈大小:分别为128、256和128。

现在我们将对Rijndael CreateEncryptor()调用进行反编译。当它创建Transform对象时,它根本不会对关键帧执行任何操作(除了设置m_Nk,我稍后会讨论它)。相反,它直接从给定的字节生成密钥扩展。

现在它变得有趣了:

switch (this.m_blockSizeBits > rgbKey.Length * 8 ? this.m_blockSizeBits : rgbKey.Length * 8)

因此:

128 > len(k) x 8 = 128
128 <= len(k) x 8 = len(k) x 8

128/8=16,所以如果len(k)是16,我们可以期望切换到len(k)x 8。如果它更多,那么它也会打开len(k)x 8。如果它较小,它将打开块大小128。

有效的开关值为128、192和256。这意味着只有当它的长度超过16个字节,而不是某种类型的有效块(而不是密钥)长度时,它才会变成默认值(并引发异常)

换句话说,它从不检查RijndaelManaged对象中指定的密钥长度。它直接进入密钥扩展,并开始在块级别操作,只要密钥长度(以位为单位)是128、192、256或中的一个小于128。这实际上是对块大小的检查,而不是对键大小的检查。

那么,既然我们显然没有检查密钥长度,会发生什么呢?答案与关键时间表的性质有关。在Rijndael中输入密钥时,需要先展开密钥才能使用。在这种情况下,它将被扩展到176个字节。为了实现这一点,它使用了一种专门设计的算法,将短字节数组转换为更长的字节数组。

其中一部分涉及检查密钥长度。有点反编译的乐趣,我们发现这被定义为m_Nk。听起来很熟悉?

this.m_Nk = rgbKey.Length / 4;

对于16字节的密钥,Nk是4,当我们输入较短的密钥时,它会更小。这是4个单词,对于任何想知道神奇数字4从哪里来的人来说。这导致密钥调度程序中出现一个奇怪的分叉,Nk<=6.

如果不深入了解细节,这实际上会在密钥长度小于16字节的情况下"工作"(即不会在火球中崩溃)。。。直到它低于8字节。

然后整个事情就崩溃了。

那么我们学到了什么呢?当你使用CreateEncryptor时,你实际上是在把一个完全无效的密钥直接扔到密钥调度程序中,这是一个偶然的发现,有时它不会完全崩溃(或者严重的合同完整性违约,取决于你的POV);这可能是一个意想不到的副作用,因为短键长度有一个特定的分叉。

为了完整起见,我们现在可以看看在RijndaelManaged对象中设置Key和IV的其他实现。这些存储在SymmetricAlgorithm基类中,该基类具有以下设置器:

if (!this.ValidKeySize(value.Length * 8))
throw new CryptographicException(Environment.GetResourceString("Cryptography_InvalidKeySize"));

宾果。正确执行合同。

显而易见的答案是,你不能在另一个库中复制这一点,除非该库恰好包含同样明显的问题,我将称之为微软代码中的错误,因为我真的看不到任何其他选项。

但这样的回答将是一种逃避。通过检查密钥调度程序,我们可以计算出实际发生了什么。

当扩展密钥被初始化时,它会用0x00s填充自己。然后,它用我们的密钥写入前Nk个字(在我们的情况下,Nk=2,因此它填充前2个字或8个字节)。然后,它进入第二阶段,通过将扩展键的其余部分填充到该点之外来对此进行扩展。

所以现在我们知道它本质上是用0x00填充超过8个字节的所有内容,我们可以用0x00s填充它,对吗?不因为这将Nk向上移位到Nk=4。因此,尽管我们的前4个字(16个字节)将按预期填充,但第二阶段将从第17个字节开始扩展,而不是第9个!

这样的解决方案就显得微不足道了。与其用6个额外的字节填充我们的初始密钥,不如剪掉最后2个字节。

所以你在PHP中的直接答案是:

$key = substr($key, 0, -2);

很简单,对吧?:)

现在您可以使用此加密函数进行互操作了。但不要。它可能会破裂。

假设你的密钥使用小写、大写和数字,那么你的搜索空间只有218万亿个密钥。

62字节(26+26+10)是每个字节的搜索空间,因为您从未使用过其他194(256-62)个值。由于我们有8个字节,所以有62^8个可能的组合。218万亿。

我们能以多快的速度试试那个空间里的所有钥匙?让我们来问问openssl我的笔记本电脑(运行大量混乱)能做什么:

Doing aes-256 cbc for 3s on 16 size blocks: 12484844 aes-256 cbc's in 3.00s

这是4161615次传球/秒。218340105584896/4161615/3600/24=607天。

好吧,607天还不错。但我总是可以启动一堆亚马逊服务器,并通过询问607个等效实例来计算1/607的搜索空间,将其减少到大约1天。那要多少钱?不到1000美元,假设每个实例的效率都和我繁忙的笔记本电脑一样高。其他方面更便宜更快。

还有一个实现的速度是openssl1的两倍,所以把我们最终得到的数字减半。

然后我们必须考虑,在耗尽整个搜索空间之前,我们几乎肯定会找到密钥。所以据我们所知,它可能在一个小时内完成。

在这一点上,我们可以断言数据是否值得加密,破解密钥可能是值得的。

好了。

最新更新