Хеши паролей обладают важным свойством: при одинаковых входных данных и одинаковых параметрах алгоритма результат должен быть воспроизводим. Именно это свойство делает возможной проверку пароля без его хранения в открытом виде. Однако при изменении параметров хеширования возникает проблема совместимости: старые хеши продолжают существовать в базе, но новые уже формируются иначе.
В библиотеке Password-hash (как и в большинстве современных решений для хеширования паролей) результат зависит от набора параметров, которые могут меняться со временем:
Любое изменение этих значений приводит к тому, что одинаковый пароль будет давать другой хеш. Это создаёт несовместимость между новыми и старыми записями.
Сохранённые в базе хеши являются «снимком» конкретной конфигурации алгоритма. При изменении параметров возникает ситуация:
По этой причине любые изменения в параметрах требуют стратегии миграции, а не простого обновления конфигурации.
Одним из базовых решений является включение версии схемы в сам хеш. В Password-hash это часто реализуется через структурированный формат строки:
algorithm:version:parameters:hash
Пример:
pbkdf2:v1:iterations=10000:salt.hash
pbkdf2:v2:iterations=200000:salt.hash
Такой подход позволяет однозначно определить, как именно был получен хеш, и выбрать корректный алгоритм проверки.
При аутентификации процесс проверки обычно включает следующие шаги:
Пример логики на Jav * aScript:
import passwordHash from "password-hash";
function verifyPassword(password, storedHash) {
const parsed = passwordHash.parse(storedHash);
if (parsed.algo === "pbkdf2" && parsed.version === 1) {
return passwordHash.pbkdf2.verify(password, storedHash);
}
if (parsed.algo === "pbkdf2" && parsed.version === 2) {
return passwordHash.pbkdf2.verify(password, storedHash);
}
return false;
}
Такой подход позволяет поддерживать несколько поколений хешей одновременно.
Если библиотека не поддерживает версионирование, возникает скрытая несовместимость. Например:
В этом случае новые хеши невозможно отличить от старых, что приводит к двум рискам:
Практическая стратегия обновления хешей заключается в «перехешировании при входе»:
Пример:
function login(password, user) {
const isValid = passwordHash.verify(password, user.passwordHash);
if (!isValid) return false;
const needsUpgrade = passwordHash.needsRehash(user.passwordHash, {
iterations: 200000
});
if (needsUpgrade) {
user.passwordHash = passwordHash.generate(password, {
iterations: 200000
});
save(user);
}
return true;
}
Такой подход обеспечивает постепенную миграцию без принудительного сброса паролей.
Для корректной совместимости библиотека должна уметь сравнивать текущие параметры с эталонными:
Если параметры отличаются от рекомендованных, хеш помечается как устаревший.
Наиболее устойчивый подход — самодостаточный хеш, содержащий всю информацию о конфигурации:
argon2id$v=19$m=65536,t=3,p=1$salt$hash
Такой формат позволяет:
Смена алгоритма (например, с PBKDF2 на Argon2) является наиболее критическим изменением. Прямая совместимость невозможна, поэтому используется комбинация стратегий:
В этот период система должна уметь распознавать оба формата.
Неправильная работа с параметрами приводит к ряду проблем:
Особенно критичной является ситуация, когда изменённые параметры применяются без сохранения информации о предыдущих конфигурациях.
Для обеспечения совместимости используется единый формат хранения:
{
"hash": "pbkdf2:v2:iterations=200000:salt.hash",
"createdAt": "2025-01-01",
"algorithm": "pbkdf2",
"version": 2
}
Такой подход позволяет:
Система может автоматически определять необходимость обновления:
Если хотя бы один параметр не соответствует текущей политике, хеш считается устаревшим и подлежит обновлению при следующей успешной аутентификации.
В распределённых системах часто встречается ситуация, когда разные сервисы используют разные версии параметров. Для этого вводится:
Это позволяет исключить расхождение логики проверки между компонентами системы.