Hú, hihetetlen, hogy mennyit szívtam ennek a bejegyzésnek a kódjával. De erről majd egy picit később, kezdjük az elejéről: ismét eszembe ötlött egy hasonlóan haszontalan ötlet, mint előző bejegyzésemben, amikor is egy titkosító algoritmusról volt szó. A téma - ahogy azt a cím is mutatja - közel áll az előbb említett bejegyzéshez, csak most nem titkosítani szeretnénk, hanem el szeretnénk rejteni az adatainkat a kíváncsi szemek elől. Ki akarna bármit is megfejteni, egy olyan helyen, ahol senki sem gondolná, hogy van bármi is, amit meg lehetne fejteni?

A kódoláshoz ismét a PHP svájci bicskát megszégyenítő eszköztára lesz a segítségünkre, azon belül is a GD könyvtár. Csapjunk is bele!

Először is ejtsünk pár szót a képekről. Vegyünk egy átlagos 300*300 pixeles képet, melynek minden egyes pontja három vagy esetleg négy darab nyolc bites szám által van meghatározva. Ez a híres nevezetes RGBa, azaz Red-Green-Blue-alpha. Az alpha része most minket nem érdekel, foglalkozzunk csak az RGB-vel. Mint fentebb említettem, ezek nyolc bites számok vagyis 0 és 255 közé esnek (vagy akinek úgy barátságosabb, 00 és FF közé), tehát egy pixel meghatározásához 24 bitre van szükségünk. Így tovább okoskodva, egy 300*300 pixeles kép mérete 300*300*24 bit (közel 263 kilobyte). Ez elég soknak tűnik, de ha minden igaz ez a BMP tömörítetlen módszere. Mivel ez egy ilyen hatalmas dolog, megjelentek különböző tömörítési eljárások, mint a JPG, amivel most nem fogunk foglalkozni, mivel számunkra létfontosságú, hogy a készülő képünk minden pixelének színét pontosan meg tudjuk határozni.

Mi is akkor a megoldás? Mivel a PHP nem tud kezelni BMP-ket, ezért egyetlen választásunk a PNG (vagy esetleg GIF) által nyújtott indexelt képek maradtak. Hogy mik is azok az indexelt képek? Az indexelt képben van egy vektor (egy dimenziós tömb), 0-tól 255-ig indexelve, amiben vannak tárolva a kép által használt színek (tehát 256*24 bit eddig), a képben pedig a szín helyén csak az index száma van megjelenítve. A dolog lényege, hogy jóval kisebb fájlméretet eredményez (a fenti 300*300 pixeles képnél maradva ez úgy 88 kilobyte), viszont megvan az a megkötése, hogy csak 256 vagy kevesebb különböző színt lehet a képben használni. Természetesen a dolog a való életben nem ennyire egyszerű, de nekünk most ennyi is elég. Térjünk át az adatrejtési eljárás működési elvére.

A lényeg abban rejlik, hogy vesszük egyesével a pixeleket, megnézzük a pixel egyik (vagy mindhárom) színének értéket (ugye ami 0 és 255 között van) majd páros illetve páratlan számra változtatjuk, attól függetlenül, hogy az elrejteni kívánt bitünk éppen nulla vagy egy. A bitet pedig egyszerűen úgy szerezzük, hogy vesszük a karakter ascii kódjának bináris formáját (szintén 0 és 255 közötti szám), a kriptós bejegyzéshez hasonló módon feltöltjük az elejét kellő mennyiségű nullával, majd szépen a pixelekbe kódoljuk. Az elgondolás szép, csak van vele pár probléma. Fentebb ugye említettem, hogy csak 256 különböző színt használhatunk, így ha egy eleve 256 színű kép színeit változtatjuk, előfordulhat, hogy túllépjük ezt a keretet és nem sikerül megfelelően az adatrejtés. Még szerencse, hogy pontosan 256 darab szürkeárnyalat van (a feketével és a fehérrel együtt), amiket bárhogy változtatunk páros vagy páratlan számmá, ugyanúgy szürkék maradnak (tehát a meglévő palettán lesznek).

Úgy érzem egyre inkább itt az ideje egy kis kódot is látni a sok száraz anyag után, hogy mindez, amit felvázoltunk valósággá válhasson:

$text = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Sed posuere risus eget orci. Nullam augue. Nulla porta lacus vitae nulla.
Donec volutpat, libero nec ultrices condimentum, metus est congue velit, in
tincidunt felis sem vel ante. Integer at tellus vitae lorem mollis viverra.
Nulla eros. Class aptent taciti sociosqu ad litora torquent per conubia
nostra, per inceptos hymenaeos. Praesent sollicitudin arcu ac leo. Donec
nulla justo, imperdiet id, eleifend et, viverra non, tellus. Nunc auctor,
ligula ac tempor ornare, ipsum diam consequat justo, vitae tristique leo
massa non velit. Praesent tincidunt pharetra felis. Curabitur nec massa.
Nam vel purus. Nulla suscipit dui ac enim. Fusce bibendum varius lorem.
Pellentesque cursus massa. Phasellus ut risus vel massa dignissim
hendrerit.';
$length = strlen($text);

