paint-brush
Стеганография: как скрыть текст на изображениях с помощью JavaScriptк@andriiromasiun
1,278 чтения
1,278 чтения

Стеганография: как скрыть текст на изображениях с помощью JavaScript

к Andrii Romasiun9m2024/06/26
Read on Terminal Reader

Слишком долго; Читать

Стеганография — это метод сокрытия секретных сообщений в другом, несекретном файле. Пользователи могут загрузить изображение, чтобы прочитать скрытое в нем сообщение, или самостоятельно закодировать сообщение в изображении. В этой статье описывается, как реализовать такой механизм кодирования с помощью JavaScript.
featured image - Стеганография: как скрыть текст на изображениях с помощью JavaScript
Andrii Romasiun HackerNoon profile picture
0-item

Что такое стеганография?

Представьте, что вы хотите отправить секретное сообщение другу, но канал, который вы хотите использовать, взломан и контролируется. Вы могли бы использовать какое-нибудь шифрование, но это вызовет подозрения у людей, отслеживающих ваши разговоры, поэтому вам придется использовать что-то другое.

В настоящее время стеганография — это метод сокрытия секретных сообщений в другом, несекретном файле (например, изображении кошки), чтобы, если вы отправите этот файл, он не был бы обнаружен. Стеганография не ограничивается сокрытием текста в изображениях и обычно означает «сокрытие секретной информации в другом несекретном сообщении или физическом объекте»: вы можете скрыть некоторые сообщения в аудио, видео или других текстах, используя, например, столбчатое транспонирование.


Стеганография также может быть чрезвычайно полезна во многих других случаях, например, она может стать хорошей альтернативой водяным знакам в конфиденциальных документах для защиты их от утечки.


Существует множество способов скрыть информацию на изображениях: от простого добавления текста в конец файла до его сокрытия в метаданных. В этой статье я хочу осветить более продвинутый метод стеганографии, спускающийся на двоичный уровень и скрывающий сообщения внутри границ самого изображения.

Создание стеганографического движка

Пользовательский интерфейс

Для моего примера стеганографии я решил использовать JavaScript, поскольку это мощный язык программирования, который можно выполнять в браузере.


Я придумал простой интерфейс, который позволяет пользователям загружать изображение, чтобы прочитать скрытое в нем сообщение или самостоятельно закодировать сообщение в изображении.


 <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Steganography</title> <style> body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; } textarea { width: 300px; height: 100px; } button { margin: 10px; } #outputImage { margin-top: 20px; max-width: 100%; } </style> </head> <body> <h1>Steganography Example</h1> <input type="file" id="upload" accept="image/*"><br> <canvas id="canvas" style="display:none;"></canvas><br> <textarea id="text" placeholder="Enter text to encode"></textarea><br> <button id="encode">Encode Text</button> <button id="decode">Decode Text</button> <p id="decodedText"></p> <img id="outputImage" alt="Output Image"> <script src="./script.js"></script> </body> </html>


Чтобы использовать его, пользователи могут просто выбрать изображение, которым они хотят манипулировать, и либо попытаться декодировать из него некоторый текст, либо закодировать его и загрузить изображение позже.



Обработка изображений

Для работы с изображениями в JavaScript мы можем использовать Canvas API . Он предоставляет множество различных функций для управления и рисования изображений, анимации или даже игровой графики и видео.


Canvas API в основном используется для 2D-графики. Если вы хотите работать с 3D-графикой с аппаратным ускорением, вы можете использовать API WebGL (который, кстати, также использует элемент <canvas>).


 const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const image = new Image();


Чтобы прочитать файл изображения из файловой системы и добавить его в контекст холста, мы можем использовать FileReader API . Это позволяет нам легко читать содержимое любого файла, хранящегося на компьютере пользователя, без необходимости использования специальной библиотеки.

 function handleFileUpload(event) { const reader = new FileReader(); reader.onload = function (e) { image.src = e.target.result; image.onload = function () { canvas.width = image.width; canvas.height = image.height; ctx.drawImage(image, 0, 0); }; }; reader.readAsDataURL(event.target.files[0]); }


