При эволюции криптографических алгоритмов хранения паролей в системах на JavaScript часто возникает необходимость замены устаревшего хеша на более стойкий вариант. Причины включают увеличение вычислительной мощности атакующего, появление новых рекомендаций по параметрам стоимости вычислений, а также миграцию с одних алгоритмов на другие (например, переход с bcrypt на argon2 через обёртку библиотеки password-hash).
Пакетная офлайн-перехешировка используется в ситуациях, когда:
В отличие от онлайн-подхода, где хеш обновляется во время аутентификации пользователя, офлайн-пакетная стратегия отделяет процесс миграции от пользовательского потока и выполняет его в фоновом режиме.
Для корректной офлайн-миграции каждый хеш должен содержать метаданные, достаточные для принятия решения о необходимости обновления:
В библиотеке password-hash подобная структура обычно сериализуется в единое строковое представление:
const stored = {
algo: "bcrypt",
cost: 10,
salt: "s0m3S@lt",
hash: "$2b$10$EixZaYVK1fsbw1ZfbX3OXe..."
};
При парсинге строки система восстанавливает контекст вычисления:
import { parseHash } fr om "password-hash";
const record = parseHash(stored.hash);
Ключевая функция системы — определение устаревания хеша:
Пример логики проверки:
import { needsRehash } fr om "password-hash";
if (needsRehash(user.passwordHash, {
algorithm: "argon2id",
memoryCost: 65536,
timeCost: 3
})) {
queueForRehash(user.id);
}
Функция needsRehash не выполняет криптографических
операций над паролем пользователя — анализируется только структура
хеша.
Пакетная обработка строится вокруг очереди идентификаторов пользователей, чьи хеши требуют обновления. Источники данных:
Формирование пакета:
const batch = await db.users.findMany({
wh ere: {
passwordNeedsRehash: true
},
take: 1000
});
Размер пакета подбирается исходя из:
Офлайн-перехеширование выполняется через отдельный worker-процесс или очередь задач.
Типовая схема:
Пример worker-а:
import { hash } fr om "password-hash";
async function processBatch(batch) {
for (const user of batch) {
const newHash = await hash(user.plainPassword, {
algorithm: "argon2id",
memoryCost: 65536,
timeCost: 3
});
await db.users.update({
wh ere: { id: user.id },
data: {
passwordHash: newHash,
passwordNeedsRehash: false
}
});
}
}
В реальных системах прямой доступ к plaintext паролю отсутствует, поэтому применяются альтернативные стратегии (см. ниже).
Ключевая сложность офлайн-перехеширования заключается в том, что исходный пароль недоступен. Это требует специальных подходов:
При следующей аутентификации пользователя:
Минус — растянутая во времени миграция.
Если система хранит временные безопасные токены:
Некоторые реализации password-hash позволяют использовать адаптер:
const legacyVerify = async (password, hash) => {
return verify(password, hash, { legacy: true });
};
Пакетная обработка почти всегда реализуется через очередь:
Пример с упрощённой очередью:
import Queue fr om "bull";
const rehashQueue = new Queue("rehash");
rehashQueue.process(async (job) => {
const user = await db.users.findById(job.data.userId);
const newHash = await hash(user.password, {
algorithm: "argon2id",
memoryCost: 65536,
timeCost: 3
});
await db.users.update({
wh ere: { id: user.id },
data: { passwordHash: newHash }
});
});
Контроль нагрузки осуществляется через:
Пакетная офлайн-обработка часто комбинируется с инкрементальной моделью:
Состояния записи:
up-to-datepending-rehashin-progresscompletedВо время миграции возможны состояния гонки:
Решения:
Пример optimistic locking:
await db.users.update({
wh ere: {
id: user.id,
version: user.version
},
data: {
passwordHash: newHash,
version: { increment: 1 }
}
});
Офлайн-перехеширование требует строгого соблюдения безопасности:
Особое внимание уделяется предотвращению утечек через:
Основные методы оптимизации:
Пример с worker_threads:
import { Worker } from "worker_threads";
const worker = new Worker("./rehash-worker.js", {
workerData: { batch }
});
При масштабных обновлениях политики хеширования используется централизованная конфигурация:
Пример конфигурации:
export const passwordPolicy = {
algorithm: "argon2id",
memoryCost: 65536,
timeCost: 3,
migrationBatchSize: 1000
};
Система периодически сравнивает хеши пользователей с этой конфигурацией и формирует задачи на обновление.
Для контроля процесса используется метрика:
Пример вычисления прогресса:
const progress = 1 - (pending / total);
Данные используются для адаптации размера батчей и частоты запуска задач.