$old = imagecreatefrompng('image.png');
$width = imagesx($old);
$height = imagesy($old);

$new = imagecreate($width, $height);

for ($c=0;$c<256;++$c) {
	$colors[$c.$c.$c] = imagecolorallocate($new, $c, $c, $c);
}

Kezdetben van egy szép hosszú szövegünk, amit bele akarunk kódolni a képbe (maximum képszélesség*képmagasság/8 karakter hosszú szöveg lehet). Ezután megnyitjuk a képet és létrehozunk az eredeti kép méretével azonos méretű üres képet. Ebben a képben az imagecolorallocate() függvény segítségével lefoglaljuk az összes szürke színt (0,0,0) és (255,255,255) között.

$tp = 0;
$bp = 0;
for ($i=0;$i<$width;++$i) {
	for ($j=0;$j<$height;++$j) {
		$old_color = imagecolorsforindex($old, imagecolorat($old, $i, $j));

		if ($tp < $length) {
			$char = base_convert(ord(substr($text, $tp, 1)), 10, 2);
		}
		else {
			// Ha végeztünk a bekódolandó szöveggel, szóközöket kódolunk
			// bele a képbe.
			$char = '00100000';
		}
		if (($len=strlen($char))<8) {
			$char = str_repeat('0', 8-$len).$char;
		}
		$bit = substr($char, $bp, 1);

		if ($bit === '1') {
			// A szám páros, de nekünk páratlan kell
			if (($old_color['red']%2) === 0) {
				$new_color = $old_color['red']+1;
			}
			else {
				$new_color = $old_color['red'];
			}
		}
		else {
			if (($old_color['red']%2) === 0) {
				$new_color = $old_color['red'];
			}
			// A szám páratlan, de nekünk páros kell
			else {
				$new_color = $old_color['red']-1;
			}
		}

		imagerectangle(
			$new,
			$i, $j,
			$i+1, $j+1,
			$colors[$new_color.$new_color.$new_color]
		);

		if ($bp === 7) {
			$bp = -1;
			++$tp;
		}
		++$bp;
	}
}

Ez magának az elrejtésnek a ciklusa, végigmegyünk a kép összes pixelén, veszünk hozzá egy bitet a szövegből, megnézzük, hogy megfelelő szám szerepel-e a pixelen (ha nullás bitünk van, akkor páros, ha egyes akkor páratlan), ha nem, akkor pedig megfelelővé tesszük, majd berajzoljuk az új képben a megfelelő helyre. Érdemes szót ejteni az imagecolorat() függvényről, ami True Color képek esetén egy int-et adna vissza, ami az RGBa-t tárolja elég csúnya módon. Viszont a dokumentációban sehol sem láttam említést arra, hogy indexelt kép esetén a szín indexével tér vissza, pedig ez így van, ezért van szükség az imagecolorsforindex() függvényre is.

imagepng($new, 'image-data.png');
imagedestroy($old);
imagedestroy($new);

Végül lementjük az új képet, amibe bele van kódolva a szöveg és töröljük a memóriában lévő példányokat, amikre már nincs szükségünk. És íme a két kép, felül az eredeti, alul pedig az, amelyikben már elrejtettük a szöveget:

Az eredeti kép
A kép, amiben adatokat rejtettünk el

Természetesen nincs szemmel látható különbség, mivel csak maximum egyel változtattuk meg a pixel értékét. Az eredeti kép (színekkel) megtalálható itt. Nincs más hátra, mint, hogy megmutassam, hogyan is kaphatjuk vissza a szövegünket a képből (különben mi értelme lenne ennek a kis rejtésnek, ha mi sem találunk rá). Íme tehát a kód:

$data = imagecreatefrompng('image-data.png');
$width = imagesx($data);
$height = imagesy($data);

$char = '';
$bp = 0;
for ($i=0;$i<$width;++$i) {
	for ($j=0;$j<$height;++$j) {
		$data_color = imagecolorsforindex($data, imagecolorat($data, $i, $j));
		$char .= ($data_color['red']%2);

		if ($bp === 7) {
			$bp = -1;
			echo chr(base_convert($char, 2, 10));
			$char = '';
		}
		++$bp;
	}
}

Már csak azzal maradtam adós, hogy elmeséljem, miért is szívtam én annyit egy ilyen egyszerűnek mondható kóddal? A dolog teljes mértékben az utóbbi dekódoló script hibája volt. Ugyanis mindig értelmetlen szövegekkel tért vissza, még véletlenül sem olyasmivel, ami egy picit is hasonlítana az eredetihez. Természetesen én az adatrejtő scriptre gyanakodtam, majd a gyanúm átterelődött a PHP GD könyvtárára, hogy az machinál valamit és nem kapom meg az eredeti RGB értékeket (ugyebár ebben az eljárásban létfontosságú az, hogy ugyanazt vissza is kapjuk). Végül aztán kiderül, hogy a dekódoló 10-edik sora volt a hibás, ugyanis az ott szereplő moduló kifejezést valamiért negáltam (a fene tudja, hogy miért), így pont az inverzét kaptam meg a biteknek, ami természetesen értelmetlen karaktersorozat volt.

Ennyi mára, mindenkinek kellemes bitbújócskát a hétvégére.