Создание собственного провайдера алгоритма

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

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

Ключевые элементы архитектуры:

  • Ядро библиотеки — отвечает за управление провайдерами и единый API.
  • Провайдер алгоритма — реализация конкретного способа хеширования.
  • Формат результата — унифицированная структура хранения хеша и метаданных.
  • Регистратор провайдеров — механизм подключения и выбора алгоритма.

Такое разделение снижает связанность компонентов и упрощает сопровождение кода.


Контракт провайдера алгоритма

Каждый провайдер в системе password-hash реализует строго определённый набор методов. Эти методы образуют интерфейс, через который ядро взаимодействует с алгоритмом.

Типичный контракт включает:

  • generateSalt(options)
  • hash(password, salt, options)
  • verify(password, hashedValue, options)
  • identify(hashedValue) — определение принадлежности алгоритму
  • serialize(data) и deserialize(string) — работа с форматом хранения

Обязательным требованием является детерминированность проверки: один и тот же пароль при одинаковой соли должен давать одинаковый результат.


Регистрация и выбор провайдера

Ядро библиотеки поддерживает реестр алгоритмов. Каждый провайдер регистрируется под уникальным идентификатором, например:

registerProvider('argon2', argon2Provider);
registerProvider('bcrypt', bcryptProvider);

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

Алгоритм выбора:

  1. Анализ структуры входной строки хеша.
  2. Вызов identify() у каждого зарегистрированного провайдера.
  3. Выбор подходящего обработчика.
  4. Делегирование операции соответствующему модулю.

Создание собственного провайдера алгоритма

Создание нового провайдера требуется при интеграции нестандартных алгоритмов или адаптации существующих криптографических решений под унифицированный API библиотеки.

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

Базовая структура провайдера

const customProvider = {
  generateSalt(options) {
    return cryptoRandom(options.saltLength || 16);
  },

  hash(password, salt, options) {
    const iterations = options.iterations || 10000;
    return customKdf(password, salt, iterations);
  },

  verify(password, hashedValue, options) {
    const { salt, hash } = this.deserialize(hashedValue);
    const newHash = this.hash(password, salt, options);
    return timingSafeEqual(newHash, hash);
  },

  identify(hashedValue) {
    return hashedValue.startsWith('$custom$');
  },

  serialize({ salt, hash, options }) {
    return `$custom$${options.iterations}$${salt}$${hash}`;
  },

  deserialize(serialized) {
    const parts = serialized.split('$');
    return {
      salt: parts[2],
      hash: parts[3],
      iterations: Number(parts[1])
    };
  }
};

Валидация входных параметров

Надёжный провайдер обязан контролировать входные данные. Ошибки на этом уровне приводят к уязвимостям или нестабильности системы.

Основные проверки:

  • корректность длины пароля;
  • допустимость параметров итераций;
  • проверка формата соли;
  • защита от null/undefined значений.

Пример:

hash(password, salt, options) {
  if (typeof password !== 'string') {
    throw new TypeError('Password must be a string');
  }

  if (!salt || typeof salt !== 'string') {
    throw new Error('Invalid salt');
  }

  if (options.iterations < 1000) {
    throw new Error('Iterations too low');
  }

  return customKdf(password, salt, options.iterations);
}

Криптографическая функция внутри провайдера

Внутренняя реализация алгоритма может опираться на Node.js crypto API или сторонние библиотеки. Важно, чтобы провайдер не раскрывал детали реализации наружу и возвращал только результат в стандартизированном формате.

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

const crypto = require('crypto');

function customKdf(password, salt, iterations) {
  return crypto.pbkdf2Sync(
    password,
    salt,
    iterations,
    64,
    'sha512'
  ).toString('hex');
}

Безопасное сравнение значений

Сравнение хешей должно выполняться через механизмы, устойчивые к timing-атакам. Прямое сравнение строк недопустимо.

function timingSafeEqual(a, b) {
  const bufferA = Buffer.from(a);
  const bufferB = Buffer.from(b);

  if (bufferA.length !== bufferB.length) return false;

  return crypto.timingSafeEqual(bufferA, bufferB);
}

Подключение провайдера к системе

После реализации объект регистрируется в ядре библиотеки:

import { registerProvider } from 'password-hash';

registerProvider('custom', customProvider);

После регистрации алгоритм становится доступен через общий API:

hashPassword('secret', { algorithm: 'custom' });

Обработка обратной совместимости

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

Система должна:

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

Ошибки проектирования провайдеров

Распространённые проблемы:

  • отсутствие строгой сериализации;
  • зависимость от внешнего состояния;
  • использование небезопасных сравнений;
  • игнорирование параметров конфигурации;
  • нарушение детерминированности.

Любое отклонение от контракта приводит к нестабильной работе всей системы хеширования.


Расширяемость и комбинированные алгоритмы

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

Пример цепочки:

  • старый алгоритм проверяется первым;
  • при успехе происходит автоматическая перегенерация хеша новым провайдером;
  • данные обновляются без вмешательства пользователя.

Такая стратегия позволяет постепенно усиливать безопасность без массовой миграции паролей.