Стеганография — сокрытие сообщений внутри сообщений (начало). Инструмент Typescript, созданный для сокрытия зашифрованных сообщений в изображениях (в основном мемах), вдохновленный просмотром многих шоу, похожих на «Древних пришельцев», с моим новорожденным.

Репозиторий проекта: https://github.com/AdamGuar/stoi

Мотивация
Смотрю очередной выпуск какого-то родового шоу о «неразгаданных тайнах». Низкий, жуткий и таинственный голос ведущего описывает, что круги на полях могут быть зашифрованными сообщениями, отправленными инопланетянами, спрятанными в геометрической форме — он называет это стеганографией. Стеганография…слово звучит красиво, интригующе. Особенно в 3 часа ночи. Я гуглил и по какой-то странной причине решил, что (из-за того, как сильно я люблю отправлять мемы своим друзьям) было бы неплохо иметь возможность отправлять зашифрованные текстовые сообщения в картинках. Странно, как человеческий мозг работает в 3 часа ночи. В любом случае, я хотел иметь какой-нибудь любимый проект в моем github, помимо некоторых моих проектов Uni, которые он содержит в настоящее время. И код, который я сделал тогда… о боже, там все еще есть некоторые программные решения, за которые мне стыдно до сих пор.

Кроме того, из-за всего этого настоящего криминала, древних инопланетян, сериалов, которые я смотрел, мне захотелось поработать над чем-то загадочным. Что-то, что выглядело бы круто, нуар… по крайней мере, в моей голове. На рисунке 1 показано, как я себя чувствовал, работая над этим «проектом».

Идея…
была очень проста:
Каждую строку можно преобразовать в байтовый массив, в принципе, на любом языке программирования. В TypeScript массив байтов был бы массивом (да) с целочисленными значениями от 0 до 255.
Я вспомнил, что в Uni, когда я писал еще одну программу, похожую на «Игру в жизнь», для некоторых классов, я использовал вещь под названием «Canvas API», встроенную в JavaScript API для рисования изображений. Одной из его особенностей была возможность рисовать прямоугольники и заполнять их цветами с помощью кодирования RGB. RGB... это звоночек... RGB использует 8 бит на цвет для кодирования цвета пикселя.
8 бит… один байт! Таким образом, каждое значение из массива текстовых байтов может быть помещено в качестве значения цвета с помощью Canvas API!

Первый набросок того, как поток для сокрытия сообщения в изображение начал формироваться.

Как показано на рис. 2, это было максимально просто. Пользователь отправлял текст, который нужно скрыть, информацию о том, где его скрыть, и само изображение. Самым важным был класс ImgProcessor. Canvas API может быть довольно сложным для работы. Фрагмент ниже показывает краткий пример того, как работать с Canvas. Это много операций. Мне нужна была какая-то обертка.

Я хотел иметь класс, который инкапсулировал бы все это. Это создаст
холст и контекст в качестве его свойств. Затем я могу работать над этими свойствами, добавляя методы к моему классу. Это будет выглядеть примерно так:

Метод loadImage инициирует объекты холста и контекста, используя размеры изображения. Оттуда я могу начать работу над дальнейшей обработкой созданного холста.

Переосмысление и чрезмерное усложнение.

Еще одна ночь, еще одно странное шоу. Больше мыслей об инструменте и почему первоначальная идея должна быть изменена.

  1. Как насчет расшифровки поиска текста на изображении?

Если я хочу скрыть текст в мемах, которые я так отчаянно рассылаю, я также хочу, чтобы у получателей была возможность их «найти» и расшифровать.
Мне нужен другой режим моей программы для чтения сообщений.

2. Может быть, я могу добавить шифрование текста, прежде чем помещать его в изображение?

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

Я хотел, чтобы мою стеганографию было так же трудно взломать, как фигуры на полях, сделанные инопланетянами! Решение было принято. Я добавляю шифрование текста.

3. Ключи шифрования и где их найти… Я имею в виду, как они будут выглядеть.

Рассмотрим простейший способ защиты передачи данных по ненадежному каналу:

  • Мы шифруем данные каким-то секретным ключом
  • Мы отправляем зашифрованные данные по ненадежному каналу получателю
  • Мы отправляем секрет получателю по защищенному каналу
  • Получатель расшифровывает данные с секретом

