Wprowadzenie
Witam.
Niedawno, kiedy studiowałem na uniwersytecie, miałem projekt dotyczący przedmiotu "Programowe metody ochrony informacji". Zadanie polegało na stworzeniu programu, który wprowadzałby wiadomość do plików w formacie GIF. Postanowiłem wykonać to w Javie.
W tym artykule opiszę kilka teoretycznych kwestii, a także to, jak tworzono ten mały program.
Część teoretyczna
Format GIF
GIF (ang. Graphics Interchange Format — format do wymiany obrazów) to format przechowywania grafiki, który potrafi przechowywać skompresowane dane bez utraty jakości w formacie do 256 kolorów. Format ten został opracowany w 1987 roku (GIF87a) przez firmę CompuServe w celu przesyłania rastrowych obrazów przez sieci. W 1989 roku format został zmodyfikowany (GIF89a), dodano obsługę przezroczystości i animacji.
Pliki w formacie GIF mają strukturę blokową. Bloki te zawsze mają stałą długość (lub ich długość zależy od niektórych flag), więc praktycznie niemożliwe jest pomylenie się w tym, gdzie jaki blok się znajduje. Struktura najprostszej nieanimowanej grafiki GIF w formacie GIF89a:

Spośród wszystkich bloków struktury w tym przypadku interesować nas będą blok globalnej palety oraz parametry odpowiedzialne za paletę:
CT— obecność globalnej palety. Jeśli ten flag jest ustawiony, to zaraz po deskryptorze logicznego ekranu powinna zaczynać się globalna paleta.Size— rozmiar palety i liczba kolorów obrazka. Wartości tego parametru:
Size
Liczba kolorów
Rozmiar palety, bajty
7
256
768
6
128
384
5
64
192
4
32
96
3
16
48
2
8
24
1
4
12
0
2
6
Metody szyfrowania
Jako metody szyfrowania wiadomości w plikach obrazów będą stosowane:
- Metoda LSB (Least Significant Bit, najmniej znaczący bit)
- Metoda uzupełnienia palety
Metoda LSB — powszechnie stosowana metoda steganografii. Polega ona na wymianie ostatnich znaczących bitów w kontenerze (w naszym przypadku bajtów globalnej palety) na bity ukrywanej wiadomości.
W programie będą wykorzystywane dwa ostatnie bity w bajtach globalnej palety w ramach tej metody. Oznacza to, że dla obrazu 24-bitowego, gdzie kolor palety reprezentuje trzy bajty dla koloru czerwonego, niebieskiego i zielonego, po wprowadzeniu wiadomości, każda składowa koloru zmieni się maksymalnie o 3/255 odcieni. Taka zmiana, po pierwsze, będzie niewidoczna lub trudna do zauważenia dla ludzkiego oka, a po drugie, nie będzie różnicowana na niskiej jakości urządzeniach wyjściowych.
Ilość informacji będzie bezpośrednio zależała od wielkości palety obrazu. Ponieważ maksymalny rozmiar palety to 256 kolorów, a jeśli zapisujemy po dwa bity wiadomości w każdej składowej koloru, maksymalna długość wiadomości (przy maksymalnej palecie w obrazie) wynosi 192 bajty. Po wprowadzeniu wiadomości do obrazu rozmiar pliku nie zmienia się.
Metoda rozszerzania palety, działająca tylko dla struktury GIF. Będzie najskuteczniejsza w obrazach o małej palecie. Jej istota polega na tym, że zwiększa rozmiar palety, a tym samym daje dodatkowe miejsce na zapis niezbędnych bajtów w miejscu bajtów kolorów. Biorąc pod uwagę, że minimalny rozmiar palety wynosi 2 kolory (6 bajtów), maksymalny rozmiar wprowadzonej wiadomości może wynosić 256×3–6=762 bajty. Wadą jest niska kryptograficzna ochrona, wprowadzone wiadomości można przeczytać przy użyciu dowolnego edytora tekstu, jeśli wiadomość nie była poddana dodatkowemu szyfrowaniu.
Część praktyczna
Projektowanie programu
Wszystkie niezbędne narzędzia do realizacji algorytmów szyfrowania i deszyfrowania będą znajdować się w pakiecie com.tsarik.steganography. Ten pakiet zawiera interfejs Encryptor z metodami szyfruj i decrypt, klasę Binary, oferującą możliwość pracy z tablicami bitów, a także klasy wyjątków UnableToEncryptException i UnableToDecryptException, które powinny być używane w metodach interfejsu Encryptor w przypadku błędów kodowania i dekodowania odpowiednio.
Główny pakiet programu com.tsarik.programs.gifed będzie zawierał uruchamialną klasę programu z metodą statyczną main, umożliwiającą uruchomienie programu; klasę, która przechowuje parametry programu; oraz pakiety z innymi klasami.
Realizacja samych algorytmów zostanie przedstawiona w pakiecie com.tsarik.programs.gifed.gif klasami GIFEncryptorByLSBMethod i GIFEncryptorByPaletteExtensionMethod. Oba te klasy będą implementować interfejs Encryptor.
Na podstawie struktury formatu GIF można stworzyć ogólny algorytm wprowadzenia wiadomości do palety obrazu:

