Реализация собственного адаптера хеширования с заменяемым алгоритмом

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

Базовая идея строится вокруг унифицированного интерфейса, который описывает минимально необходимый набор операций:

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

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

class HashAdapter {
  async hash(password) {}
  async verify(password, hash) {}
  getName() {}
}

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

Интеграция bcrypt.js как базовой реализации

Библиотека bcrypt.js реализует алгоритм bcrypt полностью на JavaScript, что делает её удобной для окружений без нативных зависимостей.

Адаптер поверх bcrypt.js формирует конкретную реализацию интерфейса:

import bcrypt from 'bcryptjs';

class BcryptAdapter {
  constructor(rounds = 10) {
    this.rounds = rounds;
    this.name = 'bcrypt';
  }

  async hash(password) {
    const salt = await bcrypt.genSalt(this.rounds);
    return bcrypt.hash(password, salt);
  }

  async verify(password, hash) {
    return bcrypt.compare(password, hash);
  }

  getName() {
    return this.name;
  }
}

Здесь алгоритм зашит внутри адаптера, но сам интерфейс остаётся неизменным, что позволяет использовать его как взаимозаменяемый компонент.

Стратегия замены алгоритма

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

class HashService {
  constructor(adapter) {
    this.adapter = adapter;
  }

  async hash(password) {
    return this.adapter.hash(password);
  }

  async verify(password, hash) {
    return this.adapter.verify(password, hash);
  }
}

Подстановка конкретного алгоритма становится вопросом конфигурации:

const bcryptAdapter = new BcryptAdapter(12);
const hashService = new HashService(bcryptAdapter);

Таким образом достигается полная изоляция бизнес-логики от криптографического слоя.

Расширение: поддержка нескольких алгоритмов

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

class MultiHashAdapter {
  constructor(adapters) {
    this.adapters = new Map();

    for (const adapter of adapters) {
      this.adapters.set(adapter.getName(), adapter);
    }
  }

  async hash(password, algorithm = 'bcrypt') {
    const adapter = this.adapters.get(algorithm);
    return adapter.hash(password);
  }

  async verify(password, hash, algorithmHint) {
    const adapter = this.adapters.get(algorithmHint || 'bcrypt');
    return adapter.verify(password, hash);
  }
}

Такой подход позволяет одновременно поддерживать несколько алгоритмов, например bcrypt, scrypt или argon2, без изменения верхнего уровня системы.

Детектирование алгоритма по хешу

Некоторые алгоритмы содержат сигнатуру в строке хеша. Bcrypt, например, всегда начинается с $2a$, $2b$ или $2y$.

function detectAlgorithm(hash) {
  if (hash.startsWith('$2')) {
    return 'bcrypt';
  }
  return 'unknown';
}

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

async verify(password, hash) {
  const algorithm = detectAlgorithm(hash);
  const adapter = this.adapters.get(algorithm);

  if (!adapter) {
    throw new Error('Unsupported hash algorithm');
  }

  return adapter.verify(password, hash);
}

Изоляция конфигурации параметров

Разные алгоритмы требуют различных параметров сложности. В bcrypt.js это количество раундов (salt rounds), в других алгоритмах — память, параллелизм или длина ключа.

Создание унифицированной конфигурации решает проблему различий:

const config = {
  bcrypt: {
    rounds: 12
  },
  scrypt: {
    cost: 16384,
    blockSize: 8,
    parallelization: 1
  }
};

Адаптер получает конфигурацию через конструктор:

class BcryptAdapter {
  constructor(config) {
    this.rounds = config.rounds ?? 10;
  }
}

Обработка ошибок и деградация безопасности

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

Механизм fallback реализуется через цепочку адаптеров:

class FallbackHashAdapter {
  constructor(adapters) {
    this.adapters = adapters;
  }

  async verify(password, hash) {
    for (const adapter of this.adapters) {
      try {
        const result = await adapter.verify(password, hash);
        if (result) return true;
      } catch (_) {
        continue;
      }
    }
    return false;
  }
}

Такой подход предотвращает потерю доступа к ранее созданным учетным данным.

Производительность и ограничения bcrypt.js

bcrypt.js работает в чистом JavaScript, что делает его менее производительным по сравнению с нативными реализациями. Это особенно заметно при высоких значениях salt rounds.

Ключевые особенности:

  • линейное увеличение времени хеширования с ростом rounds
  • высокая нагрузка на event loop
  • отсутствие аппаратного ускорения

Для компенсации используется:

  • ограничение rounds в production
  • вынесение хеширования в worker threads
  • использование очередей задач

Вынос вычислений в worker threads

import { Worker } from 'worker_threads';

function hashInWorker(password) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./hash-worker.js', {
      workerData: password
    });

    worker.on('message', resolve);
    worker.on('error', reject);
  });
}

Внутри worker используется тот же bcrypt.js, но без блокировки основного потока.

Безопасное хранение метаданных алгоритма

Для корректной миграции алгоритмов в хеш часто добавляется префикс:

bcrypt$2b$10$...

или структурированный формат:

{
  "alg": "bcrypt",
  "hash": "$2b$10$..."
}

Это позволяет однозначно восстанавливать контекст при проверке.

Архитектура расширяемого хеширования

В зрелой архитектуре слой хеширования превращается в отдельный модуль системы:

  • HashService (фасад)
  • Adapters (bcrypt, scrypt, argon2)
  • Registry (реестр алгоритмов)
  • Detector (определение алгоритма)
  • Config Provider (централизованные настройки)

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

Типизация контракта (TypeScript подход)

interface HashAdapter {
  hash(password: string): Promise<string>;
  verify(password: string, hash: string): Promise<boolean>;
  getName(): string;
}

Типизация фиксирует поведение адаптера и предотвращает появление несовместимых реализаций.

Инварианты адаптера

При проектировании слоя хеширования фиксируются ключевые свойства:

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

Эти ограничения формируют границы допустимых реализаций адаптера.

Миграция между алгоритмами

Переход от bcrypt.js к более современным алгоритмам осуществляется поэтапно:

  1. сохранение алгоритма в хеше
  2. добавление нового адаптера
  3. двойная проверка (старый + новый)
  4. перехеширование при успешной аутентификации
async function verifyAndUpgrade(password, hash) {
  const valid = await hashService.verify(password, hash);

  if (valid && detectAlgorithm(hash) !== 'argon2') {
    const newHash = await argon2Adapter.hash(password);
    return { valid, newHash };
  }

  return { valid };
}

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

Итоговая структура взаимодействия

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