Stéganographie

Série 11 – corrigé

Introduction

Le code du corrigé est fourni sous la forme d'une archive Zip, et des explications concernant les principales difficultés des exercices sont données ci-dessous.

Exercice 1 : Extraction d'un texte

La méthode extract n'est pas très difficile à écrire étant donné que les bits sont stockés dans les pixels par poids décroissant. Dès lors, il est facile de les accumuler progressivement dans une variable (bits ci-dessous), en les insérant par la droite, tout en comptant le nombre de bits lu dans une autre variable (bitsCount ci-dessous). Dès que 16 bits ont été ainsi lus, ils sont convertis en un caractère, qui est ajouté à la chaine en construction, et le processus peut recommencer.

public static String extract(BufferedImage image) {
  StringBuilder b = new StringBuilder();
  int bits = 0, bitsCount = 0;
  for (int y = 0; y < image.getHeight(); ++y) {
    for (int x = 0; x < image.getWidth(); ++x) {
      int rgb = image.getRGB(x, y);
      bits = (bits << 1) | (rgb & 1);
      if (++bitsCount == CHAR_BITS) {
	b.append((char)bits);
	bits = bitsCount = 0;
      }
    }
  }
  return b.toString();
}

A noter qu'il serait encore possible de simplifier ce code en se passant de la variable bitsCount, en initialisant bits à 1 et en détectant que 16 bits ont été lus en constatant que bits est supérieur ou égal à 1 << CHAR_BITS.

Exercice 2 : Insertion d'un texte

La méthode insert est un peu plus complexe à écrire, car trois entités sont parcourues en même temps : les pixels de l'image, les caractères de la chaîne, et les bits du caractère courant.

Les pixels sont parcourus comme dans la méthode extract, au moyen d'une paire de boucles imbriquées.

Les caractères de la chaîne sont parcourus au moyen d'un index (chrI) qui donne l'index dans la chaîne du prochain caractère à insérer. Le caractère en cours d'insertion est quant à lui stocké dans une variable (chr).

Les bits de ce caractère sont parcourus au moyen d'un index (bitI), qui donne la position du dernier bit inséré (entre 15 et 0).

Lorsque l'index du bit vaut 0, cela signifie que le bit de poids faible a été inséré, et donc que la totalité des bits du caractère courant l'ont été. A ce moment, le prochain caractère est obtenu de la chaîne, ou 0 s'il n'en reste plus.

public static BufferedImage insert(BufferedImage image,
				   String string) {
  BufferedImage outImage =
    new BufferedImage(image.getWidth(),
		      image.getHeight(),
		      BufferedImage.TYPE_INT_RGB);
  char chr = 0;
  int chrI = 0, bitI = 0;
  for (int y = 0; y < image.getHeight(); ++y) {
    for (int x = 0; x < image.getWidth(); ++x) {
      if (bitI == 0) {
	chr = chrI < string.length()
	  ? string.charAt(chrI++)
	  : 0;
	bitI = CHAR_BITS;
      }
      int rgb = image.getRGB(x, y);
      int bit = (chr >> --bitI) & 1;
      outImage.setRGB(x, y, (rgb & ~1) | bit);
    }
  }
  return outImage;
}

Exercice 3 : Améliorations possibles

On peut imaginer de nombreuses améliorations, parmi lesquelles :

  1. le texte pourrait être compressé avant d'être stocké, pour augmenter la quantité de texte stockable dans une image,
  2. plus d'un bit de la composante bleue pourrait être utilisé (p.ex. les 2 bits de poids faible), et/ou un ou plusieurs bits des autres composantes,
  3. plutôt que d'être stockés dans l'ordre, les bits du texte (compressé) pourraient être mélangés dans un ordre dépendant d'une clef, ce qui rendrait l'extraction du texte très difficile à toute personne ne connaissant pas celle-ci.