Он считывает файл и рисует изображение из этого файла в нашем ранее определенном контексте 2D-холста, после чего мы можем либо закодировать некоторый текст в это изображение, либо попытаться прочитать текст из изображения.

Скрытие текста в изображениях

Изображения состоят из пикселей, и каждый пиксель содержит информацию о своих цветах. Например, если изображение закодировано с использованием модели RGBA, каждый пиксель будет содержать 4 байта информации о том, сколько красного, зеленого, синего и альфа (непрозрачности) он представляет.


Чтобы закодировать некоторый текст в изображении, мы могли бы использовать один из этих каналов (например, альфа-канал). Поскольку эта информация представлена в двоичной системе (например, 01001100), мы можем переключить последний бит на то, что нам нужно. Он называется наименее значащим битом (LSB), и его изменение приводит к минимальным изменениям в самом изображении, что делает его неотличимым от человеческого.


Теперь представьте, что у нас есть текст типа «Привет», и мы хотим закодировать его в изображение. Алгоритм для этого будет


  1. Преобразуйте текст «Привет» в двоичный.


  2. Переберите байты данных изображения и замените младший бит этих байтов битом двоичного текста (каждый пиксель содержит 4 байта данных для каждого цвета, в моем примере я хочу изменить канал непрозрачности изображения). , поэтому я бы повторял каждый 4-й байт).


  3. Добавьте нулевой байт в конце сообщения, чтобы при декодировании мы знали, когда остановиться.


  4. Примените измененные байты изображения к самому изображению.


Во-первых, нам нужно взять у пользователя текст, который мы хотим закодировать, и выполнить с ним некоторые базовые проверки.

 const text = document.getElementById("text").value; if (!text) { alert("Please enter some text to encode."); return; }


Затем нам нужно преобразовать текст в двоичный формат и создать холст изображения, в которое мы собираемся закодировать этот текст.

 const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let binaryText = ""; for (let i = 0; i < text.length; i++) { let binaryChar = text.charCodeAt(i).toString(2).padStart(8, "0"); binaryText += binaryChar; }


Для этого мы можем просто перебрать каждый из символов и получить индекс Unicode, используя функцию charCodeAt . Затем этот Юникод преобразуется в двоичный и дополняется так, чтобы его длина была такой же, как у любого другого символа.


Например, буква «H» представлена в Юникоде как 72; затем мы преобразуем это число в двоичный формат (1001000) и добавляем 0 в начале (01001000), чтобы убедиться, что все буквы будут одинаковой длины (8 бит).


Затем нам нужно добавить нулевой байт в конце сообщения, чтобы быть уверенными, что при его расшифровке мы сможем отличить реальный текст от случайных пиксельных данных изображения.

 binaryText += "00000000";


Затем нам нужно выполнить базовую проверку, чтобы убедиться, что изображение имеет достаточно пикселей для кодирования нашего сообщения и не допускает его переполнения.

 if (binaryText.length > data.length / 4) { alert("Text is too long to encode in this image."); return; }


А затем наступает самое интересное — кодирование сообщения. Массив данных, который мы определили ранее, содержит информацию о пикселях в виде значений RGBA для каждого пикселя изображения. Итак, если изображение закодировано RGBA, каждый пиксель в нем будет представлен четырьмя значениями массива данных; каждое значение показывает, сколько красного, зеленого и синего имеет этот пиксель.

 for (let i = 0; i < binaryText.length; i++) { data[i * 4] = (data[i * 4] & 0b11111110) | parseInt(binaryText[i]); } ctx.putImageData(imgData, 0, 0); const outputImage = document.getElementById("outputImage"); outputImage.src = canvas.toDataURL();


В приведенном выше коде мы перебираем текст в двоичном коде. data[i * 4] находит байт, который нам нужно изменить, и поскольку мы хотим изменить только байты определенного канала, мы умножаем переменную i на 4 чтобы получить к ней доступ.


