Отладка через разбор структуры хеш-строки вручную

Большинство современных библиотек для хеширования паролей сохраняют результат не в виде «голого» хеша, а как структурированную строку, содержащую:

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

Такой подход позволяет:

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

Типичная структура выглядит так:

$algorithm$parameters$salt$hash

Для разных алгоритмов формат отличается, но принцип остаётся одинаковым.


Разбор структуры bcrypt-хеша

Пример строки bcrypt:

$2b$12$LQv3c1yqBWVHxkd0LQh3U.O7B9Q9sM1uW6G9Kx6YFQ1p7A1kJwY9e

Разделение по сегментам:

Сегмент Значение
$2b$ версия bcrypt
12 cost factor
LQv3c1yqBWVHxkd0LQh3U. соль
O7B9Q9sM1uW6G9Kx6YFQ1p7A1kJwY9e хеш

Извлечение частей bcrypt вручную

Простейший разбор строки

const hash = '$2b$12$LQv3c1yqBWVHxkd0LQh3U.O7B9Q9sM1uW6G9Kx6YFQ1p7A1kJwY9e';

const parts = hash.split('$');

console.log(parts);

Результат:

[
  '',
  '2b',
  '12',
  'LQv3c1yqBWVHxkd0LQh3U.O7B9Q9sM1uW6G9Kx6YFQ1p7A1kJwY9e'
]

Последний сегмент содержит сразу:

  • соль;
  • итоговый digest.

Выделение соли и хеша в bcrypt

bcrypt использует фиксированные размеры:

Элемент Длина
соль 22 символа
хеш 31 символ

Пример:

const body = parts[3];

const salt = body.substring(0, 22);
const digest = body.substring(22);

console.log('Salt:', salt);
console.log('Digest:', digest);

Проверка корректности bcrypt-строки

При повреждении строки библиотека часто выдаёт малоинформативную ошибку:

Invalid salt

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

Проверка длины

function validateBcrypt(hash) {
  return hash.length === 60;
}

console.log(validateBcrypt(hash));

bcrypt-хеш всегда имеет длину 60 символов.


Проверка версии алгоритма

Допустимые версии:

  • 2a
  • 2b
  • 2y

Проверка:

function getVersion(hash) {
  return hash.split('$')[1];
}

console.log(getVersion(hash));

Анализ параметра cost

Параметр cost определяет вычислительную сложность.

const rounds = Number(hash.split('$')[2]);

console.log(rounds);

Возможные проблемы

Значение Последствие
слишком маленькое слабая защита
слишком большое перегрузка CPU
NaN повреждённая строка

Поиск ошибок миграции bcrypt

Частая проблема — потеря символов при переносе данных.

Симптом

Error: Invalid salt version

Причина

В базе хранится:

2b$12$...

Вместо:

$2b$12$...

Диагностика

if (!hash.startsWith('$')) {
  console.error('Повреждён формат bcrypt');
}

Анализ кодировки

Иногда строка повреждается из-за:

  • UTF-8/UTF-16 преобразований;
  • неправильного JSON-сериализатора;
  • URL encoding;
  • обрезки в VARCHAR.

Проверка длины после чтения из БД

console.log(Buffer.byteLength(hash));

Разбор Argon2-хеша вручную

Пример:

$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$w8Y7F0jvX8f8V0J6nD8Y7A

Структура:

Часть Значение
argon2id тип алгоритма
v=19 версия
m=65536,t=3,p=4 параметры
c29tZXNhbHQ соль
w8Y7F0jvX8f8V0J6nD8Y7A хеш

Разделение Argon2-строки

const hash = '$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$w8Y7F0jvX8f8V0J6nD8Y7A';

const parts = hash.split('$');

console.log(parts);

Результат:

[
  '',
  'argon2id',
  'v=19',
  'm=65536,t=3,p=4',
  'c29tZXNhbHQ',
  'w8Y7F0jvX8f8V0J6nD8Y7A'
]

Разбор параметров Argon2

Параметры находятся в CSV-подобном формате.

const rawParams = parts[3];