Но наш случай немного сложнее. Нам также нужно сообщить получателю, где на изображении (в каких x и y) и в каком цвете (красном, зеленом или синем) искать закодированное сообщение.

Поэтому я решил использовать пару «ключей»:

  • Открытый ключ — он будет содержать позиции пикселей и информацию о том, какой цвет будет использоваться для кодирования строки, а также случайный ключ шифрования, который будет использоваться для кодирования строки перед помещением ее в изображение. В коде это будет выглядеть так:
  • Закрытый ключ (secret) — секретная строка, которая будет использоваться для шифрования открытого ключа. Объект JSON, созданный из открытого ключа, будет зашифрован с использованием этого значения.

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

Только секретная фраза-пароль — закрытый ключ, необходимо отправлять безопасным способом.

4. Простой в использовании интерфейс командной строки

Поскольку не все мои потенциальные пользователи моего инструмента являются энтузиастами TypeScript, я хочу создать CLI из своего проекта. У него будет 3 режима:

  • keygen — для генерации открытого ключа
  • hide — для скрытия сообщений в изображениях
  • find — для скрытия сообщений в изображениях

Режимы должны переключаться с помощью входных параметров CLI с хорошим справочным сообщением, описывающим, как его использовать.

Окончательный дизайн приложения.

После нескольких ночей чрезмерного усложнения вещей в уме и еще нескольких предположений, почему Стоунхендж не был построен людьми ОДНИМИ, я, наконец, подготовил дизайн своего приложения для стеганографии. Я также понял, что мне лень рисовать эти потоки на диаграммах последовательности. Поэтому я решил нарисовать его, используя совершенно неопределенный тип диаграммы, показанной на рисунке 5.

Мое Приложение тоже получило имя, могучее!
S(secured) T(ext) O(ver. ) Я(маг)

Итак, теперь, когда я наконец понял, как будет выглядеть мое приложение, давайте перейдем к более интересным частям этой статьи. Куди.

Все еще скучно — настройка проекта.

Я инициализировал свой проект с помощью npm. Добавлен машинописный текст. Позже в процессе я добавил несколько зависимостей. Наиболее интересные части:

  • main и bin — указывают на точку входа моего скомпилированного приложения. Используется для создания CLI
  • скрипты — тестовые — для тестирования мокка — сборка только для компиляции приложения командой tsc — старт-скрыть; старт-найти; старт-скрытый для быстрого запуска приложения в нужном режиме с предварительно заполненными тестовыми данными для локального тестирования и отладки.

Вот так мой package.json выглядит в конце.

И tsconfig.json выглядит так:

Наконец-то интересное — index.ts — точка входа в приложение

Первая строка — сделать скомпилированный файл исполняемым. Это для функций CLI. Чтобы узнать больше о том, как сделать CLI из вашего приложения TypeScript, я рекомендую эту статью:



Теперь вернемся к index.ts. Я использую пакет command-line-args npm для получения входных параметров из командной строки. Это объект модели со свойствами, показанными под файлом index.ts. Существует также служебный класс InputParametersOptionsBuilder, который используется для возврата объекта option для commandLineArgs. Ничего особенного.

Давайте посмотрим, что на самом деле делает index.ts. Так:

  1. Инициирует runnersModeMap, который сообщает, какое средство запуска приложения должно «запускаться» в зависимости от режима, предоставленного пользователем. Если режим не указан, бегуном по умолчанию является HelpRunner.
  2. Он получает команду LineArgs
  3. Он запускает логику внутри выбранного бегуна с параметрами, предоставленными пользователем.
  4. Отображает статус запуска для пользователя.

Интерфейсы в index.ts

В index.ts используются два интерфейса.

  1. ApplicationRunner — используется для того, чтобы сделать приложение «открытым для модификаций». Если в будущем я захочу добавить еще один режим приложения, я просто создам новую реализацию ApplicationRunner и добавлю ее на карту.

