Как спрятать любые данные в 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, сокрытие файлов в картинке)
Всем спасибо. Пользуйтесь!