Библиотеки хеширования паролей в JavaScript обычно строятся вокруг идеи абстракции алгоритмов. Вместо жесткой привязки к конкретной криптографической функции используется слой провайдеров, позволяющий подменять реализацию хеширования без изменения остального кода системы.
В основе такого подхода лежит единый контракт, определяющий поведение алгоритма: генерация соли, вычисление хеша, проверка пароля и сериализация результата. Подобная архитектура делает систему расширяемой и позволяет внедрять новые алгоритмы без модификации ядра.
Ключевые элементы архитектуры:
Такое разделение снижает связанность компонентов и упрощает сопровождение кода.
Каждый провайдер в системе password-hash реализует строго определённый набор методов. Эти методы образуют интерфейс, через который ядро взаимодействует с алгоритмом.
Типичный контракт включает:
generateSalt(options)hash(password, salt, options)verify(password, hashedValue, options)identify(hashedValue) — определение принадлежности
алгоритмуserialize(data) и deserialize(string) —
работа с форматом храненияОбязательным требованием является детерминированность проверки: один и тот же пароль при одинаковой соли должен давать одинаковый результат.
Ядро библиотеки поддерживает реестр алгоритмов. Каждый провайдер регистрируется под уникальным идентификатором, например:
registerProvider('argon2', argon2Provider);
registerProvider('bcrypt', bcryptProvider);
При выполнении операций используется либо явно указанный алгоритм, либо автоматическое определение по сигнатуре хеша.
Алгоритм выбора:
identify() у каждого зарегистрированного
провайдера.Создание нового провайдера требуется при интеграции нестандартных алгоритмов или адаптации существующих криптографических решений под унифицированный 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])
};
}
};
Надёжный провайдер обязан контролировать входные данные. Ошибки на этом уровне приводят к уязвимостям или нестабильности системы.
Основные проверки:
Пример:
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().
Система должна:
Распространённые проблемы:
Любое отклонение от контракта приводит к нестабильной работе всей системы хеширования.
Некоторые реализации допускают составные провайдеры, где один алгоритм использует другой как базовый слой. Это применяется для миграции данных или усиления защиты.
Пример цепочки:
Такая стратегия позволяет постепенно усиливать безопасность без массовой миграции паролей.