Замок с двумя ключами
Шифровальщик решил, что шестнадцатибитный ключ его самодельного шифра слишком короток, и стал шифровать каждое сообщение дважды: сначала ключом 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.