Настало время открыть Америку!
Меня действительно удивило предельно малое кол-во информации на данную тему. Будем исправлять.
И так, сразу к делу! Что нам нужно знать, чтобы спрятать что-то внутри 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/
Добавить комментарий