Абстракция слоя хеширования позволяет отделить прикладной код от конкретного алгоритма криптографического преобразования, что особенно важно в системах, где требования к безопасности и производительности могут изменяться со временем. Вместо прямых вызовов функций библиотеки формируется единый контракт, через который приложение взаимодействует с механизмом хеширования независимо от его реализации.
Базовая идея строится вокруг унифицированного интерфейса, который описывает минимально необходимый набор операций:
Такой контракт позволяет подменять реализацию без изменения бизнес-логики.
class HashAdapter {
async hash(password) {}
async verify(password, hash) {}
getName() {}
}
Важным моментом становится требование асинхронности, поскольку большинство современных криптографических операций в Node.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 работает в чистом JavaScript, что делает его менее производительным по сравнению с нативными реализациями. Это особенно заметно при высоких значениях salt rounds.
Ключевые особенности:
Для компенсации используется:
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$..."
}
Это позволяет однозначно восстанавливать контекст при проверке.
В зрелой архитектуре слой хеширования превращается в отдельный модуль системы:
Такое разделение снижает связанность компонентов и упрощает миграции между криптографическими стандартами.
interface HashAdapter {
hash(password: string): Promise<string>;
verify(password: string, hash: string): Promise<boolean>;
getName(): string;
}
Типизация фиксирует поведение адаптера и предотвращает появление несовместимых реализаций.
При проектировании слоя хеширования фиксируются ключевые свойства:
Эти ограничения формируют границы допустимых реализаций адаптера.
Переход от bcrypt.js к более современным алгоритмам осуществляется поэтапно:
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 };
}
Такой механизм обеспечивает постепенное усиление безопасности без массовой миграции данных.