2

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

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

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

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

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

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

那么问题来了:怎么CreateEncryptor扩展key,有没有PHP实现?我无法更改 C# 代码,所以我不得不在 PHP 中复制这种行为。

4

1 回答 1

3

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

首先,在您的示例中,您将 IV 和 Key 设置为字符串。这是做不到的。因此,我假设我们在字符串上调用 GetBytes(),顺便说一下,这是一个糟糕的主意,因为可用 ASCII 空间中的潜在字节值少于一个字节中所有 256 个值的值;这就是 GenerateIV() 和 GenerateKey() 的用途。我会在最后谈到这一点。

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

现在我们将反编译 Rijndael CreateEncryptor() 调用。当它创建 Transform 对象时,它根本不会对键做任何事情(除了 set 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,当我们输入较短的密钥时,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"));

答对了。合同得到妥善执行。

显而易见的答案是,除非该库碰巧包含相同的明显问题,否则您无法在另一个库中复制它,我将在 Microsoft 的代码中称为错误,因为我真的看不到任何其他选项。

但这个答案将是一个警察。通过检查密钥调度程序,我们可以弄清楚实际发生了什么。

初始化扩展密钥时,它会使用 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

那是 4,161,615 次传球/秒。218,340,105,584,896 / 4,161,615 / 3600 / 24 = 607 天。

好吧,607天还不错。但我总是可以启动一堆亚马逊服务器,然后通过询问 607 个等效实例来计算 1/607 的搜索空间,将其缩短到大约 1 天。那要花多少钱?不到 1000 美元,假设每个实例在某种程度上只和我忙碌的笔记本电脑一样高效。否则更便宜,更快。

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

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

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

所以你去。

于 2013-09-25T23:02:44.660 回答