
Stéganographie linguistique prouvablement sécurisée intégrant des messages secrets dans du texte généré par LLM via un codage par plage de rotation. Comprend des scripts d'encodage, d'extraction et de vérification en boucle.
Rotation Range-Coding (RRC) Steganography — une méthode stéganographique linguistique efficace et prouvablement sûre qui intègre des messages secrets dans du texte en langage naturel généré par de grands modèles de langage.
Article : Efficient Provably Secure Linguistic Steganography via Range Coding (ACL 2026)
| Étape | Description |
|---|---|
| Intégration (Algorithme 3) | Convertit le message secret en une valeur décimale et la fait tourner de manière itérative dans un intervalle rétrécissant, guidé par la distribution de probabilité du LM et un décalage généré par PRNG. Chaque étape de rotation produit un token. |
| Extraction (Algorithme 4) | Réexécute le LM sur le stégotexte pour récupérer les bornes de l'intervalle, puis effectue une rotation inverse pour revenir à la valeur décimale d'origine et la binarise. |
Le mécanisme de rotation résout deux problèmes du stéganographie par codage d'intervalle classique :
.
├── 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
L'encodeur et le décodeur partagent rrc_core.py plutôt que de conserver des copies séparées de la boucle. Les deux côtés doivent s'accorder bit à bit, et une divergence entre deux copies maintenues indépendamment ne se manifeste que par un échec de l'aller-retour, jamais à l'inspection.
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
L'encodeur enregistre le secret qu'il a intégré et les identifiants de tokens qui l'ont porté, de sorte que l'extraction rapporte un taux de récupération. Deux modèles de récepteur sont évalués :
ids — le récepteur reçoit la séquence de tokens. Cela isole le schéma de codage ; tout résultat inférieur à 100 % est un défaut numérique ou logique.text — le récepteur reçoit la chaîne détokénisée et la retokénise, comme l'exige un canal textuel. La retokénisation ne reproduit pas toujours la segmentation de l'émetteur, il s'agit donc d'une borne inférieure ; l'écart entre les deux mesures l'ambiguïté de tokénisation plutôt que le schéma de codage. Sur Llama-2-7b à 128 bits : 100 % pour ids, ~91 % pour text.Important : l'émetteur et le récepteur doivent partager le modèle de langage, la clé, la longueur en bits et le réglage top-k, et doivent également exécuter les mêmes versions de matériel et de bibliothèques. Voir Exigences numériques.
Partagés par RRC_embed.py et RRC_extract.py :
| Argument | Défaut | Description |
|---|---|---|
--language_model | meta-llama/Llama-3.1-8B | Identifiant de modèle HuggingFace |
--bit_length | 128 | Longueur du message secret en bits |
--top_k | -1 | Troncature top-k ; -1 = vocabulaire complet |
--key | 42 | Clé symétrique K (graine PRNG) |
--precision_margin | 40 | Chiffres décimaux au-delà de ceux nécessaires à 2**bit_length |
--max_ctx_len | 3900 | Fenêtre de cache KV |
--device | auto | cuda ou cpu |
RRC_embed.py accepte également --prompts, --output, --limit, --seed, --max_tokens, --resume, --no_verify, et --part / --part_max pour répartir un ensemble de prompts entre processus (--part_max 3 avec --part 0,1,2, un par GPU).
RRC_extract.py accepte également --input_file, --mode {ids,text,both}, --limit et --verbose.
RRC réduit un intervalle de largeur 2**l à une largeur ~1, de sorte que la valeur récupérée est sensible à l'erreur relative d'un facteur 2**l : à 128 bits, une perturbation relative de 1e-16 n'importe où dans la chaîne décale le résultat d'environ 1e22 pour une tolérance de 0,5. L'extraction ne se dégrade pas progressivement — elle renvoie soit le message, soit des bits sans rapport. Mesuré sur Llama-2-7b :
| Réglage | Perturbation relative des probabilités | Extraction |
|---|---|---|
| Répétition du même décodage batch-1 | 0 (identique bit à bit) | fonctionne |
| La même ligne en batch 2 / 8 / 16 | ~1e-2 | échoue |
| Remplissage à gauche du prompt | ~1e-2 | échoue |
| Une passe teacher-forced au lieu du décodage incrémental | ~4e-3 | échoue |
Deux conséquences façonnent l'implémentation. Le traitement par lots est indisponible, donc les séquences sont traitées une à une. Et le récepteur rejoue le décodage incrémental de l'émetteur étape par étape plutôt que de scorer le stégotexte en une seule passe avant, même s'il détient tous les tokens à l'avance. Les deux raccourcis mesurent ~9x plus rapides et aucun ne préserve les probabilités.
Tout ce qui suit les logits est en float64 et produit des bornes identiques bit à bit sur CPU et GPU, de sorte que la passe avant est la seule étape qui lie un stégotexte à son matériel.
La précision est dérivée de la longueur du message comme
ceil(bit_length * log10(2)) + margin et définie avant que quoi que ce soit d'autre ne s'exécute. Elle évolue avec le message : 2**256 nécessite 78 chiffres et 2**8192 en nécessite 2466.
Une précision inférieure à ce seuil n'échoue pas proprement. Les chiffres de poids faible de la valeur secrète sont arrondis, l'intervalle s'effondre en quelques tokens et le test de terminaison se déclenche prématurément, de sorte qu'une exécution peut se terminer et rapporter des chiffres arithmétiquement inatteignables. Maintenir la précision à 50 chiffres quelle que soit la longueur du message donne :
| bits | utilisation à 50 chiffres | échantillons complétés | avec précision dérivée |
|---|---|---|---|
| 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, aucune erreur levée | 100.87% |
| 8192 | 4766.49% | 1/2 | 99.07% |
--precision_margin ajuste la marge au-dessus du minimum dérivé ; le minimum lui-même est appliqué.
L'algorithme 3 s'arrête lorsque (L + R) / 2 - d_s tombe dans (-0.5, 0.5]. Ce test peut se déclencher alors que l'intervalle est plus étroit que 1, et la fenêtre [mid - 0.5, mid + 0.5) est alors plus large que l'intervalle qui la porte. La rotation inverse est une isométrie modulo la largeur de l'intervalle, donc une fenêtre à cheval sur la coupure laisse le récepteur à un intervalle complet de d_s.
L'émetteur rejoue donc la rotation inverse du récepteur avant de décider de s'arrêter et émet un token supplémentaire si le rejeu ne restitue pas le secret. Le rejeu est du pur travail Decimal (~0,05 ms contre ~20 ms pour une passe avant), donc le garde-fou ne coûte rien de mesurable. --no_verify le désactive.
Mesuré sur Llama-2-7b :