Подробно разбираем алгоритм Rabbit, структуру инициализации и реализацию на Python.

Эта статья — практический воркшоп по шифру Rabbit. Сначала мы научимся "узнавать" алгоритм в чужом коде по его уникальным сигнатурам. Затем мы перейдем к делу: пошагово реализуем на Python весь конвейер, включая инициализацию ключа (key setup) и вектора (IV), а также потоковую обработку данных «чтение-обработка-запись». Результатом станет готовый модуль для шифрования и дешифровки файлов, который мы немедленно проверим на корректность с помощью официальных тестовых векторов.
Rabbit относится к синхронным потоковым шифрам. Он генерирует псевдослучайный поток байтов, который складывается по XOR с открытым текстом. Ключ 128 бит и по желанию вектор инициализации 64 бита. На каждый такт получается 128 бит выходных данных. Алгоритм рассчитан на высокую скорость в программных реализациях и давно опубликован для свободного использования. Для вхождения в тему достаточно помнить два правила. Первое. Каждый ключ с каждым IV образуют независимый поток. Второе. Один и тот же IV нельзя повторять с тем же ключом, иначе теряется конфиденциальность.
Для углубления полезно держать под рукой описание алгоритма и тестовые векторы. Мы дадим ссылки в конце, а по ходу текста напомним, где искать нужные фрагменты.
Если на руках есть проект с заглушками вида cipher_update, но без явных названий алгоритмов, Rabbit можно вычислить по нескольким признакам. Ниже перечислены самые заметные детали. Они встречаются вместе и хорошо ловятся поиском по коду.
x[8] и c[8], плюс одиночный перенос b.0x4D34D34D, 0xD34D34D3, 0x34D34D34. Они прибавляются по кругу при обновлении c[i].g(). Вызывается для каждой пары (x[i], c[i]) и вычисляется как квадрат суммы с последующим смешиванием старших и младших частей. В коде это похоже на «возвести 32-битную сумму в квадрат как 64-битное число, затем сложить XOR ее верхнюю и нижнюю половины и снова усечь до 32 бит».x и c, потом четыре прогонки, затем c[i] ^= x[i+4]. При наличии IV делается специфическое раскладывание 64 бит IV на четыре 32-битных слова, которые по кругу XORятся в c, после чего снова четыре прогонки.
Эти приметы в совокупности дают уверенную идентификацию. Если в проекте совпали константы счетчиков, квадратная g() и двойные сдвиги, можно считать, что перед нами Rabbit.
Перед тем как писать модуль, зафиксируем простой, но надежный конвейер. Для файлов шифрование строится на трех шагах. Читаем очередной блок байт, генерируем такое же число байт ключевого потока, выполняем XOR и пишем результат. Размер блока выбираем исходя из баланса между скоростью и потреблением памяти. На практике 1–4 МБ на чтение подходят для большинства конфигураций.
Стримовая природа полезна тем, что не требуется загружать файл целиком. Можно обрабатывать гигабайты последовательно. Важно только синхронно породить ровно столько же байт ключевого потока, сколько пришло из источника. Ошибки со сдвигом по потоку приводят к порче результата на всем хвосте файла, а не только в месте сбоя.
Инициализация в Rabbit состоит из двух частей. Сначала создается «мастер-состояние» из 128-битного ключа. Ключ разбивается на восемь 16-битных слов в порядке от младших к старшим байтам. Из них формируются начальные x[i] и c[i] по чередующейся схеме. Затем выполняются четыре итерации обновления, после чего счетчики c дополнительно смешиваются с состоянием x сдвигом на четыре позиции по модулю восемь.
Если нужен IV, его 64 бита раскладываются на четыре 32-битных значения в заданном порядке и по кругу XORятся в массив c. После этого снова выполняются четыре итерации. Такой порядок обеспечивает независимое начальное состояние для каждой пары ключ и IV. В следующем разделе покажем это на коде Python.
Ниже самодостаточная реализация Rabbit. Она не зависит от внешних библиотек и годится для встраивания в служебные утилиты. Все операции выполняются в рамках 32-битных слов с усечением по модулю 232, а извлечение блока отдает 16 байт за такт. Комментарии оставлены максимально простыми.
# rabbit_cipher.py
# Реализация потокового шифра Rabbit на чистом Python.
# Комментарии на русском чтобы упрощать разбор.
from __future__ import annotations
from typing import Iterable, Generator
import struct
import os
_WORD = 0x100000000 # 2**32 для усечения
class Rabbit:
"""
Потоковый шифр Rabbit. Ключ 16 байт. IV опционально 8 байт.
"""
_A = (0x4D34D34D, 0xD34D34D3, 0x34D34D34) # константы инкремента счетчиков
def __init__(self, key: bytes, iv: bytes | None = None) -> None:
if len(key) != 16:
raise ValueError("Ключ должен быть ровно 16 байт")
if iv is not None and len(iv) not in (0, 8):
raise ValueError("IV должен быть 8 байт или не задан")
# Разбиваем ключ на 8 слов по 16 бит. Младшие байты вперед.
k = struct.unpack("<8H", key)
# Начальные массивы состояния и счетчиков.
self.x: list[int] = []
self.c: list[int] = []
self.b: int = 0 # перенос для системы счетчиков
for j in range(8):
v1, v2 = k[(j + 0) & 7], k[(j + 1) & 7]
w1, w2 = k[(j + 4) & 7], k[(j + 5) & 7]
if j % 2 == 0:
# Xj = K_{j+1} || K_j
# Cj = K_{j+4} || K_{j+5}
self.x.append(((v2 << 16) | v1) & 0xFFFFFFFF)
self.c.append(((w1 << 16) | w2) & 0xFFFFFFFF)
else:
# Xj = K_{j+5} || K_{j+4}
# Cj = K_j || K_{j+1}
self.x.append(((w2 << 16) | w1) & 0xFFFFFFFF)
self.c.append(((v1 << 16) | v2) & 0xFFFFFFFF)
# Четыре прогонки для вымешивания
self._iterate4()
# Дополнительное смешивание счетчиков через XOR с X[j+4]
self.c = [(cj ^ self.x[(j + 4) & 7]) & 0xFFFFFFFF for j, cj in enumerate(self.c)]
# Если задан IV, применяем процедуру IV setup
if iv:
# Разложение 64 бит IV на i0..i3 в порядке little-endian
i0, i2 = struct.unpack("<LL", iv)
i1 = ((i0 >> 16) | (i2 & 0xFFFF0000)) & 0xFFFFFFFF
i3 = ((i2 << 16) | (i0 & 0x0000FFFF)) & 0xFFFFFFFF
mix = (i0, i1, i2, i3)
self.c = [(cj ^ mix[j % 4]) & 0xFFFFFFFF for j, cj in enumerate(self.c)]
self._iterate4()
# --- Внутренние примитивы ---
@staticmethod
def _rotl32(v: int, r: int) -> int:
r &= 31
return ((v << r) | (v >> (32 - r))) & 0xFFFFFFFF
@staticmethod
def _g(x: int, c: int) -> int:
# u = (x + c) mod 2**32
u = (x + c) & 0xFFFFFFFF
# t = u*u как 64-битное, затем XOR верхней и нижней половин
t = (u * u) & 0xFFFFFFFFFFFFFFFF
return ((t ^ (t >> 32)) & 0xFFFFFFFF)
def _counter_update(self) -> None:
carry = self.b
for i in range(8):
carry = (self.c[i] + self._A[i % 3] + carry)
self.c[i] = carry & 0xFFFFFFFF
carry >>= 32
self.b = carry & 1
def _next_state(self) -> None:
# Обновляем счетчики
self._counter_update()
# Вычисляем g[i] для каждой пары
g = [self._g(self.x[i], self.c[i]) for i in range(8)]
# Обновляем X с перекрестным смешиванием через сдвиги на 8 и 16
self.x[0] = (g[0] + self._rotl32(g[7], 16) + self._rotl32(g[6], 16)) & 0xFFFFFFFF
self.x[1] = (g[1] + self._rotl32(g[0], 8) + g[7]) & 0xFFFFFFFF
self.x[2] = (g[2] + self._rotl32(g[1], 16) + self._rotl32(g[0], 16)) & 0xFFFFFFFF
self.x[3] = (g[3] + self._rotl32(g[2], 8) + g[1]) & 0xFFFFFFFF
self.x[4] = (g[4] + self._rotl32(g[3], 16) + self._rotl32(g[2], 16)) & 0xFFFFFFFF
self.x[5] = (g[5] + self._rotl32(g[4], 8) + g[3]) & 0xFFFFFFFF
self.x[6] = (g[6] + self._rotl32(g[5], 16) + self._rotl32(g[4], 16)) & 0xFFFFFFFF
self.x[7] = (g[7] + self._rotl32(g[6], 8) + g[5]) & 0xFFFFFFFF
def _iterate4(self) -> None:
self._next_state()
self._next_state()
self._next_state()
self._next_state()
def _extract_block(self) -> bytes:
# Собираем 128 бит из восьми половинок по схеме Rabbit
s = []
pairs = ((0, 5), (3, 0), (2, 7), (5, 2), (4, 1), (7, 4), (6, 3), (1, 6))
for a, b in pairs:
lo = self.x[a] & 0xFFFF
hi = (self.x[b] >> 16) & 0xFFFF
s.append(lo ^ hi)
return struct.pack("<8H", *s)
def keystream(self) -> Generator[int, None, None]:
# Генератор байт ключевого потока без ограничения длины
while True:
self._next_state()
block = self._extract_block()
for byte in block:
yield byte
def xor_bytes(self, data: bytes) -> bytes:
# Выделяем столько же байт из потока и складываем по XOR
out = bytearray(len(data))
ks = self.keystream()
for i, b in enumerate(data):
out[i] = b ^ next(ks)
return bytes(out)
def encrypt_stream(fin, fout, key: bytes, iv: bytes | None, chunk: int = 1 << 20) -> None:
"""
Потоковое шифрование из файла в файл. По XOR с ключевым потоком.
"""
cipher = Rabbit(key, iv)
ks = cipher.keystream()
# Буферизованная обработка
while True:
buf = fin.read(chunk)
if not buf:
break
out = bytearray(len(buf))
for i, b in enumerate(buf):
out[i] = b ^ next(ks)
fout.write(out)
def decrypt_stream(fin, fout, key: bytes, iv: bytes | None, chunk: int = 1 << 20) -> None:
"""
Для потокового шифра операция та же самая. Функция оставлена для симметрии интерфейса.
"""
encrypt_stream(fin, fout, key, iv, chunk)
def hex_to_bytes(x: str, expected_len: int | None = None) -> bytes:
x = x.replace(" ", "").replace("-", "")
if len(x) % 2 != 0:
raise ValueError("Нечетная длина hex-строки")
b = bytes.fromhex(x)
if expected_len is not None and len(b) != expected_len:
raise ValueError(f"Ожидалось {expected_len} байт, получено {len(b)}")
return b
Добавим тонкую обертку, чтобы удобно зашифровать и расшифровать файл из командной строки. Ключ и IV передаются в шестнадцатеричном виде. При необходимости IV можно сгенерировать заранее и сохранить рядом с шифртекстом.
# rabbit_file.py
import argparse, sys, os
from rabbit_cipher import encrypt_stream, decrypt_stream, hex_to_bytes
def main():
p = argparse.ArgumentParser(prog="rabbit_file", description="Шифрование и дешифровка файлов потоком Rabbit")
p.add_argument("mode", choices=["enc", "dec"], help="Режим работы enc или dec")
p.add_argument("-k", "--key", required=True, help="Ключ в hex длиной 16 байт")
p.add_argument("-i", "--iv", default="", help="IV в hex длиной 8 байт. Можно опустить")
p.add_argument("inp", help="Входной файл")
p.add_argument("out", help="Выходной файл")
p.add_argument("--chunk", type=int, default=1<<20, help="Размер блока чтения в байтах")
args = p.parse_args()
key = hex_to_bytes(args.key, 16)
iv = hex_to_bytes(args.iv, 8) if args.iv else None
with open(args.inp, "rb") as fin, open(args.out, "wb") as fout:
if args.mode == "enc":
encrypt_stream(fin, fout, key, iv, args.chunk)
else:
decrypt_stream(fin, fout, key, iv, args.chunk)
if __name__ == "__main__":
main()
Надежный способ убедиться в корректности реализации. Сначала берем случай из набора «без IV», где на нулевом ключе первые 16 байт выходного потока равны последовательности B1 57 54 F0 36 A5 D6 EC F5 6B 45 26 1C 4A F7 02. Затем проверяем один пример с IV. Ниже простая проверка в коде.
# tests_rabbit.py
from rabbit_cipher import Rabbit, hex_to_bytes
def take(n, g):
out = bytearray()
for _ in range(n):
out.append(next(g))
return bytes(out)
def test_no_iv():
key = bytes(16) # 00..00
r = Rabbit(key)
ks = take(16, r.keystream())
want = bytes.fromhex("B1 57 54 F0 36 A5 D6 EC F5 6B 45 26 1C 4A F7 02")
assert ks == want
def test_with_iv():
key = bytes(16)
iv = bytes(8)
r = Rabbit(key, iv)
ks = take(16, r.keystream())
want = bytes.fromhex("C6 A7 27 5E F8 54 95 D8 7C CD 5D 37 67 05 B7 ED")
assert ks == want
if __name__ == "__main__":
test_no_iv(); test_with_iv()
print("Обе проверки пройдены")
Если обе проверки проходят, базовая часть реализована верно. Для полной уверенности можно свериться с расширенными отладочными значениями внутреннего состояния и расхождений не будет.
Пусть есть файл photo.raw. Создадим ключ и IV и пропустим его через утилиту. В демонстрации IV задан явно, но ничто не мешает генерировать его и сохранять отдельно рядом с шифртекстом.
# ключ и IV в шестнадцатеричном виде
KEY=00112233445566778899aabbccddeeff
IV=0f1e2d3c4b5a6978
# шифрование
python rabbit_file.py enc -k $KEY -i $IV photo.raw photo.raw.rab
# расшифровка
python rabbit_file.py dec -k $KEY -i $IV photo.raw.rab photo.restored.raw
Пара файлов photo.raw и photo.restored.raw должна совпасть побайтно. Это легко проверяется сравнив хэши. Важно помнить, что для каждого нового сообщения с одним и тем же ключом выбирается новый IV. Повтор со старым IV делает поток одинаковым. Это упрощает анализ и раскрывает одинаковые префиксы входных данных.
Потоковое шифрование по XOR не защищает от изменений шифртекста. Любой байт можно подменить и это предсказуемо исказит соответствующий байт расшифровки. Значит в реальных приложениях нужен контроль целостности. Самый прямой путь применить HMAC поверх шифртекста или воспользоваться контейнером, где вместе с данными хранится тег целостности. Отдельно стоит повторить главный запрет. Нельзя повторять IV для одного и того же ключа.
Есть еще один слой практической гигиены. Если пользователю удобнее работать с паролем, а не с шестнадцатеричным ключом, нужен стойкий способ получения ключа из пароля. Для этого подойдет KDF с солью вроде PBKDF2 или scrypt. В эту статью такой код не включен, чтобы держать фокус на самом Rabbit, однако логика проста. Производим ключ длиной 16 байт, генерируем соль, сохраняем ее рядом с шифртекстом, а IV выбираем случайно и тоже сохраняем.
На практике поиск инициализации в чужом проекте строится как небольшая археология. Помогают регулярные выражения и чтение функций, где встречаются упоминания о ключе или IV. Ниже несколько подсказок, которые часто срабатывают.
4D34D34D|D34D34D3|34D34D34 выводит на систему счетчиков.uint64_t t = (u * u); g[i] = (uint32_t)((t ^ (t >> 32)) & 0xFFFFFFFF);.ROTL32(y, 8) и ROTL32(y, 16).(0,5), (3,0), (2,7), (5,2), (4,1), (7,4), (6,3), (1,6) практически уникален.Нашли это место значит вы у цели. Далее остается связать его с функциями, которые читают входной поток и пишут выходной. Там виден чистый цикл чтение → обработка → запись. Иногда авторы кэшируют несколько блоков ключевого потока вперед ради ускорения. Для корректности это не играет роли, лишь бы количество байт совпадало с длиной входа.
.iv.Мы научились узнавать Rabbit в исходниках по характерным признакам и разобрали конвейер чтение → обработка → запись. Настроили инициализацию по ключу и IV, написали модуль на Python и подтвердили корректность тестами. Такой фундамент уже подходит для реальной задачи. Дальше остается добавить аутентификацию, удобную упаковку метаданных и интеграцию с остальным кодом проекта.