Большинство современных библиотек для хеширования паролей сохраняют результат не в виде «голого» хеша, а как структурированную строку, содержащую:
Такой подход позволяет:
Типичная структура выглядит так:
$algorithm$parameters$salt$hash
Для разных алгоритмов формат отличается, но принцип остаётся одинаковым.
Пример строки bcrypt:
$2b$12$LQv3c1yqBWVHxkd0LQh3U.O7B9Q9sM1uW6G9Kx6YFQ1p7A1kJwY9e
Разделение по сегментам:
| Сегмент | Значение |
|---|---|
$2b$ |
версия bcrypt |
12 |
cost factor |
LQv3c1yqBWVHxkd0LQh3U. |
соль |
O7B9Q9sM1uW6G9Kx6YFQ1p7A1kJwY9e |
хеш |
const hash = '$2b$12$LQv3c1yqBWVHxkd0LQh3U.O7B9Q9sM1uW6G9Kx6YFQ1p7A1kJwY9e';
const parts = hash.split('$');
console.log(parts);
Результат:
[
'',
'2b',
'12',
'LQv3c1yqBWVHxkd0LQh3U.O7B9Q9sM1uW6G9Kx6YFQ1p7A1kJwY9e'
]
Последний сегмент содержит сразу:
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);
При повреждении строки библиотека часто выдаёт малоинформативную ошибку:
Invalid salt
Ручная диагностика позволяет быстро определить проблему.
function validateBcrypt(hash) {
return hash.length === 60;
}
console.log(validateBcrypt(hash));
bcrypt-хеш всегда имеет длину 60 символов.
Допустимые версии:
2a2b2yПроверка:
function getVersion(hash) {
return hash.split('$')[1];
}
console.log(getVersion(hash));
Параметр cost определяет вычислительную сложность.
const rounds = Number(hash.split('$')[2]);
console.log(rounds);
| Значение | Последствие |
|---|---|
| слишком маленькое | слабая защита |
| слишком большое | перегрузка CPU |
| NaN | повреждённая строка |
Частая проблема — потеря символов при переносе данных.
Error: Invalid salt version
В базе хранится:
2b$12$...
Вместо:
$2b$12$...
if (!hash.startsWith('$')) {
console.error('Повреждён формат bcrypt');
}
Иногда строка повреждается из-за:
console.log(Buffer.byteLength(hash));
Пример:
$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$w8Y7F0jvX8f8V0J6nD8Y7A
Структура:
| Часть | Значение |
|---|---|
argon2id |
тип алгоритма |
v=19 |
версия |
m=65536,t=3,p=4 |
параметры |
c29tZXNhbHQ |
соль |
w8Y7F0jvX8f8V0J6nD8Y7A |
хеш |
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'
]
Параметры находятся в 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'
}
Decoding failed
Причины:
function validateArgon2(parts) {
return (
parts.length === 6 &&
parts[1].startsWith('argon2') &&
parts[2].startsWith('v=')
);
}
Соль и digest обычно кодируются в base64.
Проверка:
function isBase64(str) {
return /^[A-Za-z0-9+/]+$/.test(str);
}
Некоторые реализации:
=;Invalid encoding
console.log(parts[4]);
console.log(parts[5]);
Некоторые системы используют собственный формат:
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\rfor (const char of hash) {
console.log(char, char.charCodeAt(0));
}
Полезно при:
const bcryptRegex =
/^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/;
console.log(bcryptRegex.test(hash));
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'
};
}
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)
};
}
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);
console.log(bytes.toString('hex'));
Позволяет обнаружить:
function validateBcryptSegments(hash) {
const parts = hash.split('$');
const body = parts[3];
return {
saltLength: body.substring(0, 22).length,
digestLength: body.substring(22).length
};
}
Во время отладки важно проверять:
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(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';
}
function ensureHash(hash) {
if (typeof hash !== 'string') {
throw new TypeError('Hash должен быть строкой');
}
if (!hash.length) {
throw new Error('Пустой hash');
}
}
try {
await bcrypt.compare(password, hash);
} catch (err) {
console.error({
message: err.message,
stack: err.stack,
hash
});
}
| Уровень | Тип проблемы |
|---|---|
| формат | неверная структура |
| кодировка | повреждение байтов |
| параметры | невалидный cost |
| версия | неподдерживаемый алгоритм |
| хранение | обрезка БД |
| миграция | несовместимость библиотек |
Последовательность проверки:
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;
}