Пакетная перехеширование офлайн

При эволюции криптографических алгоритмов хранения паролей в системах на JavaScript часто возникает необходимость замены устаревшего хеша на более стойкий вариант. Причины включают увеличение вычислительной мощности атакующего, появление новых рекомендаций по параметрам стоимости вычислений, а также миграцию с одних алгоритмов на другие (например, переход с bcrypt на argon2 через обёртку библиотеки password-hash).

Пакетная офлайн-перехешировка используется в ситуациях, когда:

  • требуется обновить параметры хеширования у большого числа пользователей;
  • необходимо сменить алгоритм без принудительного сброса паролей;
  • недопустима задержка логина из-за «ленивой» перегенерации хеша при входе;
  • система обслуживает миллионы записей и требует контролируемой нагрузки.

В отличие от онлайн-подхода, где хеш обновляется во время аутентификации пользователя, офлайн-пакетная стратегия отделяет процесс миграции от пользовательского потока и выполняет его в фоновом режиме.


Модель хранения и метаданные хеша

Для корректной офлайн-миграции каждый хеш должен содержать метаданные, достаточные для принятия решения о необходимости обновления:

  • идентификатор алгоритма;
  • версия параметров (cost factor, memory, iterations);
  • соль;
  • сам хеш.

В библиотеке 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);

Принцип определения необходимости перехеширования

Ключевая функция системы — определение устаревания хеша:

  • алгоритм отличается от текущего стандарта;
  • cost-фактор ниже минимально допустимого;
  • параметры соли не соответствуют текущим требованиям;
  • используется deprecated-версия схемы кодирования.

Пример логики проверки:

import { needsRehash } fr om "password-hash";

if (needsRehash(user.passwordHash, {
  algorithm: "argon2id",
  memoryCost: 65536,
  timeCost: 3
})) {
  queueForRehash(user.id);
}

Функция needsRehash не выполняет криптографических операций над паролем пользователя — анализируется только структура хеша.


Формирование офлайн-пакета

Пакетная обработка строится вокруг очереди идентификаторов пользователей, чьи хеши требуют обновления. Источники данных:

  • таблица пользователей с флагом устаревания;
  • результаты логинов (lazy marking);
  • периодический скан базы данных;
  • события миграции политики безопасности.

Формирование пакета:

const batch = await db.users.findMany({
  wh ere: {
    passwordNeedsRehash: true
  },
  take: 1000
});

Размер пакета подбирается исходя из:

  • допустимой нагрузки на CPU;
  • latency фоновых задач;
  • параллельной активности базы данных.

Вычислительный pipeline

Офлайн-перехеширование выполняется через отдельный worker-процесс или очередь задач.

Типовая схема:

  1. извлечение батча;
  2. получение исходного пароля (в безопасном виде невозможно — используется проверка через повторную аутентификацию или хранение временного derivation context);
  3. генерация нового хеша;
  4. запись результата;
  5. пометка завершения.

Пример 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 паролю отсутствует, поэтому применяются альтернативные стратегии (см. ниже).


Ограничение: отсутствие plaintext паролей

Ключевая сложность офлайн-перехеширования заключается в том, что исходный пароль недоступен. Это требует специальных подходов:

1. Lazy rehash через логин

При следующей аутентификации пользователя:

  • проверяется старый хеш;
  • при успехе пароль перехешируется;
  • новый хеш сохраняется.

Минус — растянутая во времени миграция.

2. Re-auth batch через токены восстановления

Если система хранит временные безопасные токены:

  • используется session-derived secret;
  • происходит восстановление пароля через защищённый контекст.

3. Прозрачная миграция при наличии KDF-compat слоя

Некоторые реализации password-hash позволяют использовать адаптер:

const legacyVerify = async (password, hash) => {
  return verify(password, hash, { legacy: true });
};

Очереди задач и контроль нагрузки

Пакетная обработка почти всегда реализуется через очередь:

  • Redis Queue
  • RabbitMQ
  • Kafka streams
  • встроенные job schedulers

Пример с упрощённой очередью:

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 }
  });
});

Контроль нагрузки осуществляется через:

  • concurrency limits;
  • rate limiting на worker;
  • приоритизацию задач;
  • адаптивное масштабирование.

Инкрементальная стратегия миграции

Пакетная офлайн-обработка часто комбинируется с инкрементальной моделью:

  • новые пользователи сразу получают актуальный хеш;
  • старые обновляются по мере обработки батчей;
  • система продолжает работать без блокировок.

Состояния записи:

  • up-to-date
  • pending-rehash
  • in-progress
  • completed

Консистентность данных

Во время миграции возможны состояния гонки:

  • пользователь логинится во время перехеширования;
  • два worker-а обрабатывают одну запись;
  • запись обновляется параллельно с новым входом.

Решения:

  • optimistic locking (version field);
  • атомарные update операции;
  • idempotent job design.

Пример optimistic locking:

await db.users.update({
  wh ere: {
    id: user.id,
    version: user.version
  },
  data: {
    passwordHash: newHash,
    version: { increment: 1 }
  }
});

Безопасность процесса

Офлайн-перехеширование требует строгого соблюдения безопасности:

  • отсутствие логирования секретов;
  • изоляция worker-процессов;
  • минимизация доступа к данным;
  • шифрование промежуточных структур;
  • контроль памяти процесса (zeroing buffers при необходимости).

Особое внимание уделяется предотвращению утечек через:

  • heap snapshots;
  • swap memory;
  • debug tooling.

Оптимизация производительности

Основные методы оптимизации:

  • batch processing вместо per-user операций;
  • использование native bindings (если password-hash обёртка над bcrypt/argon2);
  • параллелизация CPU-bound задач;
  • использование worker_threads в Node.js.

Пример с 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);

Данные используются для адаптации размера батчей и частоты запуска задач.