
Доказуемо безопасная лингвистическая стеганография, встраивающая секретные сообщения в текст, сгенерированный LLM, с помощью ротационного диапазонного кодирования. Включает скрипты для встраивания, извлечения и проверки полного цикла.
Rotation Range-Coding (RRC) Steganography — эффективный и доказуемо безопасный лингвистический стеганографический метод, который встраивает секретные сообщения в текст на естественном языке, сгенерированный большими языковыми моделями.
Статья: Efficient Provably Secure Linguistic Steganography via Range Coding (ACL 2026)
| Шаг | Описание |
|---|---|
| Встраивание (Algorithm 3) | Преобразует секретное сообщение в десятичное значение и итеративно вращает его внутри сужающегося интервала, руководствуясь распределением вероятностей языковой модели и смещением, сгенерированным PRNG. Каждый шаг вращения порождает один токен. |
| Извлечение (Algorithm 4) | Повторно запускает языковую модель на стеготексте, чтобы восстановить границы интервала, затем выполняет обратное вращение к исходному десятичному значению и бинаризует его. |
Механизм вращения решает две проблемы классической стеганографии на основе range coding:
.
├── rrc_core.py # Algorithms 3 and 4, shared by every entry point
├── RRC_embed.py # Embedding CLI
├── RRC_extract.py # Extraction CLI (reports recovery accuracy)
├── test_roundtrip.py # Round-trip test across several message lengths
├── fastest_embed.py # Embedding-speed benchmark
├── 0.Prompts.tsv # 1000 prompts (first ~10 words of C4 sequences)
└── requirements.txt
Кодер и декодер используют общий rrc_core.py, а не хранят отдельные
копии цикла. Обе стороны должны совпадать бит в бит, а расхождение
между двумя независимо поддерживаемыми копиями проявляется только как неудачный
round trip и никогда — при осмотре.
pip install -r requirements.txt
python test_roundtrip.py # GPT-2, 32/128/512 bits
python test_roundtrip.py --model meta-llama/Llama-2-7b-hf
python RRC_embed.py \
--language_model meta-llama/Llama-2-7b-hf \
--bit_length 128 --key 42 --top_k -1
python RRC_extract.py \
--language_model meta-llama/Llama-2-7b-hf \
--bit_length 128 --key 42 \
--input_file 1.RC_decimal_Llama-2-7b-hf_bit128.tsv
Кодер записывает секрет, который он встроил, и идентификаторы токенов, которые его переносили, поэтому извлечение сообщает коэффициент восстановления. Оцениваются две модели приёмника:
ids — приёмнику передаётся последовательность токенов. Это изолирует схему
кодирования; всё, что ниже 100%, является численной или логической ошибкой.text — приёмнику передаётся детокенизированная строка, и он токенизирует её заново,
как того требует текстовый канал. Повторная токенизация не всегда воспроизводит
сегментацию отправителя, поэтому это нижняя граница; разрыв между двумя
мерами отражает неоднозначность токенизации, а не схему кодирования. На Llama-2-7b
при 128 битах: 100% для ids, ~91% для text.Важно: отправитель и приёмник должны использовать одну и ту же языковую модель, ключ, длину в битах и настройку top-k, а также должны работать на одинаковом оборудовании и с одинаковыми версиями библиотек. См. Численные требования.
Общие для RRC_embed.py и RRC_extract.py:
| Аргумент | По умолчанию | Описание |
|---|---|---|
--language_model | meta-llama/Llama-3.1-8B | Идентификатор модели HuggingFace |
--bit_length | 128 | Длина секретного сообщения в битах |
--top_k | -1 | Усечение top-k; -1 = полный словарь |
--key | 42 | Симметричный ключ K (seed PRNG) |
--precision_margin | 40 | Десятичные цифры сверх тех, что нужны 2**bit_length |
--max_ctx_len | 3900 | Окно KV-кэша |
--device | auto | cuda или cpu |
RRC_embed.py также принимает --prompts, --output, --limit, --seed,
--max_tokens, --resume, --no_verify и --part / --part_max для
разделения набора промптов между процессами (--part_max 3 с --part 0,1,2, по одному
на GPU).
RRC_extract.py также принимает --input_file, --mode {ids,text,both},
--limit и --verbose.
RRC сужает интервал шириной 2**l до ширины ~1, поэтому восстановленное
значение чувствительно к относительной ошибке с коэффициентом 2**l: при 128 битах
относительное возмущение 1e-16 в любом месте цепочки смещает результат на
~1e22 при допуске 0.5. Извлечение не деградирует постепенно — оно
либо возвращает сообщение, либо возвращает несвязанные биты. Измерено на Llama-2-7b:
| Настройка | Относительное возмущение вероятностей | Извлечение |
|---|---|---|
| Повтор того же декодирования с batch-1 | 0 (побитово идентично) | работает |
| Та же строка при batch 2 / 8 / 16 | ~1e-2 | не работает |
| Левое дополнение промпта | ~1e-2 | не работает |
| Один проход teacher forcing вместо инкрементального декодирования | ~4e-3 | не работает |
Два следствия формируют реализацию. Батчинг недоступен, поэтому последовательности обрабатываются по одной. И приёмник воспроизводит инкрементальное декодирование отправителя шаг за шагом, а не оценивает стеготекст за один прямой проход, хотя он заранее располагает всеми токенами. Оба упрощения дают ~9x ускорение, и ни одно из них не сохраняет вероятности.
Всё после логитов имеет тип float64 и даёт побитово идентичные границы на CPU и GPU, поэтому прямой проход — единственный шаг, который привязывает стеготекст к его оборудованию.
Точность выводится из длины сообщения как
ceil(bit_length * log10(2)) + margin и устанавливается до запуска чего-либо ещё. Она
масштабируется с сообщением: 2**256 требует 78 цифр, а 2**8192 — 2466.
Точность ниже этого порога не даёт чистого отказа. Младшие цифры секретного значения округляются, интервал схлопывается в течение нескольких токенов и проверка завершения срабатывает раньше, поэтому запуск может завершиться и сообщить цифры, которые арифметически недостижимы. Удержание точности на уровне 50 цифр независимо от длины сообщения даёт:
| биты | утилизация при 50 цифрах | выборок завершено | с выведенной точностью |
|---|---|---|---|
| 256 | 157.05% | 8/20 | 100.17% |
| 512 | 299.67% | 9/20 | 102.48% |
| 1024 | 660.91% | 7/10 | 102.58% |
| 2048 | 1046.40% | 1/5 | 98.18% |
| 4096 | 2483.92% | 3/3, ошибка не возникла | 100.87% |
| 8192 | 4766.49% | 1/2 | 99.07% |
--precision_margin регулирует запас сверх выведенного минимума; сам
минимум принудительно соблюдается.
Algorithm 3 останавливается, когда (L + R) / 2 - d_s попадает в (-0.5, 0.5]. Эта проверка
может сработать, когда интервал уже ширины 1, и тогда окно
[mid - 0.5, mid + 0.5) шире интервала, который его несёт. Обратное
вращение является изометрией по модулю ширины интервала, поэтому окно, пересекающее
разрез, оставляет приёмника на целый интервал в стороне от d_s.
Поэтому отправитель воспроизводит обратное вращение приёмника, прежде чем принять
решение остановиться, и выдаёт ещё один токен, если воспроизведение не возвращает секрет. Воспроизведение
— это чистая работа с Decimal (~0.05 мс против ~20 мс для прямого прохода), поэтому
защита не стоит ничего измеримого. --no_verify отключает её.
Измерено на Llama-2-7b:
| биты | восстановлено без защиты | с ней |
|---|---|---|
| 128 | 291/300 (97.0%) | 300/300 |
| 512 | 64/65 (98.5%) | 40/40 |
| 1024 | 20/20 | 20/20 |
| 2048 | 10/10 | 10/10 |