Aby określić obecność wiadomości w obrazie, należy na początku wiadomości dodać pewną sekwencję bitów, którą deszyfrator odczytuje w pierwszej kolejności i sprawdza jej poprawność. Jeśli nie pasuje, uznaje się, że w obrazie nie ma ukrytej wiadomości. Następnie należy określić długość wiadomości. Potem sam tekst wiadomości.
Diagram klas całej aplikacji:

Realizacja programu
Realizacja całego programu może zostać podzielona na dwie części: realizację metod szyfrowania i deszyfrowania interfejsu Encryptor, w klasach GIFEncryptorByLSBMethod i GIFEncryptorByPaletteExtensionMethod, oraz realizację interfejsu użytkownika.
Rozważmy klasę GIFEncryptorByLSBMethod.

Pola firstLSBit i secondLSBit zawierają numery bitów każdego bajtu obrazu, do których należy umieszczać i z których odczytywać wiadomość. Pole checkSequence przechowuje kontrolną sekwencję bitów w celu zapewnienia rozpoznawalności wbudowanej wiadomości. Statyczna metoda getEncryptingFileParameters zwraca parametry wskazanego pliku i charakterystyki potencjalnej wiadomości.
Algorytm metody szyfruj klasy GIFEncryptorByLSBMethod:

I jego kod:
@Override
public void encrypt(File in, File out, String text) throws UnableToEncodeException, NullPointerException, IOException {
if (in == null) {
throw new NullPointerException("Input file is null");
}
if (out == null) {
throw new NullPointerException("Output file is null");
}
if (text == null) {
throw new NullPointerException("Text is null");
}
// czytaj bajty z pliku wejściowego
byte[] bytes = new byte[(int)in.length()];
InputStream is = new FileInputStream(in);
is.read(bytes);
is.close();
// sprawdz format
if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
throw new UnableToEncodeException("Plik wejściowy ma niewłaściwy format GIF");
}
// odczytaj właściwość rozmiaru palety z pierwszych trzech bitów w dziesiątym bajcie pliku
byte[] b10 = Binary.toBitArray(bytes[10]);
byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
// oblicz liczbę kolorów i możliwą długość wiadomości
int bOrigColorCount = (int)Math.pow(2, bsize+1);
int possibleMessageLength = bOrigColorCount*3/4;
int possibleTextLength = possibleMessageLength-2; // jeden bajt na kontrolę i jeden bajt na długość wiadomości
if (possibleTextLength < text.length()) {
throw new UnableToEncodeException("Tekst jest za duży");
}
int n = 13;
// zapisz sekwencję kontrolną
for (int i = 0; i < checkSequence.length/2; i++) {
byte[] ba = Binary.toBitArray(bytes[n]);
ba[firstLSBit] = checkSequence[2*i];
ba[secondLSBit] = checkSequence[2*i+1];
bytes[n] = Binary.toByte(ba);
n++;
}
// zapisz długość tekstu
byte[] cl = Binary.toBitArray((byte)text.length());
for (int i = 0; i < cl.length/2; i++) {
byte[] ba = Binary.toBitArray(bytes[n]);
ba[firstLSBit] = cl[2*i];
ba[secondLSBit] = cl[2*i+1];
bytes[n] = Binary.toByte(ba);
n++;
}
// zapisz wiadomość
byte[] textBytes = text.getBytes();
for (int i = 0; i < textBytes.length; i++) {
byte[] c = Binary.toBitArray(textBytes[i]);
for (int ci = 0; ci < c.length/2; ci++) {
byte[] ba = Binary.toBitArray(bytes[n]);
ba[firstLSBit] = c[2*ci];
ba[secondLSBit] = c[2*ci+1];
bytes[n] = Binary.toByte(ba);
n++;
}
}
// zapisz plik wyjściowy
OutputStream os = new FileOutputStream(out);
os.write(bytes);
os.close();
}
Algorytm i kod źródłowy metody decrypt klasy GIFEncryptorByLSBMethod:

