paint-brush
Stéganographie : comment masquer le texte dans les images à l'aide de JavaScriptpar@andriiromasiun
1,031 lectures
1,031 lectures

Stéganographie : comment masquer le texte dans les images à l'aide de JavaScript

par Andrii Romasiun9m2024/06/26
Read on Terminal Reader

Trop long; Pour lire

La stéganographie est une méthode permettant de cacher des messages secrets dans un autre fichier non secret. Les utilisateurs peuvent télécharger une image pour lire le message caché ou pour coder eux-mêmes un message dans une image. Cet article décrit comment implémenter un tel moteur d'encodage à l'aide de JavaScript.
featured image - Stéganographie : comment masquer le texte dans les images à l'aide de JavaScript
Andrii Romasiun HackerNoon profile picture
0-item

Qu’est-ce que la stéganographie ?

Imaginez que vous souhaitiez envoyer un message secret à un ami, mais que le canal que vous souhaitez utiliser est compromis et surveillé. Vous pourriez utiliser un certain cryptage, mais cela éveillerait les soupçons des personnes qui surveillent vos conversations, vous devrez donc utiliser autre chose.

De nos jours, la stéganographie est une méthode permettant de cacher des messages secrets dans un autre fichier non secret (comme une photo d'un chat), de sorte que si vous envoyez ce fichier, il ne sera pas détecté. La stéganographie ne se limite pas à masquer du texte dans des images et signifie généralement « cacher des informations secrètes dans un autre message non secret ou un objet physique » : vous pouvez masquer certains messages dans des textes audio, vidéo ou autres en utilisant, par exemple, la transposition en colonnes.


La stéganographie peut également être extrêmement utile dans de nombreux autres cas, elle peut par exemple être une bonne alternative au filigrane sur des documents sensibles pour les protéger contre les fuites.


Il existe de nombreuses façons de masquer des informations dans les images, depuis le simple ajout du texte à la fin du fichier jusqu'au masquage dans les métadonnées. Dans cet article, je souhaite aborder une méthode de stéganographie plus avancée, descendant jusqu'au niveau binaire et cachant les messages dans les limites de l'image elle-même.

Construire un moteur stéganographique

L'interface utilisateur

Pour mon exemple de stéganographie, j'ai décidé d'utiliser JavaScript car c'est un langage de programmation puissant qui peut être exécuté dans un navigateur.


J'ai mis au point une interface simple qui permet aux utilisateurs de télécharger une image pour y lire le message caché ou d'encoder eux-mêmes un message dans une image.


 <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Steganography</title> <style> body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; } textarea { width: 300px; height: 100px; } button { margin: 10px; } #outputImage { margin-top: 20px; max-width: 100%; } </style> </head> <body> <h1>Steganography Example</h1> <input type="file" id="upload" accept="image/*"><br> <canvas id="canvas" style="display:none;"></canvas><br> <textarea id="text" placeholder="Enter text to encode"></textarea><br> <button id="encode">Encode Text</button> <button id="decode">Decode Text</button> <p id="decodedText"></p> <img id="outputImage" alt="Output Image"> <script src="./script.js"></script> </body> </html>


Pour l'utiliser, les utilisateurs peuvent simplement sélectionner une image qu'ils souhaitent manipuler et soit essayer d'en décoder du texte, soit l'encoder et télécharger l'image plus tard.



Traitement des images

Pour travailler avec des images en JavaScript, nous pouvons utiliser l' API Canvas . Il fournit de nombreuses fonctions différentes pour manipuler et dessiner des images, des animations ou même des graphiques et des vidéos de jeux.


L'API Canvas est principalement utilisée pour les graphiques 2D. Si vous souhaitez travailler avec des graphiques 3D accélérés par le matériel, vous pouvez utiliser l' API WebGL (qui, d'ailleurs, utilise également l'élément <canvas>).


 const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const image = new Image();


Pour lire le fichier image à partir du système de fichiers et l'ajouter au contexte du canevas, nous pouvons utiliser l' API FileReader . Il nous permet de lire facilement le contenu de n'importe quel fichier stocké sur l'ordinateur de l'utilisateur sans avoir besoin d'une bibliothèque personnalisée.

 function handleFileUpload(event) { const reader = new FileReader(); reader.onload = function (e) { image.src = e.target.result; image.onload = function () { canvas.width = image.width; canvas.height = image.height; ctx.drawImage(image, 0, 0); }; }; reader.readAsDataURL(event.target.files[0]); }


