При работе с библиотеками хеширования паролей в JavaScript одной из наиболее распространённых проблем становится различие версий пакета между локальной разработкой, тестовым сервером и production-окружением. Особенно критично это для библиотек, связанных с криптографией и безопасностью, поскольку даже незначительные изменения API или алгоритмов могут привести к:
Для библиотеки Password-hash подобные расхождения особенно опасны, поскольку хеш пароля является долгоживущими данными. Пользовательский пароль может храниться годами, а код проверки обязан корректно работать со всеми ранее созданными значениями.
Наиболее частая причина — использование «плавающих» версий в
package.json.
Пример:
{
"dependencies": {
"password-hash": "^1.2.0"
}
}
Символ ^ разрешает установку более новых минорных
версий:
1.2.1
1.3.0
1.9.0
При этом локально разработчик может использовать:
1.2.0
а production-сервер автоматически установит:
1.9.0
Если внутри библиотеки изменился формат генерации хеша или параметры алгоритма, поведение станет различаться.
В экосистеме JavaScript фиксация зависимостей осуществляется через lock-файлы:
package-lock.jsonyarn.lockpnpm-lock.yamlЕсли один разработчик обновил зависимости, а другой не получил новый lock-файл, версии пакетов начнут расходиться.
Типичная ситуация:
| Окружение | Версия |
|---|---|
| Local | 1.2.0 |
| CI/CD | 1.4.1 |
| Production | 1.5.0 |
Даже при одинаковом package.json итоговая версия может
отличаться.
Менеджеры пакетов используют разные механизмы разрешения зависимостей.
Например:
npm install
и
yarn install
могут установить разные подзависимости.
Для криптографических библиотек это особенно важно, если Password-hash использует:
bcryptcryptoscryptargon2Разные версии подзависимостей могут менять:
Некоторые версии Password-hash используют API Node.js напрямую:
crypto.pbkdf2()
или:
crypto.scrypt()
Разные версии Node.js могут:
Например:
| Node.js | OpenSSL |
|---|---|
| 16 | 1.1 |
| 18 | 3.0 |
| 20 | 3.0+ |
В результате один и тот же код может генерировать несовместимые значения.
Наиболее критичная проблема.
Старый сервер создавал хеш:
sha1$8$...
Новая версия библиотеки ожидает:
pbkdf2$10000$...
При проверке:
passwordHash.verify(password, storedHash);
возникает ошибка:
Unknown hash format
или:
Hash version unsupported
Некоторые версии библиотек изменяют внутреннюю структуру строки.
Например:
salt:hash
algorithm$iterations$salt$hash
При миграции приложения это ломает:
В ранних версиях библиотека могла использовать:
SHA-1
Позднее:
PBKDF2
или:
Argon2
Без явного указания алгоритма приложение начинает работать иначе после обновления зависимости.
Пример опасного кода:
const hash = passwordHash.generate(password);
Поведение полностью зависит от версии библиотеки.
Некоторые обновления увеличивают:
Пример:
iterations = 1000
iterations = 100000
Результат:
Наиболее безопасный вариант:
{
"dependencies": {
"password-hash": "1.2.0"
}
}
Без:
^~>=Это гарантирует идентичную установку.
Lock-файлы должны:
Правильная структура репозитория:
project/
├── package.json
├── package-lock.json
└── src/
Команда:
npm ci
в отличие от:
npm install
не пересчитывает дерево зависимостей и строго использует lock-файл.
Для production это предпочтительный вариант.
const pkg = require('password-hash/package.json');
console.log(pkg.version);
const expectedVersion = '1.2.0';
const actualVersion = pkg.version;
if (actualVersion !== expectedVersion) {
throw new Error(
`Unsupported password-hash version: ${actualVersion}`
);
}
Резкое обновление криптографической библиотеки опасно.
Неправильно:
1.x → 5.x
Лучше:
1.x → 2.x → 3.x → 4.x → 5.x
На каждом этапе проверяются:
Надёжная система авторизации должна уметь распознавать разные поколения хешей.
Пример:
function verifyPassword(password, hash) {
if (hash.startsWith('sha1$')) {
return verifyLegacy(password, hash);
}
if (hash.startsWith('pbkdf2$')) {
return verifyModern(password, hash);
}
return false;
}
Популярная стратегия миграции.
Схема работы:
Пример:
if (verifyLegacy(password, oldHash)) {
const newHash = generateModernHash(password);
await users.update({
password: newHash
});
}
Это позволяет обновлять безопасность постепенно без массового сброса паролей.
Локально:
node:18
Production:
node:20
Даже при одинаковой версии Password-hash поведение может отличаться.
Особенно актуально для native-модулей:
| Архитектура | Особенности |
|---|---|
| x64 | стандартная сборка |
| ARM64 | отдельные бинарники |
| Alpine | musl libc |
| Debian | glibc |
Некоторые библиотеки хеширования компилируются по-разному.
Плохо:
FROM node:latest
Хорошо:
FROM node:20.11.1
Необходимо тестировать:
Пример:
describe('password compatibility', () => {
test('verify legacy hashes', () => {
const hash = 'sha1$test';
expect(
verifyLegacy('password', hash)
).toBe(true);
});
});
Полезно фиксировать формат результата.
test('hash format', () => {
const hash = generateHash('secret');
expect(hash).toMatchSnapshot();
});
Если новая версия библиотеки изменит структуру строки, тест упадёт.
Даже минорное обновление может менять:
Например:
1.2.0 → 1.3.0
формально совместимо, но фактически может ломать production.
Иногда библиотека исправляет уязвимость и меняет формат хеша.
После обновления:
Подобные изменения нельзя внедрять без тестирования.
Нельзя использовать Password-hash напрямую во всём проекте.
Плохо:
passwordHash.generate(password);
в десятках файлов.
Правильно:
// auth/hash.js
module.exports = {
hashPassword,
verifyPassword
};
Тогда изменение библиотеки затронет только один модуль.
Никогда не полагаться на значения по умолчанию.
Плохо:
generate(password);
Хорошо:
generate(password, {
algorithm: 'pbkdf2',
iterations: 100000,
saltLength: 32
});
Полезно сохранять метаданные:
| user_id | algorithm | version |
|---|---|---|
| 1 | pbkdf2 | 2 |
| 2 | argon2 | 3 |
Это упрощает:
Во время запуска приложения:
console.log({
passwordHashVersion: pkg.version,
node: process.version
});
Это сильно ускоряет поиск ошибок.
Команда:
npm ls password-hash
показывает:
password-hash@1.2.0
или наличие нескольких версий одновременно.
Иногда разные пакеты тянут разные версии библиотеки:
password-hash@1.2.0
password-hash@2.0.0
Это приводит к непредсказуемому поведению.
{
"dependencies": {
"password-hash": "1.2.0"
},
"engines": {
"node": "20.11.1"
}
}
FROM node:20.11.1
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]
const pkg = require('password-hash/package.json');
const SUPPORTED = ['1.2.0'];
if (!SUPPORTED.includes(pkg.version)) {
throw new Error(
`Unsupported version: ${pkg.version}`
);
}
npm install password-hash@latest
Опасно для production.
Без lock-файла невозможно гарантировать воспроизводимую сборку.
Особенно опасно для:
Некоторые системы одновременно используют:
Без явного управления версиями это превращается в источник постоянных ошибок.
Production-система должна обеспечивать:
Безопасная схема обновления:
Перед обновлением Password-hash необходимо изучать:
Даже небольшое обновление криптографической библиотеки требует такого же внимания, как изменение схемы базы данных или механизма аутентификации.