Операция data[i * 4] & 0b11111110 устанавливает младший бит в 0 . Например, если data[i * 4] равно 10101101 в двоичном формате, то результатом операции 10101101 & 11111110 будет 10101100 . Это гарантирует, что младший бит будет установлен в 0 , прежде чем мы приступим к дальнейшим манипуляциям с ним.


parseInt(binaryText[i]) — это текущий бит двоичной строки; это либо 1 , либо 0 . Затем мы можем установить этот бит в младший разряд, используя побитовую операцию ИЛИ ( | ). Например, если левая часть побитового ИЛИ равна 10101100 , а binaryText[i] равен 1 , то 10101100 | 00000001 приведет к 10101101 . Если бы текущий бит был равен 0 , результатом OR было бы 10101100 . Вот почему нам пришлось в первую очередь удалить LSB.


Как только сообщение закодировано, мы можем поместить его на текущий холст и отобразить в HTML с помощью метода canvas.toDataURL .



Расшифровка скрытых сообщений из изображений

Процесс декодирования изображения на самом деле намного проще, чем кодирование. Поскольку мы уже знаем, что закодировали только альфа-канал, мы можем просто перебрать каждый 4-й байт, прочитать последний бит, объединить его в окончательную строку и преобразовать эти данные из двоичного кода в строку Unicode.


Сначала нам нужно инициализировать переменные. Поскольку imgData уже заполнена информацией об изображении (мы вызываем ctx.drawImage каждый раз, когда читаем файл из файловой системы), мы можем просто извлечь ее в переменную данных.


 const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let binaryText = ""; let decodedText = "";


Затем нам нужно перебрать каждый 4-й байт изображения, прочитать последний бит и объединить его с переменной binaryText .

 for (let i = 0; i < data.length; i += 4) { binaryText += (data[i] & 1).toString(); }


data[i] — это закодированный байт, и для извлечения младшего разряда мы можем использовать побитовый оператор AND ( & ). Он принимает два значения и выполняет операцию И над каждой парой соответствующих битов. Сравнивая data[i] с 1 , мы по сути изолируем младший бит из информации о пикселе, и если младший бит равен 1 , то результатом такой операции будет 1 . Если младший бит равен 0 , результат также будет 0 .


После того, как мы прочитали все младшие разряды и сохранили их в binaryText , нам нужно преобразовать их из двоичного формата в обычный текст. Поскольку мы знаем, что каждый символ состоит из 8 бит (помните, как мы использовали padStart(8, "0") чтобы сделать каждый символ одинаковой длины?), мы можем выполнять итерацию для каждого 8-го символа binaryText .


Затем мы можем использовать операцию .slice() для извлечения текущего байта из binaryText на основе нашей итерации. Двоичную строку можно преобразовать в число с помощью функции parseInt(byte, 2) . Затем мы можем проверить, равен ли результат 0 (нулевой байт) — мы останавливаем преобразование и запрашиваем результат. В противном случае мы можем найти, какой символ соответствует номеру Юникода, и добавить его в нашу строку результата.


 for (let i = 0; i < binaryText.length; i += 8) { let byte = binaryText.slice(i, i + 8); if (byte.length < 8) break; // Stop if the byte is incomplete let charCode = parseInt(byte, 2); if (charCode === 0) break; // Stop if we hit a null character decodedText += String.fromCharCode(charCode); }


Декодированный текст затем можно безопасно отобразить пользователю:

 document.getElementById("decodedText").textContent = decodedText; 



Я оставил полный код, используемый в этой статье, в своем репозитории GitHub ; не стесняйтесь играть с этим. Есть много вещей, которые можно улучшить :)

Последние мысли

Стеганография — очень мощный метод, и ее можно применять во множестве различных случаев использования, начиная от проверки документов, предотвращения утечек, проверки изображений AI, управления DRM музыкальных файлов и многих других. Эту технику можно применять даже к видео, играм и даже необработанному тексту, поэтому я думаю, что у нее огромный потенциал.

В эпоху NFT и блокчейнов еще интереснее посмотреть, как они найдут свои варианты использования и как будет развиваться эта технология.