Написание собственной схемы дополнения

Блочные алгоритмы шифрования оперируют фиксированными размерами блоков данных. Например, AES работает с блоками по 16 байт. Если входные данные не кратны размеру блока, их необходимо дополнить до нужной длины. Эта операция называется дополнением (padding).

Без корректного дополнения шифрование становится невозможным или приводит к неоднозначному восстановлению исходных данных при расшифровке.

Основные требования к схеме дополнения:

  • однозначное восстановление исходного сообщения
  • корректная работа при любой длине входных данных
  • совместимость с режимами шифрования блочных алгоритмов

Архитектура обработки данных в CryptoJS

В библиотеке CryptoJS данные представлены в виде структуры WordArray. Она хранит:

  • массив 32-битных слов
  • количество значимых байт (sigBytes)

Перед шифрованием данные проходят этапы:

  1. кодирование (например, UTF-8)
  2. применение padding
  3. разбиение на блоки
  4. обработка режимом шифрования (CBC, ECB и др.)

Padding в CryptoJS реализован как объект с двумя методами:

  • pad(wordArray, blockSize)
  • unpad(wordArray)

Интерфейс пользовательской схемы дополнения

Любая кастомная схема дополнения должна соответствовать структуре:

  • pad: добавляет байты к данным до кратности блока
  • unpad: удаляет добавленные байты после расшифровки
const CustomPadding = {
    pad: function (data, blockSize) {
        // модификация data (WordArray)
    },

    unpad: function (data) {
        // удаление добавленных байтов
    }
};

blockSize передаётся в 32-битных словах, поэтому реальный размер блока в байтах:

blockSizeBytes = blockSize * 4

Пример: реализация нулевого дополнения (Zero Padding)

Одна из простейших схем — заполнение нулями.

const ZeroPadding = {
    pad: function (data, blockSize) {
        const blockSizeBytes = blockSize * 4;

        const padding = blockSizeBytes - (data.sigBytes % blockSizeBytes);

        for (let i = 0; i < padding; i++) {
            data.concat(CryptoJS.lib.WordArray.create([0x00000000], 1));
        }
    },

    unpad: function (data) {
        while (data.sigBytes > 0 && (data.words[(data.sigBytes - 1) >>> 2] & 0xff) === 0) {
            data.sigBytes--;
        }
    }
};

Недостаток этой схемы — невозможность различить реальные нулевые байты и добавленные.


Пример: реализация ANSI X.923

ANSI X.923 использует заполнение нулями и хранит длину padding в последнем байте.

const AnsiX923 = {
    pad: function (data, blockSize) {
        const blockSizeBytes = blockSize * 4;
        const padding = blockSizeBytes - (data.sigBytes % blockSizeBytes);

        // добавляем нули
        for (let i = 0; i < padding - 1; i++) {
            data.concat(CryptoJS.lib.WordArray.create([0x00000000], 1));
        }

        // последний байт — длина padding
        data.concat(CryptoJS.lib.WordArray.create([padding], 1));
    },

    unpad: function (data) {
        const padding = data.words[(data.sigBytes - 1) >>> 2] & 0xff;
        data.sigBytes -= padding;
    }
};

Подключение собственной схемы к CryptoJS

После определения объекта padding его можно зарегистрировать:

CryptoJS.pad.Custom = CustomPadding;

И использовать в параметрах шифрования:

const encrypted = CryptoJS.AES.encrypt("Secret data", "key123", {
    padding: CryptoJS.pad.Custom
});

Расшифрование автоматически использует unpad.


Взаимодействие с WordArray

При реализации padding важно учитывать особенности WordArray:

  • данные хранятся в 32-битных словах
  • sigBytes определяет реальную длину
  • добавление байтов должно корректно увеличивать sigBytes

Прямое изменение массива words без корректировки sigBytes приводит к повреждению данных.


Ошибки при реализации padding

Наиболее частые проблемы:

1. Несоответствие sigBytes и words Неверное обновление длины приводит к утечке лишних байтов.

2. Неверный расчёт блока CryptoJS использует 32-битные слова, а не байты.

3. Отсутствие обработки полного блока Если длина уже кратна блоку, padding всё равно должен добавляться.

4. Небезопасные схемы Некоторые кастомные padding-подходы могут приводить к уязвимостям при анализе длины сообщений.


Пример корректного расчёта блока

const blockSizeBytes = blockSize * 4;
const remainder = data.sigBytes % blockSizeBytes;
const padding = blockSizeBytes - remainder;

Если remainder === 0, padding должен быть равен размеру блока.


Пример: битовое дополнение (Bit Padding)

Схема добавляет единичный бит и заполняет нулями:

const BitPadding = {
    pad: function (data, blockSize) {
        const blockSizeBytes = blockSize * 4;

        data.concat(CryptoJS.lib.WordArray.create([0x80000000], 1));

        const paddingBytes = blockSizeBytes - (data.sigBytes % blockSizeBytes);

        for (let i = 0; i < paddingBytes; i++) {
            data.concat(CryptoJS.lib.WordArray.create([0x00000000], 1));
        }
    },

    unpad: function (data) {
        let i = data.sigBytes - 1;
        while (i >= 0) {
            const byte = (data.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
            if (byte === 0x80) {
                data.sigBytes = i;
                break;
            }
            i--;
        }
    }
};

Совместимость с режимами шифрования

Padding используется только в режимах, где требуется заполнение:

  • CBC
  • ECB

В потоковых режимах (CTR, OFB) padding не применяется, так как данные шифруются побайтно.


Особенности интеграции с AES

При использовании AES в CryptoJS порядок операций фиксирован:

  1. преобразование строки в WordArray
  2. применение padding
  3. шифрование блоков
  4. кодирование результата (Base64 / Hex)

Любая кастомная схема должна работать до этапа шифрования и после расшифрования, не вмешиваясь в криптографический процесс.


Поведение при некорректных данных

Если unpad реализован неправильно:

  • возможно повреждение последних байтов
  • появляются лишние символы в UTF-8 строках
  • результат становится не детерминированным

Особенно критично это при работе с бинарными данными, где нет текстовой структуры.


Минимальные требования к надёжной реализации

  • строгая симметрия pad/unpad
  • корректная обработка границ блока
  • независимость от содержимого данных
  • работа с любыми байтами, включая 0x00
  • корректное обновление sigBytes