l`énoncé complet
Transcription
l`énoncé complet
CS-107 : Mini-projet 1 Stéganographie B. Goullet, B. Jobstmann, J. Sam Table des matières 1 Présentation 3 2 Structure et code fournis 4 3 Encodage direct 6 3.1 3.2 Transformation en image binaire . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 3.1.1 Codage d’un pixel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 3.1.2 Codage de l’image complète . . . . . . . . . . . . . . . . . . . . . . . . . . 8 3.1.3 Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Dissimulation de l’image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 3.2.1 Incrustation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 3.2.2 Dévoilement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 3.2.3 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 3.2.4 Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 4 Encodage texte 4.1 4.2 4.3 11 Conversion String vers tableau de bits, et vice versa . . . . . . . . . . . . . . . . 11 4.1.1 Entier vers tableau de bits . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 4.1.2 String vers tableau de booléens, via tableau d’entiers . . . . . . . . . . . 12 4.1.3 Tableau de bits vers tableau d’entiers . . . . . . . . . . . . . . . . . . . . 12 4.1.4 Tableau d’entiers vers String . . . . . . . . . . . . . . . . . . . . . . . . . 12 Dissimulation et dévoilement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 4.2.1 Du tableau de bits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 4.2.2 De la String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 1 5 Encodage en spirale 5.1 5.2 5.3 15 Dissimulation en spirale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 5.1.1 Transformation de l’image binaire en tableau unidimensionnel . . . . . . . 16 5.1.2 Encodage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Dévoilement d’un message encodé en spirale . . . . . . . . . . . . . . . . . . . . . 17 5.2.1 Transformation du tableau unidimensionnel en image binaire . . . . . . . 17 5.2.2 Dévoilement de l’image . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 6 Complément théorique – Couleurs, pixels et binaire 19 6.1 Représentation binaire des entiers . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 6.2 Références . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2 1 Présentation L’objectif de ce premier mini-projet est de découvrir la stéganographie, et apprendre à manipuler des images et leurs pixels. La stéganographie[Lien] est un art de la dissimulation : il s’agit de cacher une information, par exemple une image ou un texte, dans un support comme une autre image ou un texte. Le but est que l’on ne puisse pas voir que le support cache le message. Contrairement à la cryptographie, qui rend l’information incompréhensible mais toujours apparente1 , la stéganographie vise à ce que l’information elle-même ne soit même pas détectée. Elle est par exemple utilisée dans des situations où les messages que l’on transmet sont surveillés, comme sous une dictature, ou par des groupes terroristes2 . Plus légalement, elle est utlisée pour le tatouage numérique (ou digital watermarking)[Lien] qui permet de marquer de façon invisible (ou visible) un fichier, afin de pouvoir retracer sa provenance ; par exemple en cas de fuite de ce fichier. Dans ce mini-projet, nous utiliserons une des méthodes les plus simples pour cacher de l’information dans une image : le Least Significant Bit Encoding (Encodage sur le Bit de Poids Faible), ou LSB, qui consiste à séparer l’information que l’on veut cacher en bits individuels, et de les stocker dans le bit le moins important (et donc le moins visible) de chaque pixel du support. (a) Image contenant la figure 1b cachée dans les deux derniers bits de chaque pixel (b) Image extraite de la figure 1a Fig. 1 : Exemple d’une image cachée dans une autre via la stéganographie (ici, les deux bits de poids faible) (images tirées de https ://fr.wikipedia.org/wiki/Stéganographie) Dans ce projet nous nous intéressons à cacher des images et du texte dans une image. 1 2 On sait que l’information existe et que l’on veut nous empêcher de la lire Ce qui intéresse d’ailleurs les sciences forensiques (voir les références en fin de document) 3 2 Structure et code fournis Le projet est divisé en 3 étapes : 1. Encodage direct – cacher (encoder) et révéler une image en noir et blanc. Il s’agit de : (a) convertir une image fournie vers une image en noir et blanc ; (b) encoder une image noir et blanc fournie, le message ou la charge, dans une autre image, le support ; (c) extraire les bits de poids faible d’un support pour révéler le message caché. 2. Encodage texte – encoder et révéler du texte. Il s’agit de : (a) préparer un message texte (c’est-à-dire, convertir une String vers un tableau de bits qui représente l’encodage UTF-16[Lien] de chaque caractère de la String) ; (b) encoder le tableau de bits dans une image support ; (c) extraire les bits de poids faible et les convertir vers une String. 3. Encodage en Spirale – encoder et révéler un texte en utilisant un encodage en spirale. Il y a trois fichiers à compléter : • ImageMessage.java pour la partie 1.a) ; • TextMessage.java pour la partie 2.a) ; • Steganography.java pour les parties 1.b) et 1.c), 2.b) et 2.c), ainsi que la partie 3. Les entêtes des méthodes à implémenter sont fournies et ne doivent pas être modifiées. Le fichier fourni SignatureChecks.java donne l’ensemble des signatures à ne pas changer. Il sert notamment d’outil de contrôle lors des soumissions. Il permettra de faire appel à toutes les méthodes requises sans en tester le fonctionnement3 . Vérifiez que ce programme compile bien avant de soumettre. La vérification du comportement correct de votre programme vous incombe. Néanmoins, pour vous aider dans vos tests, nous fournissons quelques tests automatiques dans le répertoire (package) test que vous pourrez utiliser pour tester votre programme sur les exemples fournis. L’utilisation de ces tests vous sera expliquée en temps voulu. Ces tests ne sont bien sûr pas exhaustifs et vous êtes encouragés à tester par vos propre moyens4 Lors de la correction de votre mini-projet, nous utiliserons des tests automatisés, qui passeront des entrées générées aléatoirement aux différentes fonctions de votre programme. Il y aura aussi des tests vérifiant comment sont gérés les cas particuliers. Ainsi, il est important que votre programme traite correctement n’importe quel input valide. En dehors des méthodes imposées, libre à vous de définir toute méthode supplémentaire qui vous semble pertinente. 3 4 Cela permet de vérifier que vos signatures sont correctes et que votre projet ne sera pas rejeté à la soumission. Dans des méthodes main sans test unitaire (même si ce n’est pas interdit d’en faire). 4 Modularisez et tentez de produire un code propre ! Différents programmes avec une méthode main sont fournis : MainBasics.java, MainImages.java, MainStrings.java et MainSpiral.java. Ils ne seront pas testés en tant que tels. Modifiez les comme bon vous semble, pour répondre à vos besoins (en particulier, pour tester). Vous pouvez aussi créer d’autres programme avec des méthodes main (ou complétez la coquille Main.java). La manipulation de fichiers et de fenêtres étant fastidieuse et trop avancée pour ce cours, une partie du code vous est donnée. En effet, le fichier Helper.java vous simplifie l’import, l’export et l’affichage d’images en Java : • Les fonctions read(String path) et write(String path, int[][] array) permettent de lire et écrire des images dans divers formats (JPG, PNG, BMP...). • La méthode show(String title, int[][] array) affiche une image à l’écran, et attend que l’utilisateur ferme la fenêtre pour continuer l’exécution. Pour simplifier, vous considérerez que toutes les données fournies en paramètres des méthodes que nous vous demandons d’implémenter, soient correctes par défaut. Il n’y a pas à vérifier par exemple que les tableaux soient non nuls ou correctement constitués. Vous pouvez tout de même utiliser les assertions Java, qu’il faut activer en lançant le programme avec l’option ”-ea” [Lien cliquable ici], pour vérifier les paramètres passés à vos fonctions. Une assertion s’écrit de la forme assert expr ; avec expr une expression booléenne. Si l’expression est fausse, alors le programme lance une erreur et s’arrête, sinon il continue normalement. Par exemple, pour vérifier que cover n’est pas null, vous pouvez écrire assert cover != null ; au début de la fonction. Un exemple d’utilisation d’assertion est donné dans la coquille de la méthode embedSpiralBitArray dans le fichier Steganography.java. Des méthodes utilitaires sont fournis dans Util.java que vous pouvez employer dans vos assertions. Il vous incombe donc de bien vérifier à chaque étape que vous produisez bel et bien des données correctes avant de passer à l’étape suivante qui va utiliser ces données. Les données que nous fournissons de notre côté (images/tableaux) sont correctes. Un certains nombre d’images sont fournies pour tester les différentes parties de votre projet. Vous pouvez tester avec vos propres images, mais le format supporté est uniquement le .png. Enfin, notez que les descriptions des étapes sont volontairement courtes et concises. La mise en oeuvre du mini-projet implique de connaître certains concepts de base : ce qu’est un pixel, une couleur ou une représentation binaire. Une partie de ces éléments vous aura été expliquée en cours. Certaines notions sont également décrites dans les compléments en fin de document. 5 Commencez par lire ces compléments dans les grandes lignes pour appréhender les points importants impliqués. 3 Encodage direct Dans cette première partie, vous cacherez une image binaire, la charge, (composée uniquement de pixels noirs et blancs) dans une autre image, le support (qui peut être en couleur), en changeant le dernier bit de chaque pixel du support vers la valeur du pixel correspondant de la charge. Pour cela, vous convertirez d’abord une image couleur en une image binaire (charge), en passant par une version en nuances de gris ; puis vous itèrerez sur chaque pixel d’une autre image (support) pour y modifier le dernier bit, de façon à ce que la valeur du pixel de la charge soit stockée dans ce bit. Vous implémenterez aussi le dévoilement d’une image cachée ainsi. De même, vous itèrerez sur chaque pixel du support et lirez la valeur du dernier bit pour reformer la charge en tant qu’image binaire. Ces points sont décrits plus en détail ci-dessous. 3.1 Transformation en image binaire Dans le cadre de ce projet, nous avons choisi de représenter une image comme étant un tableau à deux dimensions de couleurs RGB. La première dimension représente la ligne, la seconde la colonne (convention utilisée en algèbre linéaire, pour les matrices). Exemple de code qui lit une image depuis le disque et la transforme en une image binaire. // On charge une image depuis le disque int [][] colors = Helper .read("mickey - mouse.jpg") ; // Affiche la valeur du pixel en x=50 , y=100 // l'entier affiché est un code de couleur // dans la representation RGB System .out. println ( colors [100][50]) ; // Convertit l'image en niveaux de gris int [][] gray = ImageMessage . toGray ( colors ) ; // Affiche le niveau de gris en x=50 , y=100 System .out. println (gray [100][50]) ; // Convertit l'image en noir et blanc boolean [][] bw = ImageMessage .toBW(gray , 128) ; // Affiche la valeur en x=50 , y=100 // (" true" si blanc , "false " si noir) System .out. println (bw [100][50]) ; 6 Votre première tâche est donc de compléter le fichier ImageMessage.java pour passer d’une représentation RGB à celle en niveaux de gris, et vice-versa ; et du niveau de gris vers une image binaire. À noter qu’il y a perte d’information lors de la conversion en gris ou en noir et blanc, rendant ces opérations irréversibles. Cette partie sera réalisée au moyen des opérateurs sur les bits. Vous ne devrez pas avoir recours à des classes non vues dans le cadre du cours. Pour toutes les parties suivantes, vous êtes libre d’utiliser tout outillage de votre choix vous semblant approprié. 3.1.1 Codage d’un pixel Comme expliqué dans le complément théorique (chapitre 6), un pixel n’est autre qu’une couleur en représentation RGB. Commencez par implémenter les fonctions getRed(int RGB), getGreen(int RGB) et getBlue( int RGB) qui retournent séparément les différentes composantes (rouge, verte et bleue respectivement) d’une couleur. Cette couleur est donnée sous la forme d’un entier, et le résultat est retourné en tant qu’entier compris entre 0 et 255. Ces fonctions vous permettront d’implémenter ensuite getGray(int RGB) qui calcule et retourne la moyenne (en division entière) des composantes d’une couleur, toujours entre 0 et 255. Puis vous pourrez implémenter getBW(int gray, int threshold) qui transforme un niveau de gris en blanc ou noir, selon que ce premier est supérieur ou non au seuil fourni. Cette fonction retournera true si gray est supérieur ou égal à threshold, et, par conséquent, false s’il y est inférieur strictement. L’opération inverse doit être effectuée par différentes surcharges de la méthode getRGB : • getRGB(int red, int green, int blue) encode dans un entier les trois composantes de couleur (voir l’exemple donné dans la dernière page des compléments du chapitre 6). • getRGB(int gray) fait de même à partir d’un niveau de gris ; les trois composantes seront alors identiques et vaudront gray. • getRGB(boolean bw) encode un entier noir si le booléen passé en paramètre vaut false, ou blanc sinon. Les signatures de méthodes sont fournies. Attention Votre code doit gérer les valeurs qui ne sont pas entre 0 et 255 : un int reçu en paramètre qui ne serait pas entre 0 et 255 doit être limité à ces valeurs (ramené à 0 s’il est plus petit que 0 et ramené à 255 s’il est plus grand que 255). Lisez les commentaires descriptifs des méthodes tels que fournis pour savoir quand procéder à ces vérifications/traitements. int color = 0 b11110000_00001111_01010101 ; // -> 15732565 (0 xF00F55 ) // On extrait les differentes valeurs 7 getRed ( color ) ; getGreen ( color ) ; getBlue ( color ) ; int gray = getGray ( color ) ; getBW (gray , 128) ; // // // // // -> -> -> -> -> 240 (0 xF0) 15 (0 x0F) 85 (0 x55) 113 false // On encode des couleurs getRGB (0, 0, 255) ; // -> 255 (0 x0000ff ) getRGB (127) ; // -> 8355711 (0 x7f7f7f ) getRGB (true) ; // -> 16777215 (0 xffffff ) // Notez que les composantes sont contraintes dans [0, 255] getRGB (-175 , 0, 255) ; // -> 255 (0 x0000ff ) getRGB ( -255) ; // -> 0 (0 x000000 ) 3.1.2 Codage de l’image complète Grâce aux fonctions précédentes, implémentez • toGray(int[][] image) qui convertit une image donnée en format RGB vers une image en niveaux de gris (int[][]) ; • toBW(int[][] gray, int threshold) qui convertit une image en niveaux de gris vers une image binaire en noir et blanc (on utilise comme type de retour un boolean[][] où les entrées à false représente le noir et celle à true le blanc). • toRGB(boolean[][] bw) qui convertit dans l’autre sens une image binaire en une image au format RGB • toRGB(int[][] gray) et qui fait de même pour une image en niveaux de gris. Pour cela, vous devez créer un tableau de la bonne taille, et utiliser getGray, getBW ou getRGB pour chaque pixel. // Une image couleur 2x2 int [][] image = { {0 x20c0ff , 0 x123456 }, {0 xffffff , 0 x000000 } }; // On la convertit en gris int [][] gray = toGray ( image ) ; // -> { // {0x9f , 0x34}, // {0xff , 0x0} // }; // On revient vers du RGB int [][] back = toRGB (gray) ; 8 // -> { // {0 x9f9f9f , 0 x343434 }, // {0 xffffff , 0 x000000 } // }; 3.1.3 Tests Les données fournies en exemple dans l’énoncé peuvent être testées au moyen de ce que l’on appelle des tests unitaires5 . Pour ce faire, dans Eclipse, faites un clic-droit sur ImageMessageTests.java (dans le package ”test”) puis « Run as » et « JUnit Test ». Une fenêtre devrait apparaître indiquant que certains tests sont échoués (en rouge ou bleu) et d’autres réussis (en vert). Pour cette étape, tous les tests de ImageMessageTests.java, sauf bwImageBitArrayTest, devraient être en vert. Pour tester cette partie graphiquement, vous pouvez utiliser le programme fourni dans MainBasics.java que vous pouvez bien sûr augmenter à votre guise. Les tests fournis ne sont pas exhaustifs. Vous êtes encouragés à tester votre code dans d’autres situations (dans les main fournis ou d’autres). 3.2 Dissimulation de l’image Nous abordons maintenant le coeur de la première étape. Il s’agit de cacher puis de dévoiler une image en noir et blanc, représentée sous la forme d’un tableau de booléens, dans une image au format RGB. 3.2.1 Incrustation Dans le fichier Steganography.java, implémentez la méthode embedBWImage(int[][] cover, boolean[][] message) qui dissimule une image binaire message dans une image RGB cover en changeant le dernier bit de chaque pixel de cover vers la valeur du pixel correspondant de message. Vous itérerez donc sur chaque pixel de message, en commençant en haut à gauche6 , et remplacerez le dernier bit du pixel de cover situé à la même position par la valeur de celui de message. Important : l’incrustation ne doit pas se faire sur le support d’origine qui doit rester inchangé. Copiez par exemple les valeurs de cover avec le dernier bit modifié dans un array que vous retournez. Vous tiendrez compte aussi du fait que la charge est plus petite ou égale au support. 5 6 Sujet du semestre prochain le point (0,0) du référentiel pour les images est en haut à gauche 9 Pour implémenter embedBWImage(int[][] cover, boolean[][] message), vous aurez besoin de coder embedInLSB(int value, boolean m) qui prend un pixel RGB (value) et une valeur booléene m, et retourne ce pixel avec son bit le plus à droite mis à 0 si m est false, ou à 1 si il est true. Vous pouvez par exemple vous servir des opérateurs binaires, comme &, l’opérateur ’et’ binaire (voir les compléments ainsi que les transparents présentés en cours). 3.2.2 Dévoilement Pour pouvoir décoder l’image cachée, vous implémenterez ensuite revealBWImage(int[][] cover) qui forme une image binaire en lisant les valeurs du bit de poids faible (le plus à droite) de chaque pixel de l’image RGB cover. Comme précédemment, vous implémenterez une méthode supplémentaire getLSB(int value) qui, donné un pixel RGB value, retourne true si son dernier bit est 1, et false s’il est 0. 3.2.3 Exemple int [][] cover = Helper .read(" cover .jpg") ; int [][] message = Helper .read(" message .jpg") ; int [][] gray = ImageMessage . toGray ( message ) ; boolean [][] bw = ImageMessage .toBW(gray , 240) ; int [][] hidden = Steganography . embedBWImage (cover , bw) ; // retourne le support contenant le message caché boolean [][] decoded = Steganography . revealBWImage ( hidden ) ; 3.2.4 Tests Pour cette partie, procédez comme pour la précédente, mais avec le fichier LinearEncodingTests.java. À ce stade-ci du projet, les tests getLSBTest, embedInLSBTest, revealBWImageTest et embedBWImageTest devraient être en vert. Pour tester cette partie graphiquement, vous pouvez utiliser le programme fourni dans MainImages.java que vous pouvez bien sûr augmenter à votre guise. 10 4 Encodage texte Dans cette partie, la charge ne sera plus une image binaire, mais un texte sous forme de String. Il faudra d’abord convertir ce texte en tableau de booléens, via une représentation en tableau d’entiers, pour pouvoir le cacher dans le support (qui est toujours une image). Avec la méthode du bit de poids le plus faible utilisée dans ce projet, nous ne pouvons en effet incruster qu’une suite de bits. Pour cacher du texte (une String), il nous faut donc commencer par le convertir en un tableau de bits. Pour cela, on convertira chaque caractère vers sa valeur numérique (int), puis chaque entier ainsi obtenu vers sa représentation binaire (boolean[]). On obtiendra ainsi un tableau de booléens qui correspond à la représentation binaire de la String que l’on veut encoder. De plus, lorsqu’on dévoile le message encodé, on obtient un tableau de booléens. Il faut donc convertir ce tableau vers la String à laquelle il correspond. Pour cela, on appliquera l’opération inverse de celle utilisée pour transformer la String en tableau binaire : on convertira le tableau de booléens en tableau d’entiers, puis on convertira ce tableau d’entiers en caractères, et donc en String. Ainsi, vous implémenterez d’abord les différentes méthodes pour convertir une String en tableau de bits, ou l’inverse ; puis la méthode permettant de cacher un tableau de boolean dans une image. 4.1 Conversion String vers tableau de bits, et vice versa Pour convertir une String en tableau de boolean, vous convertirez chaque char vers sa valeur entière (int), puis vous convertirez chaque int vers sa représentation binaire (boolean[]). Tous ces tableaux de booléens mis bout-à-bout formeront la représentation binaire de la String. Pour convertir un tableau de booléens vers une String, l’opération inverse sera faite : d’abord convertir les booléens vers un tableau d’entiers, puis chaque entier vers le char correspondant, et enfin ce tableau de char servira à construire la String désirée. 4.1.1 Entier vers tableau de bits Dans le fichier TextMessage.java, implémentez la méthode intToBitArray(int value, int bits) qui, donné un entier value, retourne ses bits bits les plus faibles sous forme d’un tableau de booléen (comme dans la partie Encodage direct, un 1 sera représenté par true et un 0 par false). Cet entier sera écrit en commençant par le bit de valeur faible, c’est-à-dire à ”l’envers” de sa représentation écrite : par exemple, le nombre ”5” écrit sur 8 bits (0b00000101) sera {true, false, true, false, false, false, false, false}. 11 Vous supposerez que vous n’aurez à convertir que des entiers positifs. Référez-vous à la section 4.2 de votre livre d’ICC et/ou à la vidéo « Représentation des entiers en binaire » de Ronan Boulic (https://youtu.be/a5gLSc0tbjI [Lien cliquable]), ou à votre moteur de recherche préféré, pour plus d’information sur la représentation d’un entier en binaire (complément à deux). 4.1.2 String vers tableau de booléens, via tableau d’entiers Toujours dans le même fichier, implémentez maintenant stringToBitArray(String message). Cette méthode prend la String que l’on veut transformer en paramètre, et retourne le tableau de booléens correspondant à la représentation binaire de cette String. Pour cela, elle devra d’abord transformer la String en tableau d’entiers, où chaque entier est la valeur numérique du caractère correspondant de la String. Vous pouvez utiliser String.charAt(int index)[Lien] pour obtenir un caractère à un index voulu d’une String, et un transtypage (cast) pour convertir un char en entier (voir aussi http://proginsc.epfl.ch/wwwhiver/documents/cours04.pdf). Ensuite, elle convertira ce tableau d’entiers vers un tableau de booléens en utilisant intToBitArray, avec un argument bits de 16 (puisqu’un char est codé sur 16 bits en Java)7 . 4.1.3 Tableau de bits vers tableau d’entiers Il faut donc d’abord implémenter bitArrayToInt(boolean[] bitArray) qui transforme en entier le tableau binaire passé en paramètre. Comme avant, false correspond à un bit 0 et true à 1. N’oubliez pas que l’entier est forcément positif, et que le premier élément du tableau de booléen correspond au bit le plus à droite de la représentation binaire de l’entier. 4.1.4 Tableau d’entiers vers String Puis implémentez bitArrayToString(boolean[] bitArray) qui convertit un tableau de booléens vers la String qu’il représente. Utilisez bitArrayToInt pour transformer chaque sous-tableau de 16 bits vers un entier, puis transtypez ces entiers vers des char, pour pouvoir former la String correcte. Cherchez dans l’API de Java (ou au moyen de votre moteur de recherche favori) de la documentation sur Arrays.copyOfRange qui pourra vous rendre de précieux services ici. 4.2 Dissimulation et dévoilement La figure 2 illustre comment incruster la String : chaque nouvelle couleur correspond à un nouveau caractère. Cette figure utilise 4 bits pour un caractère, mais vous en aurez 16. 7 Utilisez la constante Java Character.SIZE qui contient la valeur 16 12 Fig. 2 : Les 4 bits du premier caractère sont incrustés dans les bits de points le plus faible des 4 premiers pixels, ceux du second caractère sont incrustés dans les bits de points le plus faible des 4 pixels suivants et ainsi de suite (dans cet exemple on veut cacher 6 caractères encodés sur 4 bits chacun au lieu de 16) 4.2.1 Du tableau de bits Maintenant il faut pouvoir cacher ce tableau binaire dans le support. Implémentez embedBitArray(int[][] cover, boolean[] message) qui incruste le tableau message dans les bits de poids faible des X premiers8 pixels de l’image cover, où X est la longueur du tableau message. Puis implémentez revealBitArray(int[][] cover) qui retourne le tableau booléen unidimensionnel correspondant aux bits de poids faible de cover. 4.2.2 De la String Implémentez maintenant embedText(int[][] cover, String message) et revealText(int[][] cover) qui, en utilisant les méthodes que vous avez codées jusqu’ici, dissimule ou révèle une 8 En commençant au pixel {0,0}, et en traversant de gauche à droite puis de haut en bas. 13 String dans/depuis une image cover. 4.3 Tests Maintenant, tous les tests de TextMessageTests.java, ainsi que testEmbedBitArray, testRevealBitArray, testEmbedText et testRevealText de LinearEncodingTests.java devraient réussir (si vous avez correctement fait la 1ère partie du projet, tous les tests de LinearEncodingTests.java devraient maintenant être en vert). Cette partie peut être testée graphiquement au moyen du programme principal fourni dans MainStrings.java (ou MainChocolate.java !). 14 5 Encodage en spirale Nous avons jusqu’ici incrusté linéairement le message à cacher sur le support. Cette façon de faire est très facile à détecter puisqu’il suffit d’examiner en séquence le contenu des bits de poids le plus faible pour dévoiler un contenu caché (figure 3, à droite). Le but de la stéganographie étant justement de ne pas être détectée, des techniques plus complexes doivent être envisagées pour ”brouiller les pistes”. Le but de cette partie est d’implémenter une variante de l’encodage linéaire sur LSB : l’encodage en spirale ; et ce, toujours sur la couche des bits de poids faibles. L’encodage en spirale consiste à convertir l’information à cacher vers un tableau de bits linéaire (unidimensionnel), puis de stocker ce tableau dans les bits de poids faible du support en suivant un parcours en spirale de ses pixels. Nous nous préterons ici à cet exercice pour cacher/révéler une image. Afin de pouvoir reconstruire l’image cachée au décodage, il est alors nécessaire de stocker aussi sa hauteur et sa longueur. Comme le montre la figure 3, la couche des bits de poids faible du support ne permet pas de visuellement détecter l’image qui y est encodée lors d’un encodage en spirale. Fig. 3 : Comparaison des couches de bits de poids faible d’une image support (au centre) entre l’encodage linéaire (à droite) et l’encodage en spirale (à gauche) 15 Fig. 4 : Illustration de la façon dont est encodé le tableau de bits dans le support lors d’un encodage en spirale La figure 4 montre comment est encodée l’image (préalablement transformée en tableau de bits) dans son support : • En bleu, 32 bits représentant la hauteur de l’image • En rouge, 32 bits représentant la largeur de l’image • En vert, les bits correspondant aux différents pixels de l’image Vous noterez que cette méthode n’est pas limitée à la dissimulation d’images : puisque ce qui est effectivement caché dans le support n’est qu’un tableau unidimensionnel de booléens (représentant une image), il en vient que l’on peut stocker n’importe quelle information (texte, audio, etc.) tant qu’elle est représentée sous la forme d’une suite de bits. 5.1 5.1.1 Dissimulation en spirale Transformation de l’image binaire en tableau unidimensionnel Dans ImageMessage.java, implémentez bwImageToBitArray(boolean[][] bwImage) qui transforme un tableau de booléens à deux dimensions vers son équivalent unidimensionnel : le contenu de chaque sous-tableau mis bout à bout. Ce tableau unidimensionnel commencera cependant par les représentations binaires de deux entiers9 : la hauteur (taille de la première dimension du tableau bwImage) et la largeur (taille de sa deuxième dimension), comme illustré dans la figure 4. 9 Contrairement aux char de la partie précédente, ici il faut encoder les int sur 32 bits 16 5.1.2 Encodage Ensuite, implémentez embedSpiralBitArray(int[][] cover, boolean[] message) dans le fichier Steganography.java. Cette fonction prend en argument l’image support cover dans laquelle on encode le tableau binaire unidimensionnel message. Ce dernier sera inscrit dans cover en un motif de spirale, dans le sens des aiguilles d’une montre, et démarrera au pixel le plus en haut à gauche10 . Implémentez aussi embedSpiralImage(int[][] cover, boolean[][] bwImage), qui, grâce aux deux fonctions implémentées précédemment, dissimule une image en noir et blanc dans une image support, en un motif en spirale comme décrit au début de cette section. 5.2 5.2.1 Dévoilement d’un message encodé en spirale Transformation du tableau unidimensionnel en image binaire Le message est encodé comme un tableau unidimensionnel : il faudra donc le reconvertir vers sa forme bidimensionnelle originale (avec hauteur et largeur d’origine). Pour cela, implémentez bitArrayToImage(boolean[] bitArray) (dans ImageMessage.java), qui retransforme bitArray vers le tableau de booléens bidimensionnel d’origine. Les hauteur et largeur originales du tableau sont encodées dans les 64 premiers éléments de bitarray (32 bits chacun). Il faut donc lire ces deux entiers, puis faire en sorte que les width premiers booléens après le 64e soient dans la 1ère ligne, les width suivants dans la 2ème , etc. Pensez à vérifier que bitArray contient assez d’éléments pour pouvoir lire les deux entiers, ainsi que le nombre d’éléments correspondant à ces deux dimensions (par exemple, un tableau de booléens représentant une image 2x2 devra avoir au moins 32 + 32 + 2 ∗ 2 = 68 éléments pour être valide). Vous pouvez vérifier ceci grâce aux assertions, comme expliqué au début de l’énoncé. 5.2.2 Dévoilement de l’image Dévoilez maintenant le tableau booléen dans revealSpiralBitArray(int[][] hidden) (dans Steganography.java), qui retourne un boolean[] correspondant à la couche des bits de poids faible de l’image hidden lus en motif de spirale. Grâce à cette méthode, implémentez revealSpiralImage(int[][] hidden) qui lit le tableau de booléens dans hidden, puis le convertit vers l’image binaire originale avec bitArrayToImage. 10 D’index [0][0] 17 5.3 Tests Tous les tests de SpiralEncodingTests.java, ainsi que bwImageBitArrayTest du fichier ImageMessage.java devraient maintenant passer. Ainsi, si vous avez correctement implémenté les deux autres parties, tous les tests des 4 fichiers du package test devraient passer. Vous pouvez tester graphiquement cette partie au moyen du programme fourni MainSpiral.java. 18 6 Complément théorique – Couleurs, pixels et binaire Une image est formée par une infinité de rayons de lumière qui viennent heurter notre rétine. Toutefois, nos yeux et notre cerveau ne sont pas capables de percevoir et encore moins de gérer une quantité infinie d’information. C’est un problème récurrent dans de nombreux domaines, et les ordinateurs n’y font pas exception. La solution consiste à approximer l’image par une quantité finie de valeurs. On parle donc d’approximation discrète d’un phénomène continu. Il est fort probable que vous connaissiez la notion de pixel (littéralement picture element), qui représente un point de l’écran. Dans la même idée, une image dite matricielle est composée d’une grille à deux dimensions de pixels. En d’autres termes, il s’agit d’une matrice de couleurs. Bien qu’il existe d’autres formats, chacun ayant leurs propres avantages et faiblesses, nous nous contenterons de cette représentation pour cet exercice. Il s’agit en effet du modèle le plus simple et efficace pour le traitement d’image. Quant aux couleurs elles-mêmes, il est aussi nécessaire de les discrétiser. Les couleurs s’obtiennent par synthèse d’un nombre limité de couleurs primaires. La peinture utilise un modèle de synthèse dit soustractif11 . C’est avec un tel modèle que l’on peut dire que mélanger du jaune et du bleu donne du vert. A contrario, les modèles dit additifs combinent les lumières de plusieurs sources colorées dans le but d’obtenir une lumière colorée. La synthèse additive[Lien] utilise généralement trois lumières colorées : une rouge, une verte et une bleue (RGB en anglais pour red, green, blue). Le mélange de ces trois lumières colorées en proportions égales donne la lumière blanche. L’absence de lumière donne du noir. Les LEDs des écrans utilisent le procédé de la synthèse additive. Nous allons donc utiliser RGB[Lien] pour représenter nos couleurs, bien qu’il soit tout à fait possible d’utiliser d’autres modèles et de les combiner. Fig. 5 : Pixel d’un écran Fig. 6 : Modèle additif Fig. 7 : Modèle soustractif Pour être précis, une couleur est représentée sur 3 bytes (soit 24 bits). Cela permet 28 = 256 niveaux de rouge, vert et bleu, allant de 0 à 255. Ainsi, on peut représenter 2563 = 16 777 216 couleurs différentes. L’oeil humain étant capable de percevoir dans l’ordre de 300 000 nuances seulement, il n’est donc pas nécessaire d’avoir plus de précision ! L’unité de base de la majorité des processeurs étant un entier 32 bits, il est possible d’y stocker les 3 composantes. Il nous reste même 8 bits inutilisés, représentant parfois la transparence que l’on nomme alpha (d’où le modèle ARGB). 11 Soustractif, car plus on ajoute des couleurs, moins il y a de luminosité 19 Coleurs Chiffres binaires Chiffres décimaux Inutilisé/Alpha 00000000 0 Rouge 00100000 32 Vert 11000000 192 Bleu 11111111 255 Le binaire a évidemment un rôle majeur à jouer en programmation. Même si vous ne les avez probablement pas encore rencontrées, il existe des opérations qui permettent de manipuler les bits des entiers. Toutes les opérations booléennes ont leurs équivalents binaires. // Notre couleur en binaire int x = 0 b00000000_00100000_11000000_11111111 ; // -> 2146559 (0 x20c0ff ) // Décale de 8 bits vers la droite int y = x >> 8 ; // -> 0 b00000000_00100000_11000000 // ET binaire , ce qui a pour effet de ne garder que les 8 // premiers bits int z = y & 0 b11111111 ; // -> 0 b11000000 // On a bien récupéré notre composante verte à 192 // on aurait aussi pu écrire : int z = y & 0xff ; 6.1 Représentation binaire des entiers Référez-vous à la section 4.2 de votre livre d’ICC et/ou à la vidéo « Représentation des entiers en binaire » de Ronan Boulic (https://youtu.be/a5gLSc0tbjI), ou à votre moteur de recherche préféré, pour plus d’information sur la représentation d’un entier en binaire (complément à deux). 6.2 Références • https://fr.wikipedia.org/wiki/St%C3%A9ganographie • http://www.aaronmiller.in/thesis/ • http://www.garykessler.net/library/fsc_stego.html 20