有两种方法可以为RijndaelManaged
对象指定密钥和IV。一个是致电CreateEncryptor
:
var encryptor = rij.CreateEncryptor(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(iv)));
和另一个直接设置Key
和IV
属性:
rij.Key = "1111222233334444";
rij.IV = "1111222233334444";
只要Key
和IV
的长度为16个字节,两种方法都会产生相同的结果。但是如果你的密钥短于16个字节,第一个方法仍然允许你对数据进行编码,第二个方法失败并出现异常。
现在这可能听起来像一个绝对抽象的问题,但我必须使用PHP
&密钥只有10个字节长,以便将加密的消息发送到使用第一种方法的服务器。
所以问题是:CreateEncryptor
如何扩展密钥并且是否有PHP实现?我无法改变C#代码,所以我不得不在PHP中复制这种行为。
答案 0 :(得分:3)
我将不得不从一些假设开始。 (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来自何处的人来说,这是字。这会导致密钥调度程序中出现一个奇怪的分支,这是Nk <= 6的特定路径。
如果不深入细节,这实际上就是“工作”。密钥长度小于16字节的情况(即不会在火球中崩溃)......直到它低于8字节。
然后整个事情发生了巨大的崩溃。
那我们学到了什么?当您使用CreateEncryptor时,您实际上是将一个完全无效的密钥直接输入密钥调度程序,并且它的意外情况有时它并不会直接导致您崩溃(或者可怕的合同完整性泄露,具体取决于您的POV) ;可能是一个意想不到的副作用,即短键长度的特定分叉。
为了完整起见,我们现在可以查看在RijndaelManaged对象中设置Key和IV的其他实现。它们存储在SymmetricAlgorithm基类中,该基类具有以下setter:
if (!this.ValidKeySize(value.Length * 8))
throw new CryptographicException(Environment.GetResourceString("Cryptography_InvalidKeySize"));
宾果。合同得到适当执行。
显而易见的答案是你不能在另一个库中复制它,除非该库恰好包含相同的明显问题,我将调用微软代码中的错误,因为我真的可以&# 39;看到任何其他选择。
但那个答案可能是一个警察。通过检查密钥调度程序,我们可以确定实际发生的情况。
初始化扩展密钥时,它会填充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速度的两倍,因此将我们最终得到的数字减少一半。
然后我们必须考虑到我们几乎肯定会在耗尽整个搜索空间之前找到密钥。所以我们知道它可能会在一小时内完成。
此时我们可以断言数据是否值得加密,破解密钥可能是值得的。
所以你去吧。