2. Шифрование — интерфейс, который необходимо предоставить исполнителям, которые делают что-либо, связанное с шифрованием/дешифрованием строк. Я создал его на случай, если в будущем я захочу использовать другую реализацию шифрования (сейчас я использую cryptr, который является оберткой стандартной библиотеки шифрования узла).

Наконец, немного пикантности — KeyGenerationRunner

Этот бегун будет запускаться, когда пользователь запускает приложение в режиме кейгена. Он отвечает за генерацию открытого ключа шифрования с использованием секрета, предоставленного пользователем.

Как это работает:

  1. Он проверяет параметры, предоставленные пользователем, и проверяет, предоставлен ли каждый необходимый параметр. Если нет, он возвращает объект RunDetails со статусом fail и списком необходимых параметров.
  2. Это была забавная часть — создание списка деталей пикселей на основе границ, установленных пользователем. А затем поместить их в ключевой объект.
  3. Я генерирую случайную строку, которая будет использоваться в качестве ключа шифрования, и помещаю ее в объект ключа.
  4. И, наконец, я шифрую весь ключ, используя зависимость шифрования, и сохраняю открытый ключ в место, указанное входными параметрами.

Уроки, извлеченные здесь:

Сначала я только что создал цикл FOR, который шел от 0 до MAX_LENGTH
строки. Я сгенерировал случайные позиции x и y, придерживаясь пользовательских границ. Он работал нормально. Но потом я понял, что это может быть ошибка. Что, если я случайно сгенерирую один и тот же объект дважды? Он будет дважды добавлен к ключу, и некоторые части зашифрованного текста пропадут при добавлении дважды к одному и тому же пикселю.

Как это решить? Множество! Это гарантирует уникальность. Я просто состряпаю объекты и помещу их в Set. Затем я буду продолжать делать это до тех пор, пока Set не будет иметь размер MAX_LENGTH! Но подождите минутку… не совсем. Следующий объект:

{x: 1, y: 20, rgb: r} будет отличаться от {x: 1, y: 20, rgb: b}. Цвет отличается, но пиксель остается прежним. В итоге еще раз затираю тот же пиксель другим цветом. Часть текста может пропасть.

Хорошо, окончательное решение. Во времена, когда еще не было GPS, что всегда использовал мой папа, чтобы не разговаривать с незнакомцами, когда мы заблудились в отпуске? Он использовал КАРТУ! Я сделаю точно так же, см. рисунок 5! Ключ на карте будет строкой x и y позиции случайно сгенерированного пикселя. И вот как на самом деле создаются пирамиды… Я имею в виду, как я реализовал KeyGenerationRunner.js.

Идем дальше, кое-что спрячем — HideRunner

HideRunner будет отвечать за обработку режима приложения, когда пользователь хочет зашифровать некоторый текст на изображении. Для этого требуется большинство параметров, поскольку пользователю необходимо указать путь к открытому ключу (keyPath), входное изображение (imagePath), секрет (secret), текст для шифрования (text) и, возможно, путь к выходному изображению (imageOutPath). Для последнего, если он не указан по умолчанию, будет установлено значение ./encrypted.png..

Что делает моя программа здесь:

  1. Расшифровка открытого ключа с помощью секретного
  2. Шифрование текста для помещения его в изображение с использованием ключа шифрования из publicKey
  3. Разбор этого текста в массив байтов
  4. Загрузка объекта изображения на холст
  5. Добавление массива байтов в холст
  6. Сохранение изображения с принудительным расширением .png

Ничего особенного, все самое интересное происходит в классе-оболочке Canvas ImgProcessor, до которого мы скоро доберемся.

Ничего необычного, кроме пункта 6. Это ограничение было необходимо, чтобы пользователи не могли сохранять в jpeg или любых других форматах сжатия с потерями, поскольку они изменяют значения RGB в изображении, интерполируя некоторые значения. Имея выходное изображение, сжатое таким образом, мы не смогли бы прочитать правильные строки в режиме Find.

И последний бегун… FindRunner

Очевидно, для его запуска нам нужны некоторые параметры: путь открытого ключа, imagePath (который в данном случае будет изображением с зашифрованным текстом внутри), секрет для декодирования открытого ключа и необязательный параметр textOut (который скажет приложению, что оно либо должно сохранять зашифрованный текст в какой-нибудь файл или просто запросить его в консоли).