@Override
public String decrypt(File in) throws UnableToDecodeException, NullPointerException, IOException {
if (in == null) {
throw new NullPointerException("Plik wejściowy jest pusty");
}
// odczyt bajtów z pliku wejściowego
byte[] bytes = new byte[(int)in.length()];
InputStream is = new FileInputStream(in);
is.read(bytes);
is.close();
// sprawdzenie formatu
if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
throw new UnableToDecodeException("Plik wejściowy ma zły format GIF");
}
// odczyt właściwości rozmiaru palety z pierwszych trzech bitów 10. bajtu z pliku
byte[] b10 = Binary.toBitArray(bytes[10]);
byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
// obliczenie liczby kolorów i możliwej długości wiadomości
int bOrigColorCount = (int)Math.pow(2, bsize + 1);
int possibleMessageLength = bOrigColorCount * 3 / 4;
int possibleTextLength = possibleMessageLength - 2; // jeden bajt na kontrolę i jeden bajt na długość wiadomości
int n = 13;
// odczyt sekwencji kontrolnej
byte[] csBits = new byte[checkSequence.length];
for (int i = 0; i < 4; i++) {
byte[] ba = Binary.toBitArray(bytes[n]);
csBits[2 * i] = ba[firstLSBit];
csBits[2 * i + 1] = ba[secondLSBit];
n++;
}
byte cs = Binary.toByte(csBits);
if (cs != Binary.toByte(checkSequence)) {
throw new UnableToDecodeException("Nie ma zaszyfrowanej wiadomości w obrazie (Sekwencja kontrolna jest niepoprawna)");
}
// odczyt długości tekstu
byte[] cl = new byte[8];
for (int i = 0; i < 4; i++) {
byte[] ba = Binary.toBitArray(bytes[n]);
cl[2 * i] = ba[firstLSBit];
cl[2 * i + 1] = ba[secondLSBit];
n++;
}
byte textLength = Binary.toByte(cl);
if (textLength < 0) {
throw new UnableToDecodeException("Zdekodowana długość tekstu jest mniejsza niż 0");
}
if (possibleTextLength < textLength) {
throw new UnableToDecodeException("Brak wiadomości (Zdekodowana długość wiadomości (" + textLength + ") jest mniejsza niż Możliwa długość wiadomości (" + possibleTextLength + "))");
}
// odczyt bitów tekstu i utworzenie bajtów tekstu
byte[] bt = new byte[textLength];
for (int i = 0; i < bt.length; i++) {
byte[] bc = new byte[8];
for (int bci = 0; bci < bc.length / 2; bci++) {
byte[] ba = Binary.toBitArray(bytes[n]);
bc[2 * bci] = ba[firstLSBit];
bc[2 * bci + 1] = ba[secondLSBit];
n++;
}
bt[i] = Binary.toByte(bc);
}
return new String(bt);
}
Implementacja klasy GIFEncryptorByPaletteExtensionMethod będzie podobna, tylko różni się metodą zapisywania/odczytywania informacji.
W klasie MainFrame opisano metody-«opakowania»: encryptImage(Encryptor encryptor) i decryptImage(Encryptor encryptor), przetwarzające wyniki metod interfejsu Encryptor i realizujące interakcję z użytkownikiem, t.j. otwierają okno wyboru plików, wyświetlają komunikaty o błędach itd.; a także inne metody: openImage(), umożliwiająca użytkownikowi wybór obrazu, exit(), realizująca wyjście z aplikacji. Te metody są wywoływane z Akcja‘ów odpowiednich punktów menu. W tej klasie dodatkowo zaimplementowano metody pomocnicze: createComponents() — tworzenie komponentów formularza, loadImageFile(File f) — ładowanie obrazu do specjalnego komponentu z pliku. Implementacja klasy GIFEncryptorByPaletteExtensionMethod jest analogiczna do implementacji klasy GIFEncryptorByLSBMethod, główna różnica polega na sposobie zapisu i odczytu bajtów wiadomości z palety.
Działanie programu
Metoda LBS
Załóżmy, że jest taki obraz:

Na tym obrazku paleta składa się z 256 kolorów (tak zapisuje Paint). Pierwsze cztery kolory to: biały, czarny, czerwony, zielony. Pozostałe kolory to odcienie czerni. Sekwencja bitów globalnej palety będzie następująca:
11111111 11111111 11111111 00000000 00000000 00000000 11111111 00000000 00000000 00000000 11111111 00000000…

Po zaszyfrowaniu wiadomości podkreślone bity zostaną zastąpione bitami z wiadomości. Otrzymany obrazek prawie nie różni się od oryginału.
Oryginał
Obrazek z wbudowaną wiadomością

![]()
Metoda rozszerzania palety
Otwierając obrazek, w który wstawiono wiadomość w tym metodzie, można zobaczyć taki widok:

Jasne jest, że taka metoda nie nadaje się do profesjonalnej działalności szpiegowskiej i wymaga być może dodatkowego szyfrowania wiadomości.
Szyfrowanie/dekryptowanie w animowanych obrazach działa tak samo, jak w zwykłych statycznych obrazach, a animacja nie jest naruszona.
Źródła używane:
Pobierz:
Źródło: habr.com
