
Всем привет! Меня зовут Егор, я фронтенд-разработчик в Райффайзенбанке. В этой статье я хочу показать, как благодаря типизированным массивам мы можем взаимодействовать с бинарными данными в браузере. В качестве примера мы напишем приложение для шифрования текста внутрь изображения и посмотрим, как работают типизированные массивы.
Введение
Ни для кого не секрет, что компьютер обрабатывает данные в бинарном формате, где каждый бит указывает на наличие или отсутствие электрического сигнала. Для того, чтобы отобразить текстовые данные, мы передаем последовательность битов компьютеру, а он с помощью специальных утилит переводит ее в понятный человеку символ.
При обработке графических данных компьютеру тоже поступают сигналы, где последовательность из трех байт (или четырех, при наличии альфа-канала) является значением цвета RGB (RGBA). При разработке приложения мы будем взаимодействовать с BMP-форматом, но аналогичное возможно и с любым другим форматом файлов.
Для начала определим, что должно уметь наше приложение:
-
Кодировать текстовое сообщение в файл. При этом вес, структура и визуальное отображение файла не должны измениться.
-
Расшифровывать текстовое сообщение.
Демонстрация работы приложения
Работа с изображением
Для реализации приложения нам понадобится описание BMP-формата, которое достаточно подробно описано в «Википедии».
Описание заголовка BITMAPCOREHEADER
|
Смещение |
Размер (байты) |
Описание |
|
0 |
2 |
Отметка для отличия формата от других (сигнатура формата). Возможные значения: BM, BA, CI, CP, IC, PT |
|
2 |
4 |
Размер файла в байтах |
|
6 |
2 |
Зарезервированы. Должны содержать ноль |
|
8 |
2 |
|
|
10 |
4 |
Начальный адрес байта, в котором могут быть найдены данные растрового изображения (bitmap data) |
Что мы узнаем из описания этого формата?
По соображениям совместимости большинство приложений используют старые заголовки DIB для сохранения файлов. Поскольку OS / 2 больше не поддерживается после Windows 2000, на данный момент распространенным форматом Windows является заголовок BITMAPINFOHEADER
Поэтому во внимание берем только заголовок BITMAPINFOHEADER и сигнатуру BM.
Описание заголовка BITMAPINFOHEADER
|
Смещение |
Размер (байты) |
Описание |
|
14 |
4 |
Размер заголовка |
|
18 |
4 |
Ширина растрового изображения в пикселях (целое число со знаком) |
|
22 |
4 |
Высота растрового изображения в пикселях (целое число со знаком) |
|
26 |
2 |
Количество цветовых плоскостей. В BMP допустимо только значение 1 |
|
28 |
2 |
Количество бит на пиксель |
|
30 |
4 |
Используемый метод сжатия |
|
34 |
4 |
Размер пиксельных данных в байтах |
|
38 |
4 |
Количество пикселей на метр по горизонтали |
|
42 |
4 |
Количество пикселей на метр по вертикали |
|
46 |
4 |
Количество цветов в цветовой палитре |
|
50 |
4 |
Количество ячеек от начала цветовой палитры до последней используемой (включая её саму). |
Для начала создаем класс, отвечающий за работу с ArrayBuffer, полученным из изображения. После этого проверяем соответствие BMP-формату:
class BmpParser { #BMP_HEADER_FIELD = 'BM' #view #decoder constructor(buffer) { this.#view = new DataView(buffer) this.#decoder = new TextDecoder() this.#checkHeaderField() } #checkHeaderField() { if (this.#decoder.decode(new Uint8Array(this.#view.buffer, 0, 2)) !== this.#BMP_HEADER_FIELD) { throw new Error('Ожидается .bmp файл!') } } }
Теперь нам необходимо узнать смещение, где может быть найден массив пикселей (bitmap data). В таблице указан размер в 4 байта, поэтому мы используем маску Uint32Array, так как 1 байт равен 8 бит:
class BmpParser { // ... get offsetBits() { return this.#view.getUint32(10, true) } // ... }
Чтобы проверить, что текстовое сообщение не превышает размер пиксельных данных, мы будем использовать размер bitmap data. Его можно получить из заголовка BITMAPINFOHEADER:
class BmpParser { // ... get bitmapDataSize() { return this.#view.getUint32(34, true) } // ... }
Это все данные, которые необходимо получить из изображения.
Шифрование
Алгоритм для шифрования сообщения:
-
Конверуем текстовое сообщение в бинарный код. Возможные значения: «1», «0», «,».
-
Поочередно рассматриваем каждый символ:
-
Если этот символ имеет значение 0 или 1, оставляем без изменений.
-
Если этот символ имеет значение «,» — устанавливаем ему значение 2. Это будет свидетельствовать, что предыдущую последовательность нулей и единиц можно собрать в символ из зашифрованного сообщения.
-
Записать полученное число в bitmap data.
-
-
Добавить точку выхода, в нашем случае — значение «3».
Приступим к реализации.
Создаем класс, отвечающий за шифрование текстового сообщения внутрь изображения, добавив метод конвертации текста в бинарный код:
class Encryptor { // Текущее смещение битов в ArrayBuffer #offset = 0 #view #encryptionText #bmpParser constructor(buffer, encryptionText) { this.#view = new DataView(buffer) this.#encryptionText = encryptionText this.#bmpParser = new BmpParser(buffer) } // Конвертирует текст в бинарный код // Тест => ["10000100010", "10000110101", "10001000001", "10001000010"] encode(value) { return value.split('').map(char => char.charCodeAt(0).toString(2)) } }
Добавим проверку, что длина сообщения не превышает размер изображения в байтах:
class Encryptor { // ... #checkPhraseLength(binaryLength) { if (binaryLength >= this.#bmpParser.bitmapDataSize) { throw new Error('Фраза слишком велика для данного файла!') } } // ... }
Конфигурируем константы, которые потребуются для расшифровки:
export const MAX_HEXADECIMAL_VALUE = 0xFF export const POSSIBLE_DIFFERENCE = { EXIT_POINT: 3, SEPARATOR: 2, BINARY_ONE: 1, BINARY_ZERO: 0, }
Реализуем сам механизм шифрования. При обходе bitmap data мы будем использовать маску Uint8Array, так как каждый символ здесь равен одному байту:
class Encryptor { // ... #updateUint8(char) { // Текущий элемент bitmap data const currentValue = +this.#view.getUint8(this.#offset) // Установка значения в зависимости от символа const binaryChar = char === ',' ? POSSIBLE_DIFFERENCE.SEPARATOR : +char // Проверка на возможность добавления // Если текущее значение bitmap data после увеличения на 3 // больше верхней границы (255) - выполняем вычитание if (currentValue >= MAX_HEXADECIMAL_VALUE - POSSIBLE_DIFFERENCE.EXIT_POINT) { return currentValue - binaryChar } return currentValue + binaryChar } encrypt() { // Получение начального положения bitmap data this.#offset = this.#bmpParser.offsetBits // Конвертация текстового сообщения в бинарный код // и приведение полученного результата к виду ['1', '0', '1', '0', ',', ...] const binaryChars = this.encode(this.#encryptionText).join().split('') // Проверка длины сообщения this.#checkPhraseLength(binaryChars.length) binaryChars.forEach(char => { this.#view.setUint8(this.#offset, this.#updateUint8(char)) this.#offset++ }) // Добавляем точку выхода this.#view.setUint8(this.#offset, this.#updateUint8(POSSIBLE_DIFFERENCE.EXIT_POINT)) return this.#view } // ... }
Зашифрованное изображение получится визуально неотличимо от оригинала. Это произойдет из-за того, что изменение значений в bitmap data происходит максимум на 3 пункта, а вес и структура файла при этом остаются неизменными.
Расшифровка
Для расшифровки потребуется оригинальное изображение — оно будет являться ключом, а также изображение с закодированным сообщением.
Алгоритм для расшифровки сообщения:
Нам потребуются две переменные для хранения бинарной последовательности символов и результата.
Запускаем цикл и, начиная с первого байта bitmap data, находим разность по модулю между значением ключа и закодированного изображения
-
Если разность равна 0 или 1 — добавляем значение в строку с бинарной последовательность.
-
Если разность равна 2 — очищаем строку с бинарной последовательностью, а ее значение конвертируем в текст и добавляем в результирующую строку.
-
Если разность равна 3 — выполняем действия из п. 2 и останавливаем цикл.
Рассмотрим алгоритм на примере следующей разности: [1, 0, 1, 2, 1, 0, 1, 0, 3]
|
Разность |
Значение бинарной последовательности |
Значение результирующей строки |
|
1 |
1 |
|
|
0 |
10 |
|
|
1 |
101 |
|
|
2 |
e |
|
|
1 |
1 |
e |
|
0 |
10 |
e |
|
1 |
101 |
e |
|
0 |
1010 |
e |
|
3 |
eϲ |
Приступим к реализации:
export class Decipher { #offset = 0 #encryptedView #viewKey #bmpParserEncrypted #bmpParserKey constructor(encryptedBuffer, bufferKey) { this.#encryptedView = new DataView(encryptedBuffer) this.#viewKey = new DataView(bufferKey) this.#bmpParserEncrypted = new BmpParser(encryptedBuffer) this.#bmpParserKey = new BmpParser(bufferKey) } // Конвертация бинарного кода в текст decode(value) { return String.fromCharCode(parseInt(value, 2)) } decrypt() { this.#offset = this.#bmpParserKey.offsetBits let binaryChar = '' let string = '' while (true) { // Получение разности const encryptedByte = Math.abs(this.#encryptedView.getUint8(this.#offset) - this.#viewKey.getUint8(this.#offset)) switch (encryptedByte) { case POSSIBLE_DIFFERENCE.EXIT_POINT: string += this.decode(binaryChar) return string case POSSIBLE_DIFFERENCE.SEPARATOR: string += this.decode(binaryChar) binaryChar = '' break case POSSIBLE_DIFFERENCE.BINARY_ONE: case POSSIBLE_DIFFERENCE.BINARY_ZERO: binaryChar += encryptedByte break default: throw new Error('Недопустимое значение!') } this.#offset++ } } }
Заключение
Это небольшое приложение позволяет показать, как работать с бинарными данными. При этом возможностей у типизированных массивов при работе с файлами куда больше: можно добавить водяной знак в изображение или видео, рассчитать размер изображения перед выводом на экран или прочитать файлы из zip-архива.
ссылка на оригинал статьи https://habr.com/ru/articles/578284/
Добавить комментарий