На написание данной статьи меня вдохновил один случай, произошедший с одной женщиной. Дело в том, что она является цифровым художником и создает неплохие изображения, которые нравятся очень многим людям.
Однако, однажды она заметила, что ее дебютная работа была использована одной нечистой на руку фирмой в рекламных целях и без заключения с самой художницей какого-либо делового соглашения. Все это вылилось в своеобразное разбирательство, которое как и всегда, не обошлось без судебного вмешательства…
И тут встал интересный вопрос: как доказать всем (в том числе, и судебным работникам) свое авторство или некоторое участие в работе относительно некоторого произведения?
Давайте предположим, что самое лучшее доказательство авторства для изображения, это само данное изображение. Но, стиль, использованный в создании изображения, не является объективным критерием, а множество действительно первоклассных художников умеют неплохо подстраиваться под практически любой стиль, с легкостью его копируя. Также, множество цифровых художников очень любят оставлять в своих работах разного рода скрытые сообщения, увидеть которые могут лишь «избранные» или особо сообразительные почитатели цифрового таланта…
Так вот почему бы не сделать скрытые сообщения более привычными и более доступными для обычных людей?
Для этого существует способ сокрытия данных в медиа-контейнерах, таких как изображения, и называется данный способ стеганографией. Стеганография, в отличие от криптографии, не скрывает сообщение, но скрывает сам факт наличия сообщения. Данный метод, как нельзя лучше подходит для создания цифровой подписи художника, поскольку он практически не изменяет оригинального медиа-контейнера и не порождает визуальных отличий для изображений содержащих скрытый текст.
В данной статье мы будем использовать сокрытие текстовой информации в младших байтах цветового представления пикселей изображения, поскольку изменение младших байтов в цветовой модели RGB не приведет к созданию заметного человеческим глазом визуального отличия, несмотря на то, что таковое будет иметь место.
Для наших экспериментов с изображениями будем использовать dlib, в которой есть все необходимое для реализации простой стеганографии. Для начала реализуем сокрытие байтов сообщение внутри пикселей изображения, для чего один байт сообщения будем скрывать в двух пикселях, записывая в младшие биты красного и зеленого каналов каждого из пикселей двойки битов из байта сообщения.
Эту идею можно реализовать с помощью битовых операций:
// спрятать байт в цвете Color4f[] setWith(Color4f color1, Color4f color2, ubyte unit) { int R1 = (cast(int) (color1.r * 255)); int G1 = (cast(int) (color1.g * 255)); int R2 = (cast(int) (color2.r * 255)); int G2 = (cast(int) (color2.g * 255)); // first half of byte auto r1 = (R1 & 0b11111100) | (unit & 0b00000011); auto g1 = (G1 & 0b11111100) | ((unit & 0b00001100) >> 2); // second half of byte auto r2 = (R2 & 0b11111100) | ((unit & 0b00110000) >> 4); auto g2 = (G2 & 0b11111100) | ((unit & 0b11000000) >> 6); return [ Color4f(r1 / 255.0, g1 / 255.0, color1.b), Color4f(r2 / 255.0, g2 / 255.0, color2.b), ]; }
Извлечение скрытой информации из пикселей также несложно реализовать, просто нужно исходить из тех же положений, что и при сокрытии, но нужно извлечь двойки битов, совместив их в один байт:
// извлечь байт из цвета ubyte getWith(Color4f[] colors) { ubyte result = 0; int x0 = (cast(int) (colors[0].r * 255)); int x1 = (cast(int) (colors[0].g * 255)); int x2 = (cast(int) (colors[1].r * 255)); int x3 = (cast(int) (colors[1].g * 255)); auto r = x0 & 0b00000011; auto g = x1 & 0b00000011; auto b = x2 & 0b00000011; auto a = x3 & 0b00000011; result |= r; result |= (g << 2); result |= (b << 4); result |= (a << 6); return result; }
Поскольку на каждый байт сообщения, которое мы прячем в картинку, необходимо два пикселя, то несложно предположить, сколько информации мы можем утаить в картинке: как минимум, длина собщения, умноженная на два, должна быть меньше, чем количество пикселей в изображении, которое можно понимать как длину изображения в пикселях умноженную на ширину изображения в пикселях.
Таким образом, реализация сокрытия сообщения в пикселях, довольна проста: сделаем проход по байтам сообщения в цикле и исходя из индекса каждого байта рассчитаем индексы для двух пикселей, в которые будем подмешивать информацию из сообщения:
// добавить сообщение в изображение auto addMessage(SuperImage superImage, ubyte[] message) { auto width = superImage.width; auto height = superImage.height; SuperImage newImage = superImage.dup; if ((message.length * 2) > (width * height)) { throw new Exception("Unable to add message: message length less than number of pixels"); } for (int i = 0; i < message.length; i++) { auto N = i * 2; auto x0 = N / width; auto y0 = N % height; auto x1 = (N + 1) / width; auto y1 = (N + 1) % height; auto colors = setWith(superImage[x0, y0], superImage[x1,y1], message[i]); newImage[x0, y0] = colors[0]; newImage[x1, y1] = colors[1]; } return newImage; }
Извлечение сообщения будем производить так:
// извлечь сообщение auto extractMessage(SuperImage superImage) { auto width = superImage.width; auto height = superImage.height; ubyte[] message; for (int i = 0; i < ((width * height) / 2); i++) { auto N = i * 2; auto x0 = N / width; auto y0 = N % height; auto x1 = (N + 1) / width; auto y1 = (N + 1) % height; message ~= getWith([superImage[x0, y0], superImage[x1,y1]]); } return message; }
Единственный минус данного подхода по извлечению информации в том, что нам неизвестна длина исходного сообщения, а потому, требуется некоторая априорная информация о длине сообщения, так как функция вернет нам массив байтов, полученных из всего изображения.
Однако, данные о длине сообщения, могут быть сохранены у авторы изображения, как часть некоего «ключа» сообщения, которая может быть дополнительным свидетельством авторства, хоть и такое свидетельство очень слабое.
Давайте возьмем какую-нибудь картинку и испытаем наши функции…
Исходная картинка «смеющийся гепард» (на самом деле, зевающий):
Код для испытания:
void main() { import std.stdio; import std.string; auto img = load("gepard2.png"); auto bytes = cast(ubyte[]) "Команда LightHouse Software".representation; img .addMessage(bytes) .savePNG("gepard_with_message.png"); auto img2 = load("gepard_with_message.png"); auto message = extractMessage(img2); writeln(cast(string) message[0..34]); }
Результат:
Сообщение, извлеченное из гепарда:
Performing "debug" build using dmd for x86_64. dlib 0.12.2: target for configuration "library" is up to date. digital_sign ~master: building configuration "application"... Linking... To force a rebuild of up-to-date targets, run again with --force. Running ./digital_sign Команда LightHouse Software
Как видите, стеганографию реализовать крайне просто, а ее возможности могут быть использованы для реализации новых схем цифровых доказательств.
Но, к сожалению, такой тип сокрытия данных легко обнаруживается при помощи довольно простых методов цифровой обработки изображения (вы можете сами легко это проверить, рассмотрев гистограммы исходного и обработанного с помощью стеганографии изображений) и кроме того, изображение с сообщением крайне нестойко по отношению к методам пост-обработки изображения (таким как, масштабирование и фильтрация).
Я также вижу, что отсутствие какого-либо метода шифрования подписи, делает описанный метод очень слабым, оставляя его лишь простейшей демонстрационной моделью, которая может послужить как в образовательных целях, так и, при соответствующих улучшениях, для вполне реальных продуктов…