Как стать автором
Обновить
141.41
Рейтинг
Райффайзенбанк
Развеиваем мифы об IT в банках

Файлы как они есть. Работа с типизированными массивами

Блог компании Райффайзенбанк JavaScript *

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

Введение

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

При обработке графических данных компьютеру тоже поступают сигналы, где последовательность из трех байт (или четырех, при наличии альфа-канала) является значением цвета 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. Конверуем текстовое сообщение в бинарный код. Возможные значения: «1», «0», «,».

  2. Поочередно рассматриваем каждый символ:

    1. Если этот символ имеет значение 0 или 1, оставляем без изменений.

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

    3. Записать полученное число в bitmap data.

  3. Добавить точку выхода, в нашем случае — значение «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, находим разность по модулю между значением ключа и закодированного изображения

  1. Если разность равна 0 или 1 — добавляем значение в строку с бинарной последовательность.

  2. Если разность равна 2 — очищаем строку с бинарной последовательностью, а ее значение конвертируем в текст и добавляем в результирующую строку.

  3. Если разность равна 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

Приступим к реализации:

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++
    }
  }
}

Код приложения на GitHub

Заключение

Это небольшое приложение позволяет показать, как работать с бинарными данными. При этом возможностей у типизированных массивов при работе с файлами куда больше: можно добавить водяной знак в изображение или видео, рассчитать размер изображения перед выводом на экран или прочитать файлы из zip-архива. 

Теги:
Хабы:
Всего голосов 8: ↑8 и ↓0 +8
Просмотры 3.2K
Комментарии Комментировать

Информация

Дата основания
1996
Местоположение
Россия
Сайт
www.raiffeisen.ru
Численность
5 001–10 000 человек
Дата регистрации