Il lit un fichier et dessine l'image de ce fichier sur notre contexte de canevas 2D précédemment défini, après quoi nous pouvons soit encoder du texte dans cette image, soit essayer de lire le texte de l'image.

Masquage du texte dans les images

Les images sont constituées de pixels et chaque pixel contient des informations sur ses couleurs. Par exemple, si une image est codée à l'aide du modèle RGBA, chaque pixel contiendra 4 octets d'informations sur la quantité de rouge, de vert, de bleu et d'alpha (opacité) qu'il représente.


Pour encoder du texte dans une image, nous pourrions utiliser l'un de ces canaux (par exemple, le canal alpha). Comme cette information est représentée dans le système binaire (comme 01001100), nous pourrions basculer le dernier bit selon nos besoins. C'est ce qu'on appelle le bit le moins significatif (LSB), et sa modification entraîne une modification minime de l'image elle-même, la rendant impossible à distinguer d'un humain.


Maintenant, imaginez que nous ayons un texte comme « Bonjour » et que nous souhaitions l'encoder dans une image. L'algorithme pour faire cela serait


  1. Convertissez le texte "Bonjour" en binaire.


  2. Parcourez les octets de données d'image et remplacez le LSB de ces octets par un peu du texte binaire (chaque pixel contient 4 octets de données pour chacune des couleurs, dans mon exemple, je souhaite changer le canal d'opacité de l'image , donc je répéterais tous les 4 octets).


  3. Ajoutez un octet nul à la fin du message afin que lors du décodage, nous sachions quand s'arrêter.


  4. Appliquez les octets d’image modifiés à l’image elle-même.


Tout d’abord, nous devons prendre le texte que nous voulons encoder auprès de l’utilisateur et y effectuer quelques validations de base.

 const text = document.getElementById("text").value; if (!text) { alert("Please enter some text to encode."); return; }


Ensuite, nous devons convertir le texte en binaire et créer un canevas de l'image dans laquelle nous allons encoder ce texte.

 const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let binaryText = ""; for (let i = 0; i < text.length; i++) { let binaryChar = text.charCodeAt(i).toString(2).padStart(8, "0"); binaryText += binaryChar; }


Pour ce faire, nous pouvons simplement parcourir chacun des caractères et obtenir un index Unicode à l'aide de la fonction charCodeAt . Cet Unicode est ensuite converti en binaire et complété afin qu'il ait la même longueur que n'importe quel autre caractère.


Par exemple, la lettre « H » est représentée par 72 en Unicode ; nous convertissons ensuite ce nombre en binaire (1001000) et ajoutons des 0 au début (01001000) pour nous assurer que toutes les lettres auront la même longueur (8 bits).


Ensuite, nous devons ajouter un octet nul à la fin du message pour nous assurer que lorsque nous le déchiffrons, nous pouvons faire la distinction entre le texte réel et les données de pixels aléatoires de l'image.

 binaryText += "00000000";


Ensuite, nous devons effectuer une validation de base pour nous assurer que l'image contient suffisamment de pixels pour coder notre message afin qu'il ne déborde pas.

 if (binaryText.length > data.length / 4) { alert("Text is too long to encode in this image."); return; }


Et puis vient la partie la plus intéressante, l’encodage du message. Le tableau de données que nous avons défini précédemment contient des informations sur les pixels sous la forme de valeurs RGBA pour chaque pixel de l'image. Ainsi, si l'image est codée en RVBA, chaque pixel qu'elle contient serait représenté par 4 valeurs du tableau de données ; chaque valeur représentant la quantité de rouge, de vert et de bleu que possède ce pixel.

 for (let i = 0; i < binaryText.length; i++) { data[i * 4] = (data[i * 4] & 0b11111110) | parseInt(binaryText[i]); } ctx.putImageData(imgData, 0, 0); const outputImage = document.getElementById("outputImage"); outputImage.src = canvas.toDataURL();


Dans le code ci-dessus, nous parcourons notre texte codé en binaire. data[i * 4] trouve un octet que nous devons modifier, et comme nous voulons uniquement modifier les octets d'un canal particulier, nous multiplions la variable i par 4 pour y accéder.


