Добавление новой хэш-функции

Хэш-функции в SJCL строятся вокруг общего интерфейса sjcl.hash, который задаёт единый контракт для всех реализаций. Любая новая функция хэширования должна соответствовать этому контракту, чтобы быть совместимой с механизмами библиотеки: HMAC, PBKDF2, режимами подписи и другими криптографическими примитивами.

В основе лежит абстрактный объект:

  • reset() — инициализация внутреннего состояния
  • update(data) — обработка входных данных
  • finalize() — завершение и выдача результата
  • blockSize — размер блока в 32-битных словах
  • outputSize — длина хэша в битах

Каждая реализация наследует поведение от sjcl.hash и переопределяет только криптографически значимые части.

Внутренне SJCL оперирует не байтами, а 32-битными словами через sjcl.bitArray, что критично учитывать при добавлении нового алгоритма.

Базовый шаблон новой хэш-функции

Новая хэш-функция определяется как конструктор и прототип, наследующий sjcl.hash:

sjcl.hash.MyHash = function (hashParams) {
    if (!hashParams) hashParams = {};
    this.reset();
};

Наследование выполняется через:

sjcl.hash.MyHash.prototype = new sjcl.hash();
sjcl.hash.MyHash.prototype.constructor = sjcl.hash.MyHash;

После этого необходимо определить обязательные поля:

sjcl.hash.MyHash.prototype.blockSize = 512;
sjcl.hash.MyHash.prototype.outputSize = 256;

Внутреннее состояние алгоритма

Любой хэш-алгоритм требует внутреннего состояния. В SJCL оно обычно хранится в виде массива 32-битных слов:

sjcl.hash.MyHash.prototype.reset = function () {
    this._state = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476];
    this._count = 0;
    this._buffer = [];
};
  • _state — текущие хэш-регистры
  • _count — количество обработанных бит
  • _buffer — промежуточные данные до заполнения блока

Обработка входных данных

Метод update отвечает за накопление и обработку входного потока. SJCL передаёт данные в формате bitArray, поэтому требуется корректная работа с этим форматом.

sjcl.hash.MyHash.prototype.update = function (data) {
    var i, w;

    data = sjcl.bitArray.bitSlice(data, 0);

    for (i = 0; i < data.length; i += 1) {
        w = data[i];
        this._buffer.push(w);

        if (this._buffer.length === this.blockSize / 32) {
            this._processBlock(this._buffer);
            this._buffer = [];
        }
    }

    this._count += sjcl.bitArray.bitLength(data);
};

Здесь ключевым моментом является согласование формата входных данных с внутренними словами алгоритма.

Обработка блока данных

Сердце хэш-функции — функция обработки блока:

sjcl.hash.MyHash.prototype._processBlock = function (block) {
    var a = this._state[0],
        b = this._state[1],
        c = this._state[2],
        d = this._state[3],
        i;

    for (i = 0; i < block.length; i++) {
        var temp = (a ^ b ^ c ^ d ^ block[i]) >>> 0;

        a = d;
        d = c;
        c = b;
        b = temp;
    }

    this._state[0] = (this._state[0] + a) >>> 0;
    this._state[1] = (this._state[1] + b) >>> 0;
    this._state[2] = (this._state[2] + c) >>> 0;
    this._state[3] = (this._state[3] + d) >>> 0;
};

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

Завершение вычисления

Метод finalize обязан корректно обработать padding и вернуть результат в формате bitArray.

sjcl.hash.MyHash.prototype.finalize = function () {
    var i, pad = [];

    pad.push(0x80000000);

    while ((this._buffer.length + pad.length) % (this.blockSize / 32) !== (this.blockSize / 32 - 2)) {
        pad.push(0);
    }

    pad.push(0);
    pad.push(this._count);

    this.update(pad);

    return sjcl.bitArray.clamp(this._state, this.outputSize);
};

Padding должен учитывать специфику алгоритма. В большинстве случаев используется схема, аналогичная MD-стилю: добавление 1-битного маркера, нулей и длины сообщения.

Преобразование результата

SJCL ожидает результат в формате bitArray, поэтому часто требуется приведение:

sjcl.hash.MyHash.prototype.encrypt = function (data) {
    return this.finalize(data);
};

Хотя encrypt не является обязательным, он используется для совместимости с некоторыми слоями библиотеки.

Регистрация новой хэш-функции в SJCL

После определения алгоритма его необходимо зарегистрировать в пространстве имён:

sjcl.hash.myhash = sjcl.hash.MyHash;

Это позволяет использовать его в HMAC и других конструкциях:

var h = new sjcl.hash.myhash();
h.update(sjcl.codec.utf8String.toBits("data"));
var out = h.finalize();

Совместимость с HMAC

Любая новая хэш-функция автоматически становится доступной для HMAC, если реализует стандартный интерфейс:

var mac = new sjcl.misc.hmac(key, sjcl.hash.myhash);
mac.update(message);
var result = mac.digest();

Ключевой момент — корректная работа reset() после каждого завершения вычисления.

Особенности работы с sjcl.bitArray

SJCL не использует байтовые массивы напрямую. Вместо этого применяются операции над 32-битными словами:

  • sjcl.bitArray.bitLength(x) — длина
  • sjcl.bitArray.clamp(x, len) — обрезка
  • sjcl.bitArray.concat(a, b) — объединение
  • sjcl.bitArray.bitSlice(x, start, end) — срез

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

Типичные ошибки при реализации

Часто встречаются следующие проблемы:

  • Неверная обработка padding → разные результаты на одинаковых данных
  • Игнорирование переполнения 32-битных операций
  • Несоответствие bitArray формату
  • Отсутствие очистки состояния в reset()
  • Неправильный blockSize, из-за чего ломается HMAC

Корректная работа требует строгого соблюдения 32-битной арифметики:

(a + b) >>> 0

Интеграция с кодеками SJCL

Для удобства взаимодействия часто используется кодирование:

sjcl.codec.hex.fromBits(hash)
sjcl.codec.base64.fromBits(hash)
sjcl.codec.utf8String.toBits(text)

Новая хэш-функция автоматически совместима с этими кодеками, поскольку возвращает стандартный bitArray.

Расширение через параметры конструктора

Иногда алгоритм требует параметров (например, количество раундов):

sjcl.hash.MyHash = function (params) {
    params = params || {};
    this.rounds = params.rounds || 10;
    this.reset();
};

Это позволяет адаптировать хэш-функцию без изменения интерфейса библиотеки.

Производительность и оптимизация

SJCL изначально оптимизирован для JavaScript-движков, но хэш-функции часто становятся узким местом. Ключевые моменты оптимизации:

  • минимизация операций над массивами
  • использование локальных переменных для state
  • избегание лишних вызовов функций внутри циклов
  • использование >>> 0 для принудительного unsigned int

Новая хэш-функция в SJCL становится полноценной частью криптографического стека только при соблюдении строгого интерфейса sjcl.hash, корректной работе с bitArray и совместимости с HMAC и кодеками.