Расширение ключа 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.

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

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 405 849 964 4 416 615/3600/24 ​​= 607 дней.

Хорошо, 607 дней неплохо. Но я всегда могу просто запустить кучу серверов Amazon и сократить это до ~ 1 дня, запросив 607 эквивалентных экземпляров для вычисления 1/607-го места поиска. Сколько это будет стоить? Менее 1000 долларов, предполагая, что каждый экземпляр был как-то таким же эффективным, как мой занятый ноутбук. Дешевле и быстрее в противном случае.

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

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

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

Итак, вы идете.