L'opération data[i * 4] & 0b11111110 définit le bit le moins significatif sur 0 . Par exemple, si data[i * 4] vaut 10101101 en binaire, alors les opérations 10101101 & 11111110 donnent 10101100 . Cela garantit que le LSB est défini sur 0 avant toute autre manipulation avec lui.


Le parseInt(binaryText[i]) est un bit actuel de la chaîne codée en binaire ; c'est soit 1 , soit 0 . Nous pouvons ensuite définir ce bit sur le LSB en utilisant une opération OR au niveau du bit ( | ). Par exemple, si la partie gauche du OU au niveau du bit est 10101100 et que le binaryText[i] est 1 , alors 10101100 | 00000001 donnerait 10101101 . Si le bit actuel était 0 , alors le OU donnerait 10101100 . C'est pourquoi nous avons dû supprimer le LSB en premier lieu.


Une fois le message encodé, nous pouvons le placer dans le canevas actuel et le restituer en HTML à l'aide de la méthode canvas.toDataURL .



Décoder les messages cachés des images

Le processus de décodage d’une image est en réalité beaucoup plus simple que celui de l’encodage. Puisque nous savons déjà que nous n'avons codé que le canal alpha, nous pouvons simplement parcourir tous les 4 octets, lire le dernier bit, le concaténer dans notre chaîne finale et convertir ces données binaires en chaîne Unicode.


Tout d’abord, nous devons initialiser les variables. Puisque imgData est déjà renseigné avec les informations sur l'image (nous appelons ctx.drawImage chaque fois que nous lisons un fichier du système de fichiers), nous pouvons simplement l'extraire dans la variable data.


 const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let binaryText = ""; let decodedText = "";


Ensuite, nous devons parcourir tous les 4 octets de l'image, lire le dernier bit et le concaténer à la variable binaryText .

 for (let i = 0; i < data.length; i += 4) { binaryText += (data[i] & 1).toString(); }


data[i] est l'octet codé, et pour extraire le LSB, nous pouvons utiliser l'opérateur AND ( & ) au niveau du bit. Il prend deux valeurs et effectue une opération ET sur chaque paire de bits correspondants. En comparant data[i] avec 1 , nous isolons essentiellement le bit le moins significatif des informations de pixel, et si le LSB est 1 , alors le résultat d'une telle opération est 1 . Si le LSB est 0 , le résultat serait également 0 .


Une fois que nous avons lu tous les LSB et les avons stockés dans la variable binaryText , nous devons le convertir du binaire en texte brut. Puisque nous savons que chaque caractère est constitué de 8 bits (rappelez-vous comment nous avons utilisé padStart(8, "0") pour donner à chaque caractère la même longueur ?), nous pouvons itérer sur chaque 8ème caractère du binaryText .


Ensuite, nous pouvons utiliser l'opération .slice() pour extraire l'octet actuel du binaryText en fonction de notre itération. La chaîne binaire peut être convertie en nombre à l’aide de la fonction parseInt(byte, 2) . Ensuite, nous pouvons vérifier si le résultat est 0 (un octet nul) - nous arrêtons la conversion et interrogeons le résultat. Sinon, nous pouvons trouver quel caractère correspond au numéro Unicode et l'ajouter à notre chaîne de résultat.


 for (let i = 0; i < binaryText.length; i += 8) { let byte = binaryText.slice(i, i + 8); if (byte.length < 8) break; // Stop if the byte is incomplete let charCode = parseInt(byte, 2); if (charCode === 0) break; // Stop if we hit a null character decodedText += String.fromCharCode(charCode); }


Le texte décodé peut ensuite être affiché en toute sécurité à un utilisateur :

 document.getElementById("decodedText").textContent = decodedText; 



J'ai laissé le code complet utilisé dans cet article dans mon dépôt GitHub ; n'hésitez pas à jouer avec. Il y a beaucoup de choses qui pourraient être améliorées :)

Dernières pensées

La stéganographie est une technique très puissante et peut être appliquée à de nombreux cas d'utilisation différents, à commencer par la vérification des documents, la prévention des fuites, la vérification de l'IA des images, la gestion DRM des fichiers musicaux et bien d'autres encore. Cette technique peut même être appliquée à des vidéos, des jeux ou même du texte brut, je pense donc qu'elle a un énorme potentiel.

À l’ère des NFT et des blockchains, il est encore plus intéressant de voir comment il trouvera ses cas d’usage et comment cette technique évoluera.