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

от автора

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

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


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

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

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

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

Каждый пиксель может вмещать 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);           });     });   }

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

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/861932/