При тестировании библиотек хеширования паролей ключевая задача выходит далеко за рамки проверки корректности API. Основной акцент смещается на моделирование поведения злоумышленника и оценку устойчивости алгоритмов в условиях реальных атак. В контексте JavaScript-библиотеки Password-hash тестирование становится инструментом верификации не только функциональности, но и криптографической и эксплуатационной надёжности.
Перед формированием тестов фиксируется модель угроз, в рамках которой библиотека рассматривается как компонент системы аутентификации. Типовые классы атак, которые должны быть отражены в тестах:
Каждый из этих сценариев трансформируется в набор тест-кейсов, проверяющих не только результат, но и устойчивость свойств системы.
Перебор паролей моделируется через автоматизированные попытки подбора входных значений и анализ времени вычисления.
import { hashPassword, verifyPassword } from 'password-hash';
test('устойчивость к перебору не раскрывает структуру хеша', () => {
const hash = hashPassword('correct-horse-battery-staple');
const attempts = [
'password1',
'123456',
'admin',
'correct-horse-battery',
'correct-horse-battery-staple'
];
attempts.forEach(attempt => {
const result = verifyPassword(attempt, hash);
expect(typeof result).toBe('boolean');
});
});
Важный аспект покрытия заключается в том, что библиотека не должна демонстрировать различий в поведении, которые могут ускорить перебор (например, различное время ответа для частично совпадающих строк).
Словарные атаки проверяются через массовую подачу распространённых паролей и анализ результата хеширования. Основной критерий — отсутствие обратимости и предсказуемости.
test('словари не дают возможности восстановления пароля', () => {
const commonPasswords = ['123456', 'qwerty', 'password', 'letmein'];
const hashes = commonPasswords.map(hashPassword);
hashes.forEach((hash, i) => {
expect(verifyPassword(commonPasswords[i], hash)).toBe(true);
expect(verifyPassword('wrong-password', hash)).toBe(false);
});
});
Дополнительно проверяется, что одинаковые пароли не приводят к одинаковым хешам при корректной реализации соли.
Одним из критических сценариев является предотвращение атак через предвычисленные таблицы. Для этого тестируется уникальность результата хеширования при одинаковом входе.
test('разные соли дают разные хеши для одного пароля', () => {
const password = 'secure-password';
const hash1 = hashPassword(password);
const hash2 = hashPassword(password);
expect(hash1).not.toBe(hash2);
});
Также проверяется невозможность восстановления связи между хешами и исходными значениями без знания соли.
Атаки по времени выполнения требуют особого внимания, поскольку даже незначительные различия могут быть использованы для восстановления информации о пароле.
Тестирование включает статистическую оценку времени выполнения:
test('время проверки не зависит от правильности части пароля', () => {
const hash = hashPassword('constant-time-password');
const timings = [];
const samples = [
'a',
'ab',
'abc',
'constant-time-password'
];
samples.forEach(sample => {
const start = performance.now();
verifyPassword(sample, hash);
const end = performance.now();
timings.push(end - start);
});
const max = Math.max(...timings);
const min = Math.min(...timings);
expect(max - min).toBeLessThan(5);
});
Цель теста — убедиться, что различия во времени не коррелируют с частичным совпадением строк.
Повторное использование соли между разными паролями снижает криптостойкость. Тесты фиксируют генерацию уникальных значений:
test('каждый хеш содержит уникальную соль', () => {
const hashA = hashPassword('passwordA');
const hashB = hashPassword('passwordB');
const saltA = extractSalt(hashA);
const saltB = extractSalt(hashB);
expect(saltA).not.toBe(saltB);
});
Дополнительно проверяется, что извлечение соли не позволяет восстановить пароль.
Некоторые реализации позволяют варьировать параметры сложности (iterations, cost factor). Тесты должны фиксировать невозможность снижения безопасности через внешние параметры.
test('нельзя снизить сложность ниже минимального порога', () => {
const weakHash = hashPassword('test', { cost: 1 });
const strongHash = hashPassword('test', { cost: 10 });
expect(extractCost(weakHash)).toBeGreaterThanOrEqual(10);
expect(extractCost(strongHash)).toBeGreaterThanOrEqual(10);
});
Таким образом предотвращается деградация безопасности при некорректной конфигурации.
Предвычисление хешей становится эффективным только при отсутствии соли или её слабой реализации. Тесты моделируют попытки сопоставления заранее подготовленных значений.
test('невозможность предвычисленного совпадения', () => {
const password = 'unique-password-123';
const precomputed = new Map();
precomputed.set('unique-password-123', 'fake-hash');
const hash = hashPassword(password);
expect(precomputed.get(password)).not.toBe(hash);
});
Основной критерий — отсутствие детерминированности результата без учёта скрытых параметров.
Фаззинг применяется для выявления нестабильного поведения при неожиданных входах:
test('фаззинг входных данных не вызывает сбой', () => {
const inputs = [
'',
' ',
'\u0000\u0000',
'a'.repeat(10000),
'пароль?密码パスワード'
];
inputs.forEach(input => {
expect(() => hashPassword(input)).not.toThrow();
});
});
Цель — исключить утечки информации через ошибки исполнения.
Хеш часто сохраняется в строковом виде, поэтому важно проверить обратимость структуры:
test('сериализация и десериализация хеша сохраняют целостность', () => {
const password = 'serialization-test';
const hash = hashPassword(password);
const parsed = parseHash(hash);
expect(verifyPassword(password, parsed)).toBe(true);
});
Ошибки в сериализации могут приводить к обходу проверки пароля.
В некоторых системах злоумышленник может попытаться использовать перехваченный хеш как токен аутентификации.
test('хеш нельзя использовать как пароль напрямую', () => {
const hash = hashPassword('secure');
expect(verifyPassword(hash, hash)).toBe(false);
});
Интеграционные тесты объединяют несколько стратегий атак одновременно:
test('комбинированная атака не снижает безопасность', () => {
const hash = hashPassword('complex-password');
const attackVectors = [
'password',
'complex',
'complex-password',
'Complex-Password!',
'complex-password '
];
attackVectors.forEach(v => {
expect(verifyPassword(v, hash)).toBe(typeof true);
});
});
Систематическое покрытие атакующих сценариев формирует не только набор тестов, но и формальную модель устойчивости библиотеки Password-hash, позволяя оценивать её поведение в условиях активного противодействия эксплуатации уязвимостей.