Модульные тесты для операций шифрования и подписи

Модульные тесты криптографических операций требуют особого подхода: обычные стратегии проверки бизнес-логики здесь недостаточны, поскольку корректность определяется не только поведением кода, но и строгим соответствием математическим свойствам алгоритмов и заранее известным тестовым векторам. В случае TweetNaCl.js / nacl.js это особенно важно, так как библиотека реализует низкоуровневые примитивы (Curve25519, XSalsa20-Poly1305, Ed25519), где любая регрессия может привести к несовместимости или уязвимости.

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

  • результат должен совпадать с официальными тест-векторами;
  • операции должны быть детерминированными при фиксированных входных данных;
  • недопустимы скрытые зависимости от времени, случайности или платформы;
  • ошибки должны проявляться явно (fail-fast), а не приводить к «почти правильным» результатам.

В TweetNaCl.js ключевой принцип — работа с Uint8Array. Это накладывает обязательство тестировать не строки и абстрактные типы, а именно бинарные представления данных.

Организация тестовой среды

Для модульных тестов обычно используют Jest или Mocha. Важно обеспечить воспроизводимость:

  • фиксированная версия Node.js;
  • отсутствие параллельного влияния случайности;
  • контроль над генерацией nonce и ключей;
  • изоляция тестов (никакого общего состояния между ними).

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

import nacl from 'tweetnacl';

// фиктивный RNG для тестов
nacl.setPRNG((x, n) => {
  for (let i = 0; i < n; i++) {
    x[i] = 1; // детерминированное заполнение
  }
});

Такой подход позволяет избежать флаки-тестов, но используется только для unit-тестирования, не для production.

Тестирование симметричного шифрования (secretbox)

Операции nacl.secretbox и nacl.secretbox.open проверяются через:

  • корректное шифрование и расшифрование;
  • устойчивость к подмене данных;
  • чувствительность к nonce;
  • детерминированные тест-векторы.

Базовый тест шифрования

import nacl from 'tweetnacl';

test('secretbox encrypt/decrypt roundtrip', () => {
  const key = new Uint8Array(32).fill(2);
  const nonce = new Uint8Array(24).fill(3);
  const message = new TextEncoder().encode('hello crypto');

  const encrypted = nacl.secretbox(message, nonce, key);
  const decrypted = nacl.secretbox.open(encrypted, nonce, key);

  expect(decrypted).not.toBeNull();
  expect(new TextDecoder().decode(decrypted)).toBe('hello crypto');
});

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

test('secretbox detects tampering', () => {
  const key = new Uint8Array(32).fill(2);
  const nonce = new Uint8Array(24).fill(3);
  const message = new Uint8Array([1, 2, 3, 4]);

  const encrypted = nacl.secretbox(message, nonce, key);

  encrypted[0] ^= 1;

  const result = nacl.secretbox.open(encrypted, nonce, key);
  expect(result).toBeNull();
});

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

Тестирование публичного шифрования (box)

nacl.box требует пары ключей (public/private). Здесь тестирование усложняется:

  • необходимо генерировать ключевые пары;
  • проверять совместимость между отправителем и получателем;
  • учитывать направление шифрования.

Базовый сценарий обмена

test('box key exchange encryption', () => {
  const alice = nacl.box.keyPair();
  const bob = nacl.box.keyPair();

  const nonce = new Uint8Array(24).fill(7);
  const message = new TextEncoder().encode('secure message');

  const encrypted = nacl.box(message, nonce, bob.publicKey, alice.secretKey);
  const decrypted = nacl.box.open(encrypted, nonce, alice.publicKey, bob.secretKey);

  expect(decrypted).not.toBeNull();
  expect(new TextDecoder().decode(decrypted)).toBe('secure message');
});

Здесь важно тестировать обе стороны: отправитель и получатель должны быть симметрично валидны.

Проверка несовпадения ключей

test('box fails with wrong keypair', () => {
  const alice = nacl.box.keyPair();
  const bob = nacl.box.keyPair();
  const eve = nacl.box.keyPair();

  const nonce = new Uint8Array(24).fill(1);
  const message = new Uint8Array([10, 20, 30]);

  const encrypted = nacl.box(message, nonce, bob.publicKey, alice.secretKey);

  const result = nacl.box.open(encrypted, nonce, alice.publicKey, eve.secretKey);
  expect(result).toBeNull();
});

Тестирование цифровых подписей (sign / sign.detached)

Подписи в TweetNaCl.js основаны на Ed25519 и требуют строгой проверки:

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

Базовая проверка подписи

