Юнит-тесты для шифрования и расшифровки

Юнит-тесты для операций шифрования и расшифровки выполняют несколько ключевых задач:

  • Проверка корректности алгоритмов — гарантируется, что зашифрованные данные могут быть корректно восстановлены
  • Стабильность API — защита от регрессий при обновлении кода
  • Контроль параметров — правильная работа с ключами, IV, режимами и кодировками
  • Детерминированность (где применимо) — одинаковые входные данные дают ожидаемый результат

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


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

Для примеров используется библиотека Crypto-js и тестовый фреймворк (например, Jest или Mocha). Основные сценарии:

  1. Шифрование → расшифровка → совпадение с исходными данными
  2. Проверка на разных входных строках
  3. Проверка обработки ошибок
  4. Проверка кодировок

Проверка симметричного шифрования (AES)

Реализация функции

const CryptoJS = require('crypto-js');

function encryptAES(plaintext, key) {
  return CryptoJS.AES.encrypt(plaintext, key).toString();
}

function decryptAES(ciphertext, key) {
  const bytes = CryptoJS.AES.decrypt(ciphertext, key);
  return bytes.toString(CryptoJS.enc.Utf8);
}

Юнит-тест

test('AES: шифрование и расшифровка возвращают исходную строку', () => {
  const key = 'secret-key';
  const message = 'Hello, Crypto!';

  const encrypted = encryptAES(message, key);
  const decrypted = decryptAES(encrypted, key);

  expect(decrypted).toBe(message);
});

Тестирование с фиксированным IV

По умолчанию Crypto-js генерирует случайный IV, что делает результат шифрования недетерминированным. Для точного сравнения используется фиксированный IV.

function encryptWithIV(plaintext, key, iv) {
  return CryptoJS.AES.encrypt(plaintext, key, { iv }).toString();
}
test('AES: детерминированное шифрование с фиксированным IV', () => {
  const key = CryptoJS.enc.Utf8.parse('1234567890123456');
  const iv = CryptoJS.enc.Utf8.parse('6543210987654321');
  const message = 'Test message';

  const encrypted1 = encryptWithIV(message, key, iv);
  const encrypted2 = encryptWithIV(message, key, iv);

  expect(encrypted1).toBe(encrypted2);
});

Проверка некорректного ключа

test('AES: расшифровка с неверным ключом не совпадает', () => {
  const message = 'Sensitive data';

  const encrypted = encryptAES(message, 'correct-key');
  const decrypted = decryptAES(encrypted, 'wrong-key');

  expect(decrypted).not.toBe(message);
});

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

test('AES: пустая строка', () => {
  const encrypted = encryptAES('', 'key');
  const decrypted = decryptAES(encrypted, 'key');

  expect(decrypted).toBe('');
});
test('AES: длинная строка', () => {
  const message = 'A'.repeat(10000);

  const encrypted = encryptAES(message, 'key');
  const decrypted = decryptAES(encrypted, 'key');

  expect(decrypted).toBe(message);
});

Проверка кодировок

Crypto-js работает с различными кодировками, что важно учитывать в тестах.

test('UTF-8 кодировка сохраняется корректно', () => {
  const message = 'Привет, мир!';

  const encrypted = encryptAES(message, 'key');
  const decrypted = decryptAES(encrypted, 'key');

  expect(decrypted).toBe(message);
});

Тестирование хеш-функций

Хеш-функции должны быть детерминированными: одинаковый ввод — одинаковый результат.

function hashSHA256(message) {
  return CryptoJS.SHA256(message).toString();
}
test('SHA256: одинаковые входные данные дают одинаковый хеш', () => {
  const hash1 = hashSHA256('data');
  const hash2 = hashSHA256('data');

  expect(hash1).toBe(hash2);
});
test('SHA256: разные входные данные дают разные хеши', () => {
  const hash1 = hashSHA256('data1');
  const hash2 = hashSHA256('data2');

  expect(hash1).not.toBe(hash2);
});

Проверка HMAC

function hmacSHA256(message, key) {
  return CryptoJS.HmacSHA256(message, key).toString();
}
test('HMAC: одинаковые входные данные и ключ дают одинаковый результат', () => {
  const result1 = hmacSHA256('data', 'key');
  const result2 = hmacSHA256('data', 'key');

  expect(result1).toBe(result2);
});

Проверка устойчивости к изменению данных

test('HMAC: изменение данных меняет результат', () => {
  const result1 = hmacSHA256('data', 'key');
  const result2 = hmacSHA256('data-modified', 'key');

  expect(result1).not.toBe(result2);
});

Мокирование случайных значений

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

jest.spyOn(CryptoJS.lib.WordArray, 'random').mockReturnValue(
  CryptoJS.enc.Hex.parse('00000000000000000000000000000000')
);

Это позволяет:

  • стабилизировать результаты
  • сравнивать ожидаемые значения
  • упрощать отладку

Проверка формата выходных данных

test('Результат шифрования является строкой', () => {
  const encrypted = encryptAES('data', 'key');

  expect(typeof encrypted).toBe('string');
});

Тестирование ошибок

test('Расшифровка некорректного ciphertext', () => {
  const result = decryptAES('invalid-data', 'key');

  expect(result).toBe('');
});

Структурирование тестов

Рекомендуемая структура:

describe('AES Encryption', () => {
  test('...', () => {});
});

describe('Hashing', () => {
  test('...', () => {});
});

Разделение облегчает:

  • навигацию
  • поддержку
  • расширение тестов

Покрытие тестами

Ключевые аспекты покрытия:

  • разные алгоритмы (AES, DES, Rabbit)
  • различные ключи
  • разные кодировки
  • edge cases
  • ошибки

Инструменты:

  • Istanbul / nyc
  • встроенные средства Jest

Частые ошибки при тестировании

  • Сравнение недетерминированных результатов
  • Игнорирование IV
  • Проверка только «счастливого пути»
  • Отсутствие тестов на ошибки
  • Неправильная работа с кодировками

Практика тестирования на основе свойств

Вместо проверки конкретного значения:

  • decrypt(encrypt(x)) === x
  • hash(x) !== hash(y) при x !== y

Пример:

test('Свойство обратимости', () => {
  const message = 'random text';
  const key = 'key';

  expect(decryptAES(encryptAES(message, key), key)).toBe(message);
});

Автоматизация и CI

Юнит-тесты интегрируются в CI/CD:

  • запуск при каждом коммите
  • предотвращение регрессий
  • контроль качества криптографии

Расширенные сценарии

  • тестирование производительности (benchmark)
  • тестирование потоковых данных
  • тестирование в браузере и Node.js
  • проверка совместимости между платформами

Рекомендации по качеству тестов

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

Проверка на регрессии

Каждый найденный баг сопровождается тестом:

test('Регрессия: некорректная расшифровка UTF-8', () => {
  const message = 'тест';

  const encrypted = encryptAES(message, 'key');
  const decrypted = decryptAES(encrypted, 'key');

  expect(decrypted).toBe(message);
});

Итоговая стратегия

Комплексный подход к тестированию Crypto-js включает:

  • функциональные тесты
  • property-based тесты
  • тесты ошибок
  • контроль детерминированности
  • покрытие всех алгоритмов и сценариев

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