Вітаю.
Нещодавно, коли навчався в університеті, була курсова з дисципліни «Програмні методи захисту інформації». За завданням потрібно було створити програму, яка впроваджує повідомлення у файли формату GIF. Вирішив робити на Java.
У цій статті я опишу деякі теоретичні моменти, а також як створювалася ця невелика програма.
Теоретична частина
Формат GIF
GIF (англ. Graphics Interchange Format – формат для обміну зображеннями) – формат зберігання графічних зображень, здатний зберігати стислі дані без втрати якості у форматі до 256 кольорів. Цей формат був розроблений у 1987 році (GIF87a) фірмою CompuServe для передачі растрових зображень мережами. У 1989-му формат був модифікований (GIF89a), було додано підтримку прозорості та анімації.
Файли формату GIF мають блокову структуру. Дані блоки завжди мають фіксовану довжину (або залежить від деяких прапорів), так що помилитися в тому, де який блок знаходиться, практично неможливо. Структура найпростішого неанімованого GIF-зображення формату GIF89a:
З усіх блоків структури в даному випадку нас цікавитимуть блок глобальної палітри та параметри, які відповідають за палітру:
CT - Наявність глобальної палітри. Якщо цей прапор встановлений, відразу після дескриптора логічного екрану повинна починатися глобальна палітра.
Size - Розмір палітри і кількість кольорів картинки. Значення цього параметра:
Розмір
Кількість кольорів
Розмір палітри, байт
7
256
768
6
128
384
5
64
192
4
32
96
3
16
48
2
8
24
1
4
12
0
2
6
Методи шифрування
Як методи зашифрування повідомлень у файлах зображень будуть використовуватися:
Метод LSB (Least Significant Bit, найменший значний біт)
Метод доповнення палітри
Метод LSB - Поширений метод стеганографії. Він полягає в заміні останніх значущих біт в контейнері (у нашому випадку байти глобальної палітри) на біти повідомлення, що приховується.
У програмі будуть використовуватися в рамках цього методу два останні біти в байтах глобальної палітри. Це означає, що для 24-бітного зображення, де колір палітри є три байти для червоного, синього, і зеленого кольорів, після введення повідомлення в нього, кожна складова кольору зміниться максимум на 3/255 градації. Така зміна, по-перше, буде непомітною або важко помітною для людського ока, а по-друге, не буде помітною на низькоякісних пристроях виведення інформації.
Кількість інформації безпосередньо залежатиме від розміру палітри зображення. Оскільки максимальний розмір палітри 256 кольорів і якщо записувати по два біти повідомлення в складову кожного кольору, то максимальна довжина повідомлення (при максимальній палітрі в зображенні) становить 192 байти. Після введення повідомлення у зображення розмір файлу не змінюється.
Метод розширення палітрипрацює тільки для структури GIF. Він буде найбільш ефективним у зображеннях з палітрою невеликих розмірів. Суть його у тому, що він збільшує розмір палітри, цим давши додатковий простір для запису необхідних байт дома байт квітів. Якщо врахувати що мінімальний розмір палітри становить 2 кольори (6 байт), то максимальний розмір повідомлення може бути 256×3–6=762 байт. Недолік — низька криптозахищеність, прочитати впроваджене повідомлення можна за допомогою будь-якого текстового редактора, якщо повідомлення не було піддане додатковому шифруванню.
Практична частина
Проектування програми
Усі необхідні інструменти для реалізації алгоритмів шифрування та дешифрування будуть у пакеті com.tsarik.steganography. Цей пакет включає інтерфейс Encryptor з методами encrypt и decrypt, клас Binary, що надає можливість роботи з масивами бітів, а також класи винятків UnableToEncryptException и UnableToDecryptException, які повинні бути використані в методах інтерфейсу Encryptor у разі помилок кодування та декодування відповідно.
Основний пакет програми com.tsarik.programs.gifed буде включати запускається клас програми зі статичним методом main, що дозволяє запускати програму; клас, що зберігає параметри програми; та пакети з іншими класами.
Реалізація безпосередньо самих алгоритмів буде представлена у пакеті com.tsarik.programs.gifed.gif класами GIFEncryptorByLSBMethod и GIFEncryptorByPaletteExtensionMethod. Обидва ці класи будуть реалізувати інтерфейс Encryptor.
На основі структури формату GIF можна скласти загальний алгоритм впровадження повідомлення на палітру зображення:
Для визначення присутності повідомлення у зображенні необхідно до початку повідомлення додавати певну послідовність біт, яку дешифратор зчитує насамперед і перевіряє на коректність. Якщо вона не збігається, то вважається, що у зображенні немає прихованого повідомлення. Далі треба зазначати довжину повідомлення. Потім сам текст повідомлення.
Діаграма класів всієї програми:
Реалізація програми
Реалізацію всієї програми можна розбити на дві складові: реалізація методів шифрування та дешифрування інтерфейсу Encryptor, у класах GIFEncryptorByLSBMethod и GIFEncryptorByPaletteExtensionMethod, та реалізація інтерфейсу користувача.
Розглянемо клас GIFEncryptorByLSBMethod.
Поля firstLSBit и secondLSBit містять номери бітів кожного байта зображення, в які має заноситись та звідки зчитуватися повідомлення. Поле checkSequence зберігає контрольну послідовність біт забезпечення розпізнавання вбудованого повідомлення. Статичний метод getEncryptingFileParameters повертає параметри вказаного файлу та характеристики потенційного повідомлення.
Алгоритм методу encrypt класу GIFEncryptorByLSBMethod:
І його код:
@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");
}
// read bytes from input file
byte[] bytes = new byte[(int)in.length()];
InputStream is = new FileInputStream(in);
is.read(bytes);
is.close();
// check format
if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
throw new UnableToEncodeException("Input file has wrong GIF format");
}
// read palette size property from first three bits in the 10-th byte from the file
byte[] b10 = Binary.toBitArray(bytes[10]);
byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
// calculate color count and possible message length
int bOrigColorCount = (int)Math.pow(2, bsize+1);
int possibleMessageLength = bOrigColorCount*3/4;
int possibleTextLength = possibleMessageLength-2;// one byte for check and one byte for message length
if (possibleTextLength < text.length()) {
throw new UnableToEncodeException("Text is too big");
}
int n = 13;
// write check sequence
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++;
}
// write text length
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++;
}
// write message
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++;
}
}
// write output file
OutputStream os = new FileOutputStream(out);
os.write(bytes);
os.close();
}
Алгоритм та вихідний код методу decrypt класу GIFEncryptorByLSBMethod:
@Override
public String decrypt(File in) throws UnableToDecodeException, NullPointerException, IOException {
if (in == null) {
throw new NullPointerException("Input file is null");
}
// read bytes from input file
byte[] bytes = new byte[(int)in.length()];
InputStream is = new FileInputStream(in);
is.read(bytes);
is.close();
// check format
if (!(new String(bytes, 0, 6)).equals("GIF89a")) {
throw new UnableToDecodeException("Input file has wrong GIF format");
}
// read palette size property from first three bits in the 10-th byte from the file
byte[] b10 = Binary.toBitArray(bytes[10]);
byte bsize = Binary.toByte(new byte[] {b10[0], b10[1], b10[2]});
// calculate color count and possible message length
int bOrigColorCount = (int)Math.pow(2, bsize+1);
int possibleMessageLength = bOrigColorCount*3/4;
int possibleTextLength = possibleMessageLength-2; // one byte for check and one byte for message length
int n = 13;
// read check sequence
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("There is no encrypted message in the image (Check sequence is incorrect)");
}
// read text length
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("Decoded text length is less than 0");
}
if (possibleTextLength < textLength) {
throw new UnableToDecodeException("There is no messages (Decoded message length (" + textLength + ") is less than Possible message length (" + possibleTextLength + "))");
}
// read text bits and make text bytes
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);
}
Реалізація класу GIFEncryptorByPaletteExtensionMethod буде аналогічною, тільки відрізняється метод збереження/зчитування інформації.
У класі MainFrame описані методи-«обгортки»: encryptImage(Encryptor encryptor) и decryptImage(Encryptor encryptor), що обробляють результати методів інтерфейсу Encryptor та здійснюють взаємодію з користувачем, тобто відкривають діалог вибору файлів, показують повідомлення про помилки тощо; а також інші методи: openImage(), що дає можливість користувачеві вибору зображення, exit(), що здійснює вихід із програми. Ці методи викликаються з Action'ів відповідних пунктів меню. У цьому класі додатково реалізовано допоміжні методи: createComponents() - Створення компонентів форми, loadImageFile(File f) — завантаження зображення до спеціального компонента з файлу. Реалізація класу GIFEncryptorByPaletteExtensionMethod аналогічна реалізації класу GIFEncryptorByLSBMethod, основна відмінність полягає у способі запису та читанні байтів повідомлення з палітри.
Робота програми
Метод LBS
Допустимо є таке зображення:
У цьому зображенні панель складається з 256 кольорів (так зберігає Paint). Перші чотири кольори: білий, чорний, червоний, зелений. Інші кольори - чорні. Послідовність біт глобальної палітри буде така: