Внутренняя реализация bcrypt.js: разбор исходного кода

Библиотека bcrypt.js представляет собой чистую JavaScript-реализацию алгоритма хеширования паролей bcrypt, основанного на блочном шифре Blowfish. В отличие от нативных модулей, использующих C/C++ bindings, bcrypt.js полностью выполняется в среде JavaScript, что делает её переносимой, но одновременно накладывает ограничения на производительность.

Внутреннее устройство библиотеки можно условно разделить на несколько уровней:

  • реализация модифицированного Blowfish (eksBlowfish)
  • механизм генерации и обработки соли
  • функция экспоненциального усложнения вычислений (cost factor)
  • интерфейсные функции hash / compare
  • кодирование результата в стандартный формат bcrypt

Основной алгоритм: EksBlowfish

В основе bcrypt лежит не стандартный Blowfish, а его модификация — eksBlowfish (expensive key schedule Blowfish). Именно «дорогая» стадия и делает алгоритм устойчивым к перебору.

Классический Blowfish состоит из:

  • P-массива (18 32-битных слов)
  • 4 S-блоков (каждый по 256 элементов)

В bcrypt.js структура полностью повторяет эту модель, но инициализация проходит через многократное переобучение ключевого расписания.

Инициализация состояния

Изначально P-массив и S-блоки заполняются фиксированными константами, производными от дробной части числа π. Это важно для детерминированного старта.

P, S ← initial_state(pi_constants)

Генерация соли

Соль в bcrypt.js играет ключевую роль в защите от радужных таблиц. Она всегда имеет фиксированный размер — 16 байт.

Внутренне процесс выглядит так:

  • используется криптографически стойкий генератор случайных чисел (в Node.js — crypto.randomBytes)
  • байты кодируются в base64-алфавит bcrypt (./A-Za-z0-9)

Формат соли:

$2a$<cost>$<22 символа соли>

Где cost — показатель сложности (например, 10, 12, 14).

EksBlowfishSetup: ядро стоимости алгоритма

Самая важная часть реализации — процедура расширения ключа:

Шаги EksBlowfish

  1. Инициализация состояния Blowfish
  2. XOR P-массива с солью и паролем
  3. Многократное переинициализирование состояния
  4. Повторение цикла 2^cost раз

В псевдокоде:

EksBlowfishSetup(cost, salt, password):
    state = InitState()
    state = ExpandKey(state, salt, password)

    repeat 2^cost times:
        state = ExpandKey(state, 0, password)
        state = ExpandKey(state, 0, salt)

    return state

ExpandKey

Функция ExpandKey выполняет:

  • последовательное XOR с входными данными
  • прогон через Blowfish encryption block
  • обновление P и S массивов

Именно здесь происходит основная вычислительная нагрузка.

Шифрование блока

bcrypt использует Blowfish в режиме ECB для шифрования фиксированного блока:

"OrpheanBeholderScryDoubt"

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

Формирование хеша

После завершения EksBlowfishSetup выполняется:

  1. Берётся фиксированный текстовый блок
  2. Шифруется текущим состоянием Blowfish
  3. Результат преобразуется в bcrypt-строку

Финальный формат:

$2a$<cost>$<salt><hash>

Где:

  • salt — 22 символа
  • hash — 31 символ (192 бита)

Реализация в bcrypt.js: структура кода

Внутри библиотеки можно выделить несколько ключевых модулей:

1. crypt.js

Отвечает за основную функцию:

  • hashSync
  • hash
  • compareSync
  • compare

2. blowfish.js

Содержит реализацию:

  • P-array
  • S-boxes
  • feistel network
  • encrypt/decrypt block

3. key expansion

Реализует:

  • EksBlowfishSetup
  • ExpandKey
  • key mixing

Синхронная и асинхронная модели

bcrypt.js поддерживает две модели выполнения:

Синхронная

Выполняется в основном потоке:

bcrypt.hashSync(password, saltRounds)

Минус — блокировка event loop.

Асинхронная

Реализована через setTimeout разбиение нагрузки:

bcrypt.hash(password, saltRounds, callback)

Внутри используется разбиение EksBlowfish на чанки, чтобы избежать долгой блокировки.

Cost factor и его влияние

Cost factor определяет количество итераций:

iterations = 2^cost

Примеры:

  • cost = 8 → 256 итераций
  • cost = 10 → 1024
  • cost = 12 → 4096
  • cost = 14 → 16384

Каждое увеличение на 1 удваивает время вычисления.

Внутренние особенности JavaScript реализации

bcrypt.js вынужден обходить ограничения JS:

1. Отсутствие 32-битных unsigned операций

Используются:

  • побитовые операции с маскированием
  • >>> 0 для приведения к uint32

2. Работа с массивами вместо фиксированных буферов

Blowfish требует быстрых операций, но JS использует:

  • обычные массивы
  • иногда TypedArray для ускорения

3. Эмуляция переполнения

Все арифметические операции приводятся к 32 битам:

a = (a + b) >>> 0;

Сравнение паролей

Функция compare не просто сравнивает строки, а:

  1. Пересчитывает хеш входного пароля
  2. Сравнивает с эталоном

Важно: используется сравнение без раннего выхода, чтобы снизить риск timing attacks.

safeEqual(hash1, hash2)

Защита от атак по времени

bcrypt.js реализует:

  • константное время сравнения
  • отсутствие ветвлений по символам хеша
  • фиксированную длину операций сравнения

Это снижает вероятность утечки информации через side-channel анализ.

Производительность и ограничения

Поскольку bcrypt.js написан на чистом JS:

  • он значительно медленнее нативных реализаций
  • нагрузка растёт экспоненциально с cost
  • рекомендуется использовать умеренные значения cost (10–12)

Основное ограничение — блокировка event loop при высоких cost значениях.

Формат хранения результата

Итоговая строка всегда кодируется в модифицированном base64:

  • алфавит отличается от стандартного RFC 4648
  • используется ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789

Это необходимо для совместимости с оригинальным bcrypt.

Внутренний поток выполнения hash

Полный цикл работы:

  1. генерация соли
  2. инициализация Blowfish
  3. EksBlowfishSetup
  4. шифрование фиксированного блока
  5. кодирование результата
  6. объединение salt + hash

Ключевая идея архитектуры

bcrypt.js построен вокруг одной идеи: искусственное замедление вычислений через дорогостоящую инициализацию ключей Blowfish, где основная стоимость перенесена в фазу setup, а не в сам процесс шифрования.

Это делает алгоритм устойчивым к перебору, поскольку каждая попытка проверки пароля требует полного пересчёта всей цепочки EksBlowfish.