test('sign and verify detached', () => {
  const keyPair = nacl.sign.keyPair();
  const message = new TextEncoder().encode('signed data');

  const signature = nacl.sign.detached(message, keyPair.secretKey);
  const valid = nacl.sign.detached.verify(
    message,
    signature,
    keyPair.publicKey
  );

  expect(valid).toBe(true);
});

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

test('signature fails on modified message', () => {
  const keyPair = nacl.sign.keyPair();
  const message = new Uint8Array([1, 2, 3]);

  const signature = nacl.sign.detached(message, keyPair.secretKey);

  message[0] = 9;

  const valid = nacl.sign.detached.verify(
    message,
    signature,
    keyPair.publicKey
  );

  expect(valid).toBe(false);
});

Тестирование через известные векторы

Самый надёжный способ проверки криптографической библиотеки — использование официальных test vectors (например, из NaCl reference implementation или libsodium).

Типичный подход:

  • фиксированные ключи;
  • фиксированные nonce;
  • заранее известный ciphertext;
  • сравнение бинарных массивов побайтно.
test('secretbox matches known vector', () => {
  const key = hexToUint8Array('1c9240a5eb55d38af333888604f6b5f0...');
  const nonce = hexToUint8Array('0000000000000000000000000000000000000000');
  const message = hexToUint8Array('48656c6c6f');

  const encrypted = nacl.secretbox(message, nonce, key);

  expect(encrypted).toEqual(hexToUint8Array('expectedcipherhex...'));
});

Любое расхождение означает либо ошибку реализации, либо неправильную интерпретацию байтов.

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

Nonce в NaCl не обязан быть секретным, но обязан быть уникальным. Это создаёт важную категорию тестов:

  • проверка реакции на повтор nonce;
  • проверка корректности длины (24 байта);
  • проверка поведения при нулевых значениях.
test('nonce must be 24 bytes', () => {
  const key = new Uint8Array(32).fill(1);
  const badNonce = new Uint8Array(12);

  const message = new Uint8Array([1, 2, 3]);

  expect(() => {
    nacl.secretbox(message, badNonce, key);
  }).toThrow();
});

Хотя TweetNaCl.js часто не бросает исключения, а ведёт себя undefined-safe, тесты должны фиксировать фактическое поведение библиотеки.

Property-based тестирование

Помимо фиксированных кейсов полезно проверять свойства:

  • decrypt(encrypt(m)) = m
  • изменение одного байта ломает результат
  • подпись всегда детерминирована для фиксированного ключа и сообщения

Пример:

test('roundtrip property', () => {
  const key = nacl.randomBytes(32);
  const nonce = nacl.randomBytes(24);
  const message = nacl.randomBytes(64);

  const encrypted = nacl.secretbox(message, nonce, key);
  const decrypted = nacl.secretbox.open(encrypted, nonce, key);

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

Тестирование на границах входных данных

Криптографические функции чувствительны к размеру:

  • пустые массивы;
  • максимально допустимые длины;
  • некорректные типы данных.
test('empty message handling', () => {
  const key = new Uint8Array(32).fill(1);
  const nonce = new Uint8Array(24).fill(1);

  const message = new Uint8Array([]);

  const encrypted = nacl.secretbox(message, nonce, key);
  const decrypted = nacl.secretbox.open(encrypted, nonce, key);

  expect(decrypted).toEqual(new Uint8Array([]));
});

Интероперабельность

Отдельный класс тестов — совместимость с другими реализациями:

  • libsodium (C);
  • Go NaCl;
  • Python PyNaCl.

Такие тесты обычно используют заранее подготовленные данные и проверяют:

  • идентичность ciphertext;
  • корректность подписи;
  • совместимость ключей.

Ошибки, которые выявляются тестами

Хорошо построенный набор тестов в TweetNaCl.js обычно ловит:

  • перепутанные байтовые порядоки (endianness);
  • некорректную работу с Uint8Array vs Buffer;
  • случайное повторное использование nonce;
  • ошибки конкатенации буферов;
  • неправильную сериализацию ключей (base64 vs raw bytes).

Практика изоляции криптографических тестов

Важное правило — криптографические тесты не должны зависеть от:

  • локали;
  • кодировок по умолчанию;
  • реализации TextEncoder/TextDecoder;
  • оптимизаций движка JavaScript.

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


Модульные тесты в TweetNaCl.js становятся не просто проверкой корректности функций, а формальной системой верификации криптографических свойств, где каждая операция рассматривается как строго определённое преобразование над битовыми структурами с фиксированными инвариантами и заранее известными результатами.