
Outil de stéganographie pour images JPG
# jdvrif
***jdvrif*** (*JPG Data Vehicle*) est un outil en ligne de commande de stéganographie rapide et facile à utiliser, permettant de dissimuler et d'extraire tout type de fichier via une image **JPG**. ***Linux uniquement***.
Votre fichier de données est compressé avec ***libdeflate/zlib***, puis chiffré avec ***XChaCha20-Poly1305*** (secretstream de ***libsodium***) sous une clé dérivée par ***Argon2id*** à partir d'un ***PIN de récupération*** généré aléatoirement, et enfin intégré dans l'image de couverture. Le PIN est affiché une seule fois, à la fin de ***conceal***, et n'est jamais stocké nulle part : sans lui, le fichier dissimulé ne peut pas être récupéré.
En utilisant le ***mode de dissimulation par défaut***, vous pouvez dissimuler tout type de fichier, l'image finale étant plafonnée à ***2GiB***. Les autres modes de dissimulation par plateforme et les sites de médias sociaux compatibles (*listés ci-dessous*) ont leurs propres limites de taille ***bien plus petites*** et d'autres exigences.
Il existe une [***édition Web***](https://cleasbycode.co.uk/jdvrif/app/) de ***jdvrif***, que vous pouvez utiliser immédiatement, comme alternative pratique au téléchargement et à la compilation du code source de la CLI. Les téléversements de fichiers via le Web sont limités à **20MiB**.
Un port expérimental en ***Rust*** [***jdvrif-rs***](https://github.com/CleasbyCode/jdvrif-rs) est également disponible pour ceux qui s'intéressent à ce langage. Il est compatible au niveau du format : l'une ou l'autre version peut récupérer les images de l'autre.
***jdvrif*** dérive en partie de la ***[technique mise en œuvre](https://www.vice.com/en/article/bj4wxm/tiny-picture-twitter-complete-works-of-shakespeare-steganography)*** par le chercheur en sécurité ***[David Buchanan](https://www.da.vidbuchanan.co.uk/).***

*Image de démonstration : **"A place of concealment"** / ***PIN : 7912872750612437051****
## Comment jdvrif dissimule les données
Contrairement à la méthode de stéganographie [***LSB***](https://ctf101.org/forensics/what-is-stegonagraphy/) (*Least Significant Bit*, bit de poids faible) courante, qui dissimule les données dans les pixels d'une image de couverture, ***jdvrif*** cache principalement les données dans les ***segments applicatifs*** d'une image ***JPG*** (ICC, EXIF, XMP, etc.).
| Mode de dissimulation | Où vont les données | Où partager l'image de sortie |
| --- | --- | --- |
| *(aucune option)* | Segments APP2/profil ICC | X-Twitter, Tumblr, Mastodon, Pixelfed, PostImage, ImgBB, ImgPile, Flickr |
| ***-b*** | Segments EXIF / Photoshop / XMP encadrés | ***Bluesky*** uniquement |
| ***-r*** | Coefficients DCT JPEG (***QIM***) | ***Reddit*** uniquement |
| ***-x*** | Coefficients DCT JPEG (***J-UNIWARD/STC***) | ***X-Twitter*** uniquement |
Les deux exceptions de plateforme à la méthode de stockage par segments par défaut sont ***Reddit*** et ***X-Twitter***. Toutes deux disposent de leur propre mode de dissimulation, et aucun de ces modes n'utilise de segments de métadonnées : ***-r*** et ***-x*** transportent la charge utile dans les coefficients DCT de l'image ***JPG*** à la place.
***Reddit*** réencode les images téléversées et supprime les segments de métadonnées sur lesquels repose le mode par défaut, de sorte que ***-r*** est le seul mode qui y fonctionne. ***X-Twitter*** préserve bien un unique petit segment ICC, donc le mode par défaut fonctionne encore sur cette plateforme, mais uniquement pour une charge utile minuscule.
Pour le mode de dissimulation ***Reddit*** (***-r***), nous utilisons la [***méthode de stéganographie QIM***](https://www.google.com/search?q=QIM+steganography+method&sourceid=chrome&ie=UTF-8&source=chrome.ctxt) (*Quantization Index Modulation dans le domaine DCT JPEG*), car c'est la seule méthode de stockage qui fonctionne actuellement pour ***Reddit***. La couverture est transcodée en baseline Q75 4:2:0 et la charge utile est transportée dans ses blocs DCT de luminance.
Pour maximiser la capacité de stockage pour la plateforme ***Reddit***, utilisez une image de couverture de grandes dimensions, **2048x2048**, **4096x4096**, **8192x8192 (max)**, etc.
La qualité de l'image de couverture n'est pas importante pour cette méthode et doit rester basique pour les plus grandes dimensions afin d'aider à minimiser la taille du fichier de l'image de couverture.
Bien que la plateforme ***X-Twitter*** puisse utiliser la méthode par défaut fournie par ***jdvrif***, où les données sont dissimulées dans des segments APP2/ICC, ***X-Twitter*** limite cela à un seul segment ICC d'une taille maximale d'à peine **~10KiB**.
Pour transporter plus que ces **~10KiB**, utilisez le mode de dissimulation de la plateforme ***X-Twitter*** (***-x***). Il abandonne entièrement les segments de métadonnées — rien n'est écrit dans un profil ICC — et utilise à la place la [***méthode de stéganographie adaptative J-UNIWARD avec Syndrome-Trellis Coding (STC)***](https://www.google.com/search?q=adaptive+J-UNIWARD+steganography+method+with+Syndrome-Trellis+Coding+(STC)). La couverture est transcodée en progressif 4:2:0 à sa qualité dérivée de la source (plafonnée à **Q97**).
Pour maximiser la capacité de stockage pour la plateforme ***X-Twitter***, utilisez une image de couverture de haute qualité/détaillée avec de grandes dimensions, **1024x1024**, **2048x2048**, **4096x4096 (max)**, etc.
Les porteurs ***-r*** et ***-x*** chiffrent vos données et lient leur emplacement à votre PIN de récupération à l'aide d'***Argon2id***, mais l'intégration modifie tout de même les statistiques DCT de l'image, de sorte qu'une stéganalyse dédiée peut révéler qu'une image contient des données cachées.
Les images ***-r*** et ***-x*** créées avec ***jdvrif v9.0*** restent récupérables, mais ***v9.0*** ne peut pas récupérer les images ***-r*** ou ***-x*** créées avec ***v9.1***.
Les deux modes DCT transportent bien moins de données que le mode par défaut, utilisez donc ***capsize*** pour mesurer une image de couverture avant de choisir une charge utile (voir [Vérification de la capacité](#checking-capacity-with-capsize)).
## Prérequis et compilation (Linux)
La compilation nécessite **CMake 3.20 ou plus récent**, `flock` de **util-linux**, et soit **Ninja** (préféré) soit **Make**. Le compilateur doit être **GCC 14 ou plus récent**, ou **Clang 18 ou plus récent** associé à une bibliothèque standard C++23 qui implémente des fonctionnalités telles que `std::format` et `std::print`. Les bibliothèques natives requises sont **libsodium**, **libjpeg-turbo** (*à la fois les API `turbojpeg` et `libjpeg`*), **zlib** et **libdeflate**. **OpenMP** est facultatif : lorsqu'il est présent, il parallélise la passe de coût J-UNIWARD de ***-x***.
```console
$ sudo apt update
$ sudo apt install g++ cmake ninja-build util-linux libsodium-dev libturbojpeg0-dev libjpeg-dev zlib1g-dev libdeflate-dev
$ g++ --version # confirm the reported version is 14 or newer
$ chmod +x compile_jdvrif.sh
$ ./compile_jdvrif.sh
$ sudo cp jdvrif /usr/bin
```
Si votre distribution fournit GCC 14 sous forme de paquet versionné, installez `g++-14` et compilez avec `CXX=g++-14 ./compile_jdvrif.sh`.
Le wrapper conserve les fichiers objets suivis par les dépendances sous `src/build/`, de sorte que les invocations ultérieures ne reconstruisent que ce qui a changé, et il ne remplace le binaire `jdvrif` publié qu'après une compilation complète et réussie. Définissez `JDVRIF_JOBS=<count>` pour modifier la limite de parallélisme (par défaut : nombre de CPU, plafonné à 8), `JDVRIF_BUILD_DIR=<path>` pour utiliser un cache de compilation séparé, ou `BUILD_MODE=sanitize` pour une compilation ASan/UBSan.
## Utilisation
```console
$ jdvrif
Usage: jdvrif conceal [-b|-r|-x] <cover_image> <secret_file>
jdvrif recover <cover_image>
jdvrif capsize [-r|-x] <cover_image>
jdvrif --info
```
Exécutez `jdvrif --info` pour obtenir le guide intégré complet des modes, des options de plateforme et des limites de taille.
```console
$ jdvrif conceal your_cover_image.jpg your_secret_file.doc
Platform compatibility for output image:-
✓ X-Twitter
✓ Tumblr
✓ Mastodon
✓ Pixelfed
✓ PostImage
✓ ImgBB
✓ ImgPile
✓ Flickr
Recovery PIN: [***2166776980318349924***]
Important: Keep your PIN safe, so that you can extract the hidden file.
Saved "file-embedded" JPG image: jrif_4e87c566c.jpg (143029 bytes).
Complete!
$ jdvrif recover jrif_4e87c566c.jpg
PIN: *******************
Extracted hidden file: your_secret_file.doc (6165 bytes).
Complete! Please check your file.
```
jdvrif ***mode*** arguments :
***conceal*** - Compresse, chiffre et intègre votre fichier de données secret dans une image de couverture ***JPG***.
***recover*** - Déchiffre, décompresse et extrait le fichier de données dissimulé d'une image de couverture ***JPG*** (*code PIN de récupération requis*).
***capsize*** - Indique la capacité de portage d'une image de couverture pour le mode ***-r*** ou ***-x***. Aucune image n'est enregistrée.
Exigences pour l'image de couverture :
● JPEG uniquement, au moins **400x400** pixels, et en niveaux de gris ou en couleur YCbCr — les images CMJN/YCCK doivent d'abord être converties en RVB.
● Les dimensions maximales par défaut et en mode ***-b*** sont de **4096x4096px** ; le mode ***-r*** utilise **8192x8192px**, tandis que le mode ***-x*** est également plafonné à **4096x4096px**.
● Le mode par défaut (*sans option*) rejette également les images de couverture dont la qualité JPEG estimée est supérieure à **Q97** ; réenregistrez l'image à une qualité inférieure si elle est refusée.
Exigences pour le fichier de données secret :
● Le nom de fichier intégré ne doit pas dépasser **20 octets**, ne doit pas commencer par `.` ou `-` ni se terminer par un espace ou `.`, et doit être un UTF-8 valide sans caractères de contrôle ni de formatage bidirectionnel, ni aucun des caractères `/ \ : * ? " < > |`. Le nom est stocké dans l'image et restauré lors de la ***recover***.
● Votre fichier de données est compressé avant le chiffrement, sauf pour les types de fichiers déjà compressés reconnus (`.zip`, `.7z`, `.mp4`, `.jpg`, `.png`, etc.). Dans les modes par défaut et ***-b***, ces types ignorent la compression uniquement lorsqu'ils dépassent **10MiB** ; dans les modes ***-r*** et ***-x***, ils l'ignorent toujours. Pour tout autre fichier destiné à une petite limite de plateforme, envisagez de le compresser vous-même au préalable (*zip, rar, 7z, etc.*) afin de connaître sa taille exacte stockée.
## Plateformes compatibles
\******************
Remarque : ***Bluesky*** enregistre désormais les images en ***WEBP*** par défaut.
Pour enregistrer une image en ***JPG***, afin de pouvoir toujours récupérer les données dissimulées avec ***jdvrif*** :
Cliquez d'abord sur l'image dans la publication pour l'ouvrir, puis faites un clic droit sur l'image. Dans le menu, sélectionnez ***Ouvrir l'image dans un nouvel onglet***.
Sélectionnez le nouvel onglet et, dans la barre d'adresse, placez-vous à la fin de l'adresse et ajoutez ***@jpg*** puis appuyez sur Entrée.
Faites un clic droit sur l'image et, dans le menu, sélectionnez ***Enregistrer l'image...***
Votre image devrait maintenant être téléchargée en ***JPG***, ce qui fonctionnera désormais avec ***jdvrif***.
Si vous souhaitez un outil pour dissimuler des données à l'aide d'images ***WEBP*** à publier sur ***Bluesky***, vous pouvez utiliser mon outil CLI de stéganographie ***WEBP*** ***[wbpdv](https://github.com/CleasbyCode/wbpdv)***
\******************
*Limite de taille de publication mesurée par la taille ***combinée*** de l'***image de couverture*** + du ***fichier de données compressé :****
● ***Flickr*** (**200MiB**), ***ImgPile*** (**100MiB**), ***ImgBB*** (**32MiB**), ***PostImage*** (**32MiB**), ***Pixelfed*** (**15MiB**).
*Limite de taille mesurée ***uniquement*** par la ***taille du fichier de données compressé :****
● ***Mastodon*** (**~6MiB**), ***Tumblr*** (**~64KiB**), ***X-Twitter*** (**~10KiB / méthode par défaut**).
Par exemple, avec ***Mastodon***, si votre image de couverture fait **1MiB**, vous pouvez toujours intégrer un fichier de données jusqu'à la limite de taille de **~6MiB**.
**Autre : plateformes disposant de leur propre mode de dissimulation :**
● ***Bluesky*** (***option -b***). Le ***JPG*** final « *à fichier intégré* » ne doit pas dépasser **2 000 000 octets (~1,9MiB)**, de sorte que l'image de couverture et le fichier de données compressé partagent un même budget. Le fichier de données compressé seul ne doit pas dépasser **~171KiB**. Une image de couverture déjà à 2 000 000 octets ne laisse aucune place, donc gardez l'image de couverture plus petite que la limite d'au moins la taille de votre fichier de données compressé. Le script « ***create_bsky_post.py*** » est requis pour publier ces images sur ***Bluesky***. *Plus d'informations sur ce script plus bas dans la page.*
● ***Reddit*** (***option -r***). L'image de couverture et le fichier de données ne doivent chacun pas dépasser **20MiB**, mais la capacité de portage réelle de l'image de couverture est ***bien plus petite*** et dépend de ses dimensions. Utilisez `jdvrif capsize -r` pour la mesurer.
● ***X-Twitter*** (***option -x***). L'image de couverture et le fichier de données ne doivent pas dépasser **5MiB** chacun, et la couverture ne doit pas dépasser **4096x4096** pixels. La capacité réelle du support est ***bien inférieure*** et dépend de la qualité et des dimensions de l'image. Utilisez `jdvrif capsize -x` pour la mesurer.
Dans les modes par défaut et ***-b***, l'image de couverture est également optimisée sans perte avant utilisation et ne doit pas dépasser **4MiB** après cette étape.
Pour des plateformes telles que ***X-Twitter***, ***Reddit*** et ***Tumblr***, qui imposent des limites de taille de données réduites, vous pouvez privilégier des données qui se compressent bien, comme les fichiers texte, etc.
https://github.com/user-attachments/assets/af330d09-f65d-4f75-8290-883078be34b3
https://github.com/user-attachments/assets/fc454d42-0240-4864-b44b-ce5ef7cfd94c
## Vérification de la capacité avec capsize
***capsize*** prépare l'image de couverture exactement comme le ferait ***conceal***, puis indique la quantité de charge utile chiffrée qu'elle peut transporter. Rien n'est écrit sur le disque. Utilisez ***-r*** pour le support ***Reddit*** et ***-x*** pour le support ***X-Twitter*** (***-x*** est l'option par défaut si aucune option n'est fournie).
```console
$ jdvrif capsize -r basic_img_large_dims.jpg
Reddit capacity check for conceal -r mode only.
Cover Image: 384KiB, 8192x8192, Baseline YCbCr 4:2:0, Standard Q75 quantization (C3).
Theoretical C3 capacity limit for this cover image: 436906 bytes (~426KiB).
Conservative maximum compressed capacity with a 20-character filename: 436792 bytes (~426KiB).
Recommended maximum compressed capacity with a 20-character filename: 435768 bytes (~425KiB).
```
La taille indiquée correspond à la capacité totale de l'***enveloppe*** chiffrée, et non à une limite brute de fichier secret : le nom de fichier, les métadonnées de chiffrement et de récupération consomment 95 à 114 octets pour une charge utile à trame unique, et les charges utiles plus volumineuses ajoutent une surcharge d'encapsulation. Ne visez pas la limite théorique — lorsque la capacité le permet, maintenez la charge utile compressée au moins **1 Kio** en dessous du maximum conservateur. La vérification de taille effectuée par ***conceal*** est celle qui fait foi.
## Options de plateforme en mode conceal
Pour créer des images ***JPG*** "*data-concealed*" compatibles destinées à être publiées sur la plateforme ***Reddit***, vous devez utiliser l'option ***-r*** avec le mode ***conceal***.
```console
$ jdvrif conceal -r my_image.jpg hidden.doc
```
Ces images ne sont compatibles que pour une publication sur ***Reddit***. Votre fichier de données intégré sera perdu si vous le publiez sur une autre plateforme.
Lorsque vous enregistrez/téléchargez une image depuis ***Reddit***, assurez-vous de cliquer sur l'image dans la publication pour la développer entièrement avant de l'enregistrer.
https://github.com/user-attachments/assets/ead3c34c-dd9a-4018-9ea3-0746fbdfdb8b
Pour créer des images ***JPG*** compatibles « *data-concealed* » à publier sur la plateforme ***X-Twitter*** en utilisant la méthode de stéganographie J-UNIWARD, vous devez utiliser l'option ***-x*** avec le mode ***conceal***.
```console
$ jdvrif conceal -x my_image.jpg hidden.doc
```
Ces images ne sont compatibles qu'avec une publication sur ***X-Twitter***. Votre fichier de données intégré sera perdu si vous le publiez sur une autre plateforme.
Lorsque vous enregistrez/téléchargez une image depuis ***X-Twitter***, assurez-vous de cliquer sur l'image dans la publication pour la développer entièrement avant de l'enregistrer.
Pour créer des images ***JPG*** compatibles avec les "*données intégrées au fichier*" pour une publication sur la plateforme ***Bluesky***, vous devez utiliser l'option ***-b*** avec le mode ***conceal***.
```console
$ jdvrif conceal -b my_image.jpg hidden.doc
```
Ces images ne sont compatibles qu'avec une publication sur ***Bluesky***. Votre fichier de données intégré sera supprimé s'il est publié sur une autre plateforme.
Vous devez également utiliser le script Python [create_bsky_post.py](https://github.com/CleasbyCode/jdvrif/blob/main/src/bsky/create_bsky_post.py) (situé dans le dossier ***src/bsky*** du dépôt) pour publier l'image sur ***Bluesky***.
Cela ne fonctionnera pas si vous publiez des images sur ***Bluesky*** via le site web ou l'application mobile.
Pour utiliser le script, vous devrez créer un [***mot de passe d'application***](https://bsky.app/settings/app-passwords) depuis votre compte ***Bluesky***. Transmettez vos identifiants via l'environnement plutôt que sur la ligne de commande, où ils seraient visibles par les autres utilisateurs locaux via des outils tels que `ps` :
```console
$ pip install -r bsky/requirements.txt
$ export ATP_AUTH_HANDLE='you.bsky.social'
$ read -rsp 'Bluesky app password: ' ATP_AUTH_PASSWORD && export ATP_AUTH_PASSWORD
$ python3 bsky/create_bsky_post.py \
--image jrif_4e87c566c.jpg \
--alt-text "alt-text here [optional]" \
"standard post text here [required]"
$ unset ATP_AUTH_PASSWORD
```
Consultez [src/bsky/README.md](https://github.com/CleasbyCode/jdvrif/blob/main/src/bsky/README.md) pour l'ensemble complet des options (*images multiples, réponses, citations de publications, cartes de liens*) et pour savoir contre quoi la version durcie du script protège.
https://github.com/user-attachments/assets/1daef508-d304-491f-bfe2-2cdbb5d62081
## Tests
Les scripts de test dans `src/tests` prennent chacun le binaire à exercer :
```console
$ bash tests/run_golden_tests.sh --bin ./jdvrif # recover pre-built images with known PINs
$ bash tests/run_roundtrip_tests.sh --bin ./jdvrif # fresh conceal/recover round-trips
$ bash tests/run_security_smoke.sh --bin ./jdvrif # negative-path and boundary checks
$ bash tests/run_reddit_tests.sh --bin ./jdvrif # -r QIM carrier
$ bash tests/run_twitter_tests.sh --bin ./jdvrif # -x J-UNIWARD/STC carrier
$ bash tests/run_io_security_tests.sh --bin ./jdvrif # input-size bounds, PIN signal/job-control handling
$ bash tests/run_jpeg_safety_tests.sh # JPEG parser/decoder safety (builds its own test binary)
$ bash tests/run_bsky_tests.sh # Bluesky posting helper
```
## Logiciels et ressources tiers
### Applications principales
- [libsodium](https://github.com/jedisct1/libsodium) — génération aléatoire cryptographique, dérivation de clé Argon2id et flux secrets XChaCha20-Poly1305. Lié dynamiquement en tant que bibliothèque système.
Licence : [Licence ISC](https://github.com/jedisct1/libsodium/blob/master/LICENSE)
Copyright (c) 2013–2026 Frank Denis.
- [libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo) — traitement JPEG et transformation sans perte. Lié dynamiquement en tant que bibliothèque système.
Ce logiciel est en partie basé sur le travail de l'Independent JPEG Group.
Licences : [Licence Independent JPEG Group, Licence BSD 3-Clause modifiée,
et Licence zlib](https://github.com/libjpeg-turbo/libjpeg-turbo/blob/2.1.5/LICENSE.md).
Copyright © 1991–2020 Thomas G. Lane et Guido Vollbeding.
Copyright © 2009–2023 D. R. Commander. Tous droits réservés.
Copyright © 2015 Viktor Szathmáry. Tous droits réservés.
- [zlib](https://github.com/madler/zlib) — Compression et décompression zlib en flux. Lié dynamiquement en tant que bibliothèque système.
Licence : [Licence zlib](https://github.com/madler/zlib/blob/develop/LICENSE)
Copyright (C) 1995–2026 Jean-loup Gailly et Mark Adler.
- [libdeflate](https://github.com/ebiggers/libdeflate) — Compression rapide au format zlib sur tampon complet. Lié dynamiquement en tant que bibliothèque système.
Licence : [MIT](https://github.com/ebiggers/libdeflate/blob/master/COPYING)
Copyright 2016 Eric Biggers.
Copyright 2024 Google LLC.
### Code et ressources intégrés
- [Profils ICC compacts](https://github.com/saucecontrol/Compact-ICC-Profiles) — profil ICC compatible Adobe intégré.
Licence : [CC0 1.0 Universal](https://github.com/saucecontrol/Compact-ICC-Profiles/blob/master/license)
### Assistant facultatif de publication Bluesky
- Bryan Newbold / ATProto Hacker Cookbook — create_bsky_post.py — Base de l'assistant de publication Bluesky [forké](https://gist.github.com/CleasbyCode/1eb678ca1fa1975b1c1e20aeec33637e) (src/bsky/create_bsky_post.py).
Pour référence, voir la [copie du Cookbook](https://github.com/bluesky-social/cookbook/blob/main/python-bsky-post/create_bsky_post.py)
Licence : [CC0 1.0 Universal](https://github.com/bluesky-social/cookbook/blob/main/LICENSE-CC0).
- Requests — Requêtes HTTP et API Bluesky.
Licence : [Apache 2.0](https://github.com/psf/requests/blob/main/LICENSE) / [NOTICE](https://github.com/psf/requests/blob/main/NOTICE)
Copyright 2019 Kenneth Reitz.
- Beautiful Soup 4 — analyse des métadonnées HTML et Open Graph.
Licence : [MIT](https://pypi.org/project/beautifulsoup4/)
Copyright (c) Leonard Richardson.
- Pillow — Validation d'images, dimensions et gestion du rapport d'aspect.
Licence : [MIT-CMU](https://github.com/python-pillow/Pillow/blob/main/LICENSE)
Copyright PIL © 1997–2011 Secret Labs AB et © 1995–2011 Fredrik Lundh et contributeurs.
Copyright Pillow © 2010 Jeffrey « Alex » Clark et contributeurs.
##