Расширение ключа RijndaelManaged.CreateEncryptor

Есть два способа указать ключ и IV для объекта RijndaelManaged. Один из них, позвонив 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.


person vasily    schedule 06.03.2013    source источник
comment
Разве вы не можете просто вызвать конструктор с помощью короткой клавиши, а затем проверить свойство Key, чтобы увидеть, как расширились данные?   -  person Duncan Jones    schedule 07.03.2013
comment
Я сделал именно это, исходный ключ не соответствует ни одной части расширенного ключа. Так что, к сожалению, это не простая прокладка.   -  person vasily    schedule 07.03.2013
comment
В качестве альтернативы, можете ли вы провести аналогичный эксперимент на стороне PHP, чтобы увидеть, как он расширяет короткий ключ? Затем вы можете использовать полноразмерный ключ на стороне .NET.   -  person Duncan Jones    schedule 07.03.2013
comment
Сделал это также, PHP дополняет его '\ 0', чего не делает .NET.   -  person vasily    schedule 07.03.2013
comment
К сожалению, я не могу изменить ни пароль, ни код, расшифровывающий мое сообщение на стороне сервера. Поэтому, если я добавлю свой Key на стороне PHP, он не будет декодирован обратно на стороне .NET.   -  person vasily    schedule 07.03.2013
comment
Я удалю свои комментарии, теперь я понимаю проблему. Я немного отредактировал ваш вопрос, чтобы сделать его более понятным.   -  person Duncan Jones    schedule 08.03.2013


Ответы (1)


Я собираюсь начать с некоторых предположений. (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)

So:

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;

Nk равно 4 для 16-байтового ключа, меньше, когда мы вводим более короткие ключи. Это 4 слова, для тех, кто интересуется, откуда взялось магическое число 4. Это вызывает любопытную развилку в планировщике ключей, есть определенный путь для Nk ‹= 6.

Если не вдаваться в подробности, то на самом деле это происходит, когда "работает" (т.е. не происходит сбой в огненном шаре) с длиной ключа менее 16 байт... до тех пор, пока она не станет меньше 8 байт.

Затем все это эффектно рушится.

Итак, что мы узнали? Когда вы используете CreateEncryptor, вы на самом деле бросаете полностью недействительный ключ прямо в планировщик ключей, и по счастливой случайности иногда он не приводит к прямому сбою (или ужасному нарушению целостности контракта, в зависимости от вашего POV); вероятно, непреднамеренный побочный эффект того факта, что существует специальная вилка для коротких ключей.

Для полноты картины теперь мы можем взглянуть на другую реализацию, в которой вы устанавливаете ключ и IV в объекте RijndaelManaged. Они хранятся в базовом классе SymmetricAlgorithm, который имеет следующий установщик:

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

Бинго. Контракт соблюдается должным образом.

Очевидный ответ заключается в том, что вы не можете воспроизвести это в другой библиотеке, если только эта библиотека не содержит ту же вопиющую проблему, которую я собираюсь назвать ошибкой в ​​​​коде Microsoft, потому что я действительно не вижу другого варианта.

Но такой ответ был бы отговоркой. Изучив планировщик ключей, мы можем понять, что происходит на самом деле.

Когда расширенный ключ инициализируется, он заполняется 0x00s. Затем он записывает в первые Nk слов наш ключ (в нашем случае Nk = 2, поэтому он заполняет первые 2 слова или 8 байтов). Затем он вступает во вторую стадию расширения этого, заполняя остальную часть расширенного ключа за пределами этой точки.

Итак, теперь мы знаем, что все, что находится за пределами 8 байт, дополняется 0x00, мы можем дополнить его 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 дней неплохо. Но я всегда могу просто запустить несколько серверов Amazon и сократить время примерно до 1 дня, попросив 607 эквивалентных экземпляров вычислить 1/607 пространства поиска. Сколько это будет стоить? Менее 1000 долларов, если предположить, что каждый экземпляр был так же эффективен, как и мой загруженный ноутбук. Иначе дешевле и быстрее.

Также существует реализация, вдвое превышающая скорость openssl1, поэтому разрежьте любую фигуру, с которой мы закончили, пополам.

Тогда мы должны учитывать, что мы почти наверняка найдем ключ, прежде чем исчерпаем все пространство поиска. Так что, насколько нам известно, это может быть закончено через час.

На этом этапе мы можем утверждать, что если данные стоит зашифровать, вероятно, стоит взломать ключ.

Так вот.

person Rushyo    schedule 25.09.2013
comment
Читается как стихотворение. Очень, очень умело. Очень признателен. - person vasily; 28.09.2013