Юнит-тесты для seal и unseal

Функции seal и unseal в Iron отвечают за криптографическую упаковку и распаковку данных. Их корректность критична, поскольку любые ошибки приводят либо к невозможности восстановить данные, либо к уязвимостям безопасности.

Юнит-тестирование этих операций строится вокруг трёх ключевых аспектов:

  • корректное шифрование и обратное восстановление данных
  • детерминированность поведения при одинаковых входных параметрах
  • устойчивость к некорректным или изменённым входным данным

Особенность заключается в том, что криптографические операции часто содержат элементы случайности (nonce, IV), поэтому тесты не должны зависеть от точного совпадения зашифрованной строки.


Базовая структура тестирования

Типичный набор тестов для seal и unseal включает проверку обратимости:

  • данные после seal должны успешно проходить через unseal
  • результат должен совпадать с исходным объектом

Пример структуры:

import Iron from '@hapi/iron';

const password = 'strong-password';
const options = Iron.defaults;

Тест обратимости:

test('seal и unseal восстанавливают исходные данные', async () => {
  const data = { user: 'alice', role: 'admin' };

  const sealed = await Iron.seal(data, password, options);
  const unsealed = await Iron.unseal(sealed, password, options);

  expect(unsealed).toEqual(data);
});

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


Проверка устойчивости к изменениям зашифрованного текста

Одно из ключевых требований к unseal — строгая проверка целостности. Любое изменение строки должно приводить к ошибке.

test('unseal должен отклонять изменённые данные', async () => {
  const data = { id: 123 };

  const sealed = await Iron.seal(data, password, options);

  const tampered = sealed.slice(0, -2) + 'aa';

  await expect(Iron.unseal(tampered, password, options))
    .rejects
    .toThrow();
});

Такая проверка гарантирует, что механизм аутентифицированного шифрования действительно защищает от модификации данных.


Проверка неправильного пароля

Критически важный сценарий — использование неверного ключа.

test('unseal должен отклонять неверный пароль', async () => {
  const data = { session: 'token' };

  const sealed = await Iron.seal(data, password, options);

  await expect(Iron.unseal(sealed, 'wrong-password', options))
    .rejects
    .toThrow();
});

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


Проверка стабильности сериализации

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

Особое внимание уделяется:

  • объектам с вложенными структурами
  • массивам
  • числам, строкам, null и boolean
test('корректная обработка сложных структур', async () => {
  const data = {
    user: {
      id: 1,
      tags: ['a', 'b', 'c'],
      meta: {
        active: true,
        score: 42
      }
    }
  };

  const sealed = await Iron.seal(data, password, options);
  const unsealed = await Iron.unseal(sealed, password, options);

  expect(unsealed).toEqual(data);
});

Тестирование поведения при изменении настроек

Iron позволяет задавать параметры (например, TTL или алгоритмы). Эти параметры должны быть строго согласованы между seal и unseal.

Ошибка в конфигурации должна приводить к отказу восстановления данных.

test('ошибка при несогласованных options', async () => {
  const data = { value: 100 };

  const sealed = await Iron.seal(data, password, options);

  const alteredOptions = {
    ...options,
    ttl: 1000
  };

  await expect(Iron.unseal(sealed, password, alteredOptions))
    .rejects
    .toThrow();
});

Тестирование TTL (времени жизни)

Если используется параметр времени жизни, необходимо проверить истечение срока действия.

Для этого применяется управление временем через fake timers.

jest.useFakeTimers();

test('данные истекают после ttl', async () => {
  const data = { session: 'active' };

  const optionsWithTtl = {
    ...Iron.defaults,
    ttl: 1000
  };

  const sealed = await Iron.seal(data, password, optionsWithTtl);

  jest.advanceTimersByTime(2000);

  await expect(Iron.unseal(sealed, password, optionsWithTtl))
    .rejects
    .toThrow();
});

Этот сценарий проверяет корректность работы механизма истечения срока действия защищённых данных.


Проверка устойчивости к повторному шифрованию

Повторное шифрование одного и того же объекта не должно приводить к идентичному результату из-за использования случайных параметров (например, IV).

test('seal создаёт разные результаты для одинаковых данных', async () => {
  const data = { id: 1 };

  const first = await Iron.seal(data, password, options);
  const second = await Iron.seal(data, password, options);

  expect(first).not.toBe(second);
});

При этом оба значения должны успешно расшифровываться:

test('оба зашифрованных значения корректно восстанавливаются', async () => {
  const data = { id: 1 };

  const first = await Iron.seal(data, password, options);
  const second = await Iron.seal(data, password, options);

  expect(await Iron.unseal(first, password, options)).toEqual(data);
  expect(await Iron.unseal(second, password, options)).toEqual(data);
});

Проверка обработки некорректных входных данных

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

  • пустая строка
  • null
  • undefined
  • невалидный формат sealed-строки
test('unseal отклоняет некорректные входные данные', async () => {
  await expect(Iron.unseal('', password, options))
    .rejects
    .toThrow();

  await expect(Iron.unseal(null, password, options))
    .rejects
    .toThrow();
});

Тестирование совместимости версий формата

При обновлении Iron важно учитывать обратную совместимость. Тесты могут включать фикстуры старых sealed-значений.

test('старый формат данных корректно расшифровывается', async () => {
  const legacySealed = 'Fe26.2...legacy-value...';

  const result = await Iron.unseal(legacySealed, password, options);

  expect(result).toBeDefined();
});

Проверка криптографической стабильности

Хотя точный результат seal не фиксируется, важно контролировать структуру выходной строки:

  • наличие всех обязательных частей формата
  • корректная сериализация версии
  • отсутствие утечек исходных данных в открытом виде
test('sealed строка соответствует формату', async () => {
  const data = { test: true };

  const sealed = await Iron.seal(data, password, options);

  expect(typeof sealed).toBe('string');
  expect(sealed.split('.').length).toBeGreaterThan(1);
});