const params = rawParams.split(',');

console.log(params);

Результат:

[
  'm=65536',
  't=3',
  'p=4'
]

Преобразование параметров в объект

const parsed = Object.fromEntries(
  params.map(item => item.split('='))
);

console.log(parsed);

Результат:

{
  m: '65536',
  t: '3',
  p: '4'
}

Диагностика неверных параметров Argon2

Пример ошибки

Decoding failed

Причины:

  • отсутствует параметр;
  • повреждён разделитель;
  • неверная base64-кодировка;
  • отсутствует версия.

Проверка обязательных полей

function validateArgon2(parts) {
  return (
    parts.length === 6 &&
    parts[1].startsWith('argon2') &&
    parts[2].startsWith('v=')
  );
}

Проверка base64-сегментов

Соль и digest обычно кодируются в base64.

Проверка:

function isBase64(str) {
  return /^[A-Za-z0-9+/]+$/.test(str);
}

Проблемы padding в base64

Некоторые реализации:

  • используют padding;
  • убирают =;
  • переходят на URL-safe base64.

Симптомы

Invalid encoding

Диагностика

console.log(parts[4]);
console.log(parts[5]);

Ручная проверка PBKDF2-строки

Некоторые системы используют собственный формат:

pbkdf2:sha256:100000:salt:hash

Разбор:

const hash = 'pbkdf2:sha256:100000:salt:hash';

const [
  type,
  digest,
  iterations,
  salt,
  result
] = hash.split(':');

console.log({
  type,
  digest,
  iterations,
  salt,
  result
});

Проверка числа итераций

const count = Number(iterations);

if (Number.isNaN(count)) {
  throw new Error('Некорректное число итераций');
}

Поиск повреждений строки

Лишние пробелы

console.log(JSON.stringify(hash));

Позволяет увидеть:

  • \n
  • \r
  • пробелы
  • табуляцию

Проверка скрытых символов

for (const char of hash) {
  console.log(char, char.charCodeAt(0));
}

Полезно при:

  • копировании из терминала;
  • чтении из CSV;
  • импорте из Excel.

Диагностика ошибок через регулярные выражения

Проверка bcrypt

const bcryptRegex =
  /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/;

console.log(bcryptRegex.test(hash));

Проверка Argon2

const argonRegex =
  /^\$argon2(id|i|d)\$/;

console.log(argonRegex.test(hash));

Создание универсального анализатора

function inspectHash(hash) {
  if (hash.startsWith('$2')) {
    return inspectBcrypt(hash);
  }

  if (hash.startsWith('$argon2')) {
    return inspectArgon2(hash);
  }

  return {
    type: 'unknown'
  };
}

Анализ bcrypt внутри функции

function inspectBcrypt(hash) {
  const parts = hash.split('$');

  return {
    algorithm: 'bcrypt',
    version: parts[1],
    rounds: Number(parts[2]),
    salt: parts[3].substring(0, 22),
    digest: parts[3].substring(22)
  };
}

Анализ Argon2 внутри функции

function inspectArgon2(hash) {
  const parts = hash.split('$');

  const params = Object.fromEntries(
    parts[3]
      .split(',')
      .map(x => x.split('='))
  );

  return {
    algorithm: parts[1],
    version: parts[2],
    params,
    salt: parts[4],
    digest: parts[5]
  };
}

Вывод отладочной информации

console.dir(inspectHash(hash), {
  depth: null,
  colors: true
});

Диагностика несовместимости библиотек

Разные библиотеки могут:

  • использовать разные версии алгоритмов;
  • кодировать соль по-разному;
  • менять формат параметров;
  • по-разному сериализовать строку.

Пример

Одна библиотека:

$2a$

Другая ожидает:

$2b$

Проверка поддержки версии

const supported = ['2a', '2b', '2y'];

if (!supported.includes(version)) {
  throw new Error('Версия bcrypt не поддерживается');
}

Отладка ошибок сравнения паролей

Симптом

compare() всегда возвращает false

Возможные причины

