Как спрятать любые данные в PNG

Настало время открыть Америку!

Меня действительно удивило предельно малое кол-во информации на данную тему. Будем исправлять.

И так, сразу к делу! Что нам нужно знать, чтобы спрятать что-то внутри PNG картинки?

Нам нужно знать, что PNG внутри себя хранит информацию о каждом пикселе. В каждом пикселе в свою очередь 3 канала (R, G, B), которые описывают цвет и один альфа-канал, который описывает прозрачность.

3d755932d65b704138a3da4165a4bcdd.jpg

LSB (Least Significant Bit) — младшие биты, которые мы можем использовать для своих темных делишек. Их изменение повлечет незначительное изменение цвета, которое человеческий глаз не способен распознать.

77c0f430878b4cb4c3349229d590fb12.jpg

Нам лишь нужно привести «секретную информацию» к побитовому виду и пройтись по каждому каналу каждого пикселя, меняя LSB на нужный нам.

e1eaee5ca4dec22e7af4f55e87cea72b.jpg

Каждый пиксель может вмещать 3 бита информации. А значит. классическое «Hello world» на UTF-8 потребует 30 пикселей (изображение 6×6). Текст из 100 тыс слов поместится на 1000×1000. Хотим больше? Потенциальные 5 мб спонтанных данных разместятся на 5000×5000.

Теория понятна (надеюсь). Время практических примеров.

Кодируем наше сообщение внутри PNG:

import { PNG } from 'pngjs';
import fs from 'node:fs';

function writeData(imageBinary, dataBinary) {

   for (let i = 0, dataBitIndex = 0; i < imageBinary.length; i += 4) {

      for (let j = 0; j < 3; j++, dataBitIndex++) {

         if (dataBitIndex >= dataBinary.length * 8) {
            return imageBinary;
         }

         /**
          * Получаем текущий бит данных
          **/

         let bit = (dataBinary[Math.floor(dataBitIndex / 8)] >> (7 - (dataBitIndex % 8))) & 1;

         /**
          * Смещаем цвет
          **/
         imageBinary[i + j] = (imageBinary[i + j] & 0xFE) | bit;

      }

   }

   return imageBinary;

}

function async encode(inputPath, outputPath, message) {

   let binaryMessage = Buffer.from(message, 'utf-8');

   return new Promise(resolve => {

      /**
       * Открываем изображение и получаем его пиксели
       **/
      fs.createReadStream(inputPath)
         .pipe(new PNG())
         .on('parsed', function() {

            //this - Объект PNG
            //this.data - Объект Buffer, по сути [R, G, B, A, R, G, B, A...]

            /**
             * Запишем длинну сообщения в первые 4 байта
             **/
            let length = Buffer.alloc(4);
            length.writeUInt32BE(binaryMessage.length, 0);


            let binaryTotalData = Buffer.concat([
               length,
               binaryTotalData
            ]);

            /**
             * Заменяем пиксели
             **/
            writeData(this.data, binaryTotalData);

            /**
             * Сохраняем в файл
             **/
            let stream = fs.createWriteStream(outputPath);

            stream.on('finish', resolve);

            this.png.pack().pipe(stream);

         });

   });
}

Получаем сообщение из PNG:

function readMessage(dataBinary) {

   let bytes: number[] = [];

   for (let i = 0, dataBitIndex = 0, currentByte = 0; i < pixels.length; i += 4) {

      for (let j = 0; j < 3; j++) {

         let bit = pixels[i + j] & 1;

         currentByte = (currentByte << 1) | bit;
         dataBitIndex++;

         if (dataBitIndex % 8 === 0) {
            bytes.push(currentByte);
            currentByte = 0;
         }

      }

   }

   return Buffer.from(bytes);

}

function async decode(targetPath) {

   return new Promise(resolve => {

      /**
       * Открываем изображение и получаем его пиксели
       **/
      fs.createReadStream(targetPath)
         .pipe(new PNG())
         .on('parsed', function() {

            //this - Объект PNG
            //this.data - Объект Buffer, по сути [R, G, B, A, R, G, B, A...]

            /**
             * Читаем данные
             **/
            let binaryTotalData = = readData(this.data);

            /**
              * Узнаем длинну исходного сообщения и обрезаем
             **/
            let length = binaryTotalData.readUInt32BE();
            let binaryMessage = binaryTotalData.slice(4, 4 + length);

            resolve(binaryMessage);

         });

   });


}

53c05cfb277202b2c9b4e628b594077f.png

Дальше все зависит от вашей фантазии. Можно записать внутрь PNG другой файл, можно шифровать данные через AES, можно запрятать все свои пароли в фотографию с любимым вождем дядей.

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

Код более развернутого решения можно найти на GitHub: https://github.com/in4in-dev/png-stenography (использование AES, сокрытие файлов в картинке)

Всем спасибо. Пользуйтесь!

© Habrahabr.ru