Риндаэль справился.Расширение ключа 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.

Author: Duncan Jones, 2013-03-07

1 answers

Мне придется начать с некоторых предположений. (TL;DR - Решение находится примерно на двух третях пути вниз, но путешествие намного круче).

Во-первых, в вашем примере вы устанавливаете IV и ключ для строк. Это невозможно сделать. Поэтому я собираюсь предположить, что мы вызываем getBytes() в строках, что, кстати, ужасная идея, поскольку в используемом пространстве ASCII меньше потенциальных байтовых значений, чем во всех 256 значениях в байте; это то, что GenerateIV() и GenerateKey() предназначены для. Я доберусь до этого в самом конце.

Далее я собираюсь предположить, что вы используете блок по умолчанию, ключ и размер обратной связи для RijndaelManaged: 128, 256 и 128 соответственно.

Теперь мы декомпилируем вызов Rijndael CreateEncryptor(). Когда он создает объект преобразования, он вообще ничего не делает с ключом (кроме набора 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 байт и не является допустимой длиной блока (не ключа) какого-либо рода.

Другими словами, он никогда не сверяется с ключом длина, указанная в управляемом объекте RIJNDAEL. Он переходит прямо к расширению ключа и начинает работать на уровне блока, если длина ключа (в битах) составляет 128, 192, 256 или меньше 128. На самом деле это проверка размера блока, а не размера ключа.

Так что же происходит теперь, когда мы явно не проверили длину ключа? Ответ связан с характером ключевого графика. Когда вы вводите ключ в Rijndael, ключ необходимо развернуть перед ним может быть использован. В этом случае он будет расширен до 176 байт. Для достижения этой цели он использует алгоритм, специально разработанный для преобразования короткого массива байтов в гораздо более длинный массив байтов.

Часть этого включает проверку длины ключа. Немного больше удовольствия от декомпиляции, и мы обнаруживаем, что это определяется как m_Nk. Звучит знакомо?

this.m_Nk = rgbKey.Length / 4;

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

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

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

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

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

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

Бинго. Правильно заключайте контракт принужденный.

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

Но этот ответ был бы полицейским. Проверив планировщик ключей, мы сможем понять, что на самом деле происходит.

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

Итак, теперь мы знаем, что он, по сути, заполняет все, что превышает 8 байт, 0x00, мы можем заполнить его 0x00, верно? Нет; потому что это сдвигает 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, так что сократите любую цифру, которая у нас получилась, пополам.

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

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

Итак, вот так.

 3
Author: Rushyo, 2013-09-26 14:09:03