Задача

Замок с двумя ключами

встреча посередине хеширование безопасность

Шифровальщик решил, что шестнадцатибитный ключ его самодельного шифра слишком короток, и стал шифровать каждое сообщение дважды: сначала ключом k1, потом ключом k2. «Теперь неизвестных бит тридцать два, — рассудил он, — четыре миллиарда вариантов». Вы перехватили два блока шифра и знаете, что ему соответствует открытый текст: сообщения начинаются с первых слов «Войны и мира» в английском переводе. Найдите оба ключа.

Шифр устроен так. Блок — 32-битное число X, его половины — L = X div 216 и R = X mod 216. Ключ k — число от 0 до 65 535. Восемь раундов, i = 0, 1, …, 7: пара (L, R) заменяется на (R, L xor F(R, k, i)), где

$$F(r, k, i) = \operatorname{rotl}_5\big(((r \oplus k) \cdot 40503 + i) \bmod 2^{16}\big),$$

а rotl5 — циклический сдвиг шестнадцатибитного числа на 5 бит влево: (v · 32 + v div 211) mod 216. Шифр блока — 216 · L + R после восьми раундов. Двойной замок: C = Ek2(Ek1(P)).

Гарантируется, что ровно одна пара ключей (k1, k2) переводит оба известных блока в оба перехваченных.

Вход

Две строки, в каждой — открытый блок P и его шифр C, числа от 0 до 232 − 1.

Выход

Два числа: k1 и k2.

Примеры

Пример 1

Вход
1466264684 4203669661
740315250 3310296384
Выход
11134 6751

Ограничения

Время · Python
6000 мс
Время · Go
1500 мс
Память
256 МБ
Проверка
10 тестов

Разбор

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

1Подсказка 1

Перебор всех пар ключей — 232 двойных шифрований, больше четырёх миллиардов. Даже на Go это десятки секунд. Но что можно вычислить, зная только k1? А только k2?

2Подсказка 2

Каждый раунд обратим: из (R, L xor F(R)) пара (L, R) восстанавливается, потому что F зависит только от R, а он известен. Значит, шифр можно не только применять, но и отматывать назад.

3Подсказка 3

Нашлась пара ключей, у которой середины совпали на первом блоке. Это ответ?

4Идея

Середина M = Ek1(P) = Dk2(C) считается с двух сторон. Зашифруем первый известный блок всеми 216 ключами k1 и запомним середины в словаре «середина → ключи». Расшифруем первый перехваченный блок всеми ключами k2 и для каждой середины посмотрим в словарь. Совпадение — кандидат в ответ. Работа — 2 · 216 вызовов шифра вместо 232. Это встреча посередине.

5Разбор

Остаётся отсеять случайные совпадения. Пар ключей 232, возможных значений середины — тоже 232, поэтому кроме настоящей пары посередине в среднем встречается ещё одна — по чистой случайности. Каждого кандидата проверяем вторым известным блоком: ложная пара проходит её с вероятностью 2−32, а условие гарантирует, что такая пара одна.

Расшифровка раунда: если после раунда пара — (L′, R′), то до него было L = R′ xor F(L′, k, i) и R = L′. Раунды отматываются в обратном порядке, от седьмого к нулевому.

Словарь можно заменить отсортированным списком пар (середина, ключ) и двоичным поиском — так сделано в листинге статьи. Памяти нужно 216 записей.

5+Решение
6Сложность

2 · 216 шифрований и расшифровок по 8 раундов и 216 обращений к словарю — доли секунды даже на Python. Перебор пар был бы в 215 раз дольше.

7Типичные ошибки
  • Первое совпадение — ответ. Самая частая ошибка. Во всех тестах, кроме примера и одного из тестов с крайними ключами, первым посередине встречается случайный кандидат, и ответ без проверки вторым блоком неверен.
  • Перебор пар ключей. Верный, но 4 миллиарда двойных шифрований не укладываются во время.
  • Обратный порядок раундов. При расшифровке номер раунда i входит в F, поэтому идти нужно с 7 до 0; если пройти с 0 до 7, получится не обратная функция, а какая-то другая. Случайные совпадения посередине останутся, но ни одно не пройдёт проверку вторым блоком, и ответа не будет вовсе.
  • Переполнение в произведении. В Go произведение двух шестнадцатибитных чисел в типе uint16 само берётся по модулю 216, как и нужно; в Python результат нужно явно обрезать маской & 0xFFFF.

Алгоритмы задачи