Покрытие сценариев атак в тестах

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

Модель угроз как основа тестового покрытия

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

  • перебор (brute force);
  • словарные атаки (dictionary attacks);
  • радужные таблицы (rainbow tables);
  • атаки на основе утечек времени выполнения (timing attacks);
  • атаки на повторное использование соли;
  • попытки предвычисления хешей;
  • манипуляции с параметрами алгоритма (downgrade attacks).

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


Тестирование устойчивости к перебору

Перебор паролей моделируется через автоматизированные попытки подбора входных значений и анализ времени вычисления.

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

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


Проверка устойчивости к timing attacks

Атаки по времени выполнения требуют особого внимания, поскольку даже незначительные различия могут быть использованы для восстановления информации о пароле.

Тестирование включает статистическую оценку времени выполнения:

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

Основной критерий — отсутствие детерминированности результата без учёта скрытых параметров.


Фаззинг входных данных

Фаззинг применяется для выявления нестабильного поведения при неожиданных входах:

  • пустые строки;
  • Unicode-символы;
  • чрезвычайно длинные строки;
  • бинарные данные в виде строк.
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);
});

Комплексное моделирование атакующих сценариев

Интеграционные тесты объединяют несколько стратегий атак одновременно:

  • перебор + timing analysis;
  • словарь + фаззинг;
  • повторное использование + анализ соли.
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, позволяя оценивать её поведение в условиях активного противодействия эксплуатации уязвимостей.