Когда дело доходит до того, как это работает, это в основном перевернутый HideRunner:

  1. Это расшифровка открытого ключа
  2. Загрузка изображения из параметров в обертку холста
  3. Получение байтов из изображения и преобразование их в строку
  4. Расшифровка зашифрованной текстовой строки
  5. Запрос/сохранение в файл расшифрованного текста.

Вуаля!

Оболочка холста — ImgProcessor

Мой учитель литературы в средней школе всегда говорил нам, что хорошо заканчивать свой рассказ тем, с чего вы его начали. Я сделаю что-то подобное здесь. Я начал свое путешествие по стеганографии с разговора о Canvas API и о том, как грязно использовать его без какой-либо оболочки. Вот почему я создал сервис ImgProcessor. По сути, он обертывает Canvas API, инициируя каждый требуемый объект Canvas как свойства объекта ImgProcessor. Благодаря этому в моих бегунах Hide и Find мне нужно было использовать только методы для добавления целочисленных (байтовых) массивов в холст и чтения их из холста соответственно. Полный код процессора изображений выглядит так:

Установка и использование интерфейса командной строки

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

  1. Во-первых, вам нужно клонировать репо: https://github.com/AdamGuar/stoi
  2. Затем в каталоге проекта запустите npm install или npm i
  3. Запустите npm run build или tsc для компиляции проекта.
  4. Наконец, запустите npm link, чтобы связать CLI локально.

Теперь вы можете вызывать код по имени проекта в командной строке:
stoi *input_paramater*

Ниже я привожу примеры вызовов для генерации открытого ключа, шифрования текста и последующей его расшифровки.

Adam@DESKTOP-DODLFR3 MINGW64 ~/Desktop/testDirectory
$ stoi --mode keygen --boundary 100x100 --publicKeyOut "./keys/pk.txt" --secret julka
Program execution completed
Status: OK
Details: Key generated, saved to location ./keys/pk.txt
Adam@DESKTOP-DODLFR3 MINGW64 ~/Desktop/testDirectory
$ stoi --mode hide --text "test text lala la la" --imagePath "./in/kot.jpeg" --imageOutPath "./out/encrypted.png" --secret julka --keyPath "./keys/pk.txt"
Program execution completed
Status: OK
Details: Text encrypted and save to ./out/encrypted.png
Adam@DESKTOP-DODLFR3 MINGW64 ~/Desktop/testDirectory
$ stoi --mode find --textOut "./out/decrypted.txt" --imagePath "./out/encrypted.png" --secret julka --keyPath "./keys/pk.txt"
Program execution completed
Status: OK
Details: Text decrypted from image: test text lala la la

Сводка

Было очень весело работать над сайд-проектом. Я научился создавать свой собственный интерфейс командной строки в Typescript. Я работал над чем-то совершенно отличным от моего обычного кода. И в течение всего этого процесса я смотрел множество вдохновляющих документальных фильмов на странных телеканалах. Кроме того, моя дочь стала спать лучше, чем до того, как я начал писать эту статью, так что есть большая вероятность, что мои следующие проекты будут более сплоченными.

Когда дело доходит до скрытия текста на изображениях, мне удалось собрать его воедино.
Наконец-то я могу отправлять зашифрованные сообщения в мемах своим друзьям! Есть еще несколько вещей и «дел», над которыми я хотел бы поработать в ближайшем будущем. Например, наличие STOI в виде пакета npm, улучшение скрытия байтов в пикселях, чтобы генерировать меньше артефактов в изображении, но на данный момент я вполне доволен этим.

Надеюсь, вам было весело читать это!

Постскриптум

Вот ссылка на несжатую фотографию совершенно случайных кругов на полях:

https://drive.google.com/file/d/1tzm1o6uSkvps-A12jkUpTFNlqK55s5Sk/view

Вот ссылка на открытый ключ:

https://drive.google.com/file/d/1NWsIkBcGDffKpdw_P6FcrcKE_sEWQfQN/view?usp=sharing

Помнить! Секрет есть секрет.

Если вы разгадаете загадку, вставьте зашифрованный текст в комментарии!

Это все, друзья!