Причина Описание
повреждён hash потеря символов
разная кодировка UTF-8 vs UTF-16
двойное хеширование пароль уже был захеширован
неправильная миграция перенос между библиотеками
лишние символы \n, пробелы

Проверка двойного хеширования

Ошибка:

bcrypt.hash(
  bcrypt.hashSync(password, 10),
  10
);

Диагностика:

if (password.startsWith('$2b$')) {
  console.warn('Возможно двойное хеширование');
}

Отладка через побайтовый анализ

const bytes = Buffer.from(hash);

console.log(bytes);

Просмотр hex-представления

console.log(bytes.toString('hex'));

Позволяет обнаружить:

  • скрытые символы;
  • ошибки кодировки;
  • невалидные байты.

Проверка длины сегментов

bcrypt

function validateBcryptSegments(hash) {
  const parts = hash.split('$');

  const body = parts[3];

  return {
    saltLength: body.substring(0, 22).length,
    digestLength: body.substring(22).length
  };
}

Анализ производительности через параметры хеша

Во время отладки важно проверять:

  • количество итераций;
  • объём памяти;
  • parallelism;
  • cost factor.

Пример анализа Argon2

const memory = Number(parsed.m);

if (memory < 4096) {
  console.warn('Слишком маленький memory cost');
}

Создание диагностического отчёта

function createReport(hash) {
  const info = inspectHash(hash);

  return {
    timestamp: new Date().toISOString(),
    info
  };
}

Сериализация отчёта

console.log(
  JSON.stringify(createReport(hash), null, 2)
);

Отладка через сравнение двух хешей

function compareStructure(a, b) {
  return {
    sameLength: a.length === b.length,
    samePrefix: a.slice(0, 4) === b.slice(0, 4)
  };
}

Проверка целостности строки после БД

const original = hash;
const loaded = row.password_hash;

console.log(original === loaded);

Обнаружение обрезки VARCHAR

Частая ошибка:

VARCHAR(50)

Для bcrypt требуется минимум:

VARCHAR(60)

Для Argon2 — значительно больше.


Диагностика через логирование сегментов

parts.forEach((part, index) => {
  console.log(index, part);
});

Отладка миграции между алгоритмами

При переходе:

bcrypt -> argon2

в системе могут одновременно существовать оба формата.

Определение типа:

function detect(hash) {
  if (hash.startsWith('$2')) {
    return 'bcrypt';
  }

  if (hash.startsWith('$argon2')) {
    return 'argon2';
  }

  return 'unknown';
}

Проверка хеша перед compare()

function ensureHash(hash) {
  if (typeof hash !== 'string') {
    throw new TypeError('Hash должен быть строкой');
  }

  if (!hash.length) {
    throw new Error('Пустой hash');
  }
}

Использование try/catch для диагностики

try {
  await bcrypt.compare(password, hash);
} catch (err) {
  console.error({
    message: err.message,
    stack: err.stack,
    hash
  });
}

Выделение уровней ошибок

Уровень Тип проблемы
формат неверная структура
кодировка повреждение байтов
параметры невалидный cost
версия неподдерживаемый алгоритм
хранение обрезка БД
миграция несовместимость библиотек

Практический подход к ручной диагностике

Последовательность проверки:

  1. Проверка длины строки.
  2. Проверка префикса алгоритма.
  3. Разделение по delimiters.
  4. Проверка параметров.
  5. Проверка длины соли.
  6. Проверка digest.
  7. Анализ кодировки.
  8. Проверка данных из БД.
  9. Проверка совместимости библиотек.
  10. Побайтовый анализ при необходимости.

Минимальный универсальный инспектор

function debugPasswordHash(hash) {
  const info = {
    raw: hash,
    length: hash.length
  };

  if (hash.startsWith('$2')) {
    const parts = hash.split('$');

    info.type = 'bcrypt';
    info.version = parts[1];
    info.rounds = parts[2];
  }

  if (hash.startsWith('$argon2')) {
    const parts = hash.split('$');

    info.type = parts[1];
    info.version = parts[2];
    info.params = parts[3];
  }

  return info;
}