Stéganographie

Première mission

Dévoiler le message caché dans les 2 bits de poids faible de l’image fournie (dont l’adresse est https://info-tsi-vieljeux.github.io/cryptedimage.png).

Importons d’abord les modules nécessaires :

from PIL import Image
import urllib.request # pour récupérer une image sur le web
from IPython.display import display # pour afficher dans un notebook
import numpy as np

Ce petit code suffit pour dévoiler l’image cachée dans l’image :

def decache_image(image):
    image_decrypt = (image % 2**2) * 2**6 + 2**5
    return image_decrypt
  • image % 2**2 récupère les deux bits de poids faibles.
  • * 2**6 permet de “dilater” les 4 valeurs $\{0,1,2,3\}\rightarrow\{0,64,128,192\}$
  • + 2**5 permet de décaller les valeurs pour les centrer entre 0 et 255 $\rightarrow\{32,96,160,224\}$.

Pour l’afficher (sur un notebook type Colab) :

final = decache_image(cache)
affichage = Image.fromarray(comparaison)
display(affichage)

On peut vérifier que l’image obtenue est bien codée sur 2 bits avec le code suivant :

print(set(final.flatten()))

{32, 224, 96, 160}

flatten est une méthode de numpy permettant de transformer un tableau multidimensionnel en un tableau unidimensionnel (ici, le tableau image 3D devient donc un tableau 1D de longueur $l\times L \times 3$).
set est une fonction native python permettant de convertir une séquence d’objets en une nouvelle structure, l’ensemble (en python, c’est le typeset). Son grand avantage est d’éliminer automatiquement tout doublon !


Deuxième mission

Il faut donc réussir à convertir l’image secrète en 6 bits (2 bits pour chaque pixel) et l’image servant de cache en 18 bits (6 bits pour chaque pixel) avant d’additionner les deux images, faisant en sorte que l’image à cacher corresponde aux deux bits de poids faible de l’image finale.

def cache_image(image1,image2):
    """
    cache_image(image1 : numpy.ndarray (H1,L1,3),image2 : numpy.ndarray (H2,L2,3)) -> numpy.ndarray (H1,L1,3)
    cache image2 dans image1 après l'avoir recadrée si besoin
    """
    H1,L1,_ = image1.shape
    H2,L2,_ = image2.shape
    hidden = np.copy(image2)[:H1,:L1,:3] # sans np.copy, le slicing rend image2 mutable
    hidden = (hidden.astype(np.uint16)//2**6).astype(np.uint8)
    image1 = image1//2**2 * 2**2
    imagemix = image1[:,:,:3] # le :3 sert à retirer une éventuelle transparence, codée comme une 4e couleur
    h , l = max((H1-H2)//2,0), max((L1-L2)//2,0)
    imagemix[h:h+H2,l:l+L2] += hidden
    return imagemix

Et pour faciliter l’utilisation d’url pour chaque image :

def cachageimage():
    opener=urllib.request.build_opener()
    opener.addheaders=[('User-Agent','Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1941.0 Safari/537.36')]
    urllib.request.install_opener(opener) # sans ce bazar, certains sites refusent la visite car le fouineur se présente pas comme un browser
    url1 = input("url de l'image à afficher : ")
    urllib.request.urlretrieve(url1, '1')
    url2 = input("url de l'image à cacher : ")
    urllib.request.urlretrieve(url2, '2')
    image1 = np.array(Image.open('1'))
    image2 = np.array(Image.open('2'))
    return cache_image(image1,image2)

On obtient finalement :

Et en “décachant” (pour vérifier) :