Шифрование больших данных: разбивка на блоки

Библиотеки TweetNaCl.js и nacl.js реализуют примитивы NaCl, ориентированные на безопасное симметричное и асимметричное шифрование. Основной высокоуровневый механизм для конфиденциальных данных — secretbox (XSalsa20 + Poly1305). Он предназначен для шифрования отдельных сообщений фиксированной логики: одно сообщение → один зашифрованный блок.

Внутри secretbox существует фундаментальное ограничение: размер одного сообщения не предназначен для бесконечно больших потоков данных. Хотя технически допустимы довольно крупные буферы, работа с файлами, видеопотоками или архивами требует иной архитектуры — разбиения данных на блоки.

Базовая модель работы secretbox

Шифрование в nacl.secretbox устроено следующим образом:

  • ключ: 32 байта
  • nonce: 24 байта (обязательно уникален для каждой операции с данным ключом)
  • алгоритм: XSalsa20 (потоковый шифр) + Poly1305 (аутентификация)

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

const key = nacl.randomBytes(32);
const nonce = nacl.randomBytes(24);

const message = new TextEncoder().encode("секретные данные");

const boxed = nacl.secretbox(message, nonce, key);
const opened = nacl.secretbox.open(boxed, nonce, key);

Проблема возникает, когда message перестаёт быть “сообщением” и становится потоком данных.


Почему большие данные нельзя шифровать одним блоком

При попытке зашифровать файл целиком возникают ограничения:

  • расход памяти (вся нагрузка загружается в RAM)
  • риск блокировки UI в браузере
  • невозможность обработки потоков (streaming)
  • невозможность повторного использования nonce

Особенно критично второе: повтор nonce + один и тот же ключ → катастрофическая потеря безопасности в XSalsa20.


Архитектура разбиения данных на блоки

Основная идея: представить большой файл как последовательность независимых сообщений фиксированного размера.

Базовый принцип

Файл разбивается на блоки:

  • фиксированный размер (например, 64 KB или 256 KB)
  • каждый блок шифруется отдельно
  • nonce изменяется для каждого блока

Стратегии генерации nonce

Nonce должен быть уникальным для каждого блока. Существует несколько подходов.

1. Инкрементный nonce

Самый распространённый способ — использовать часть nonce как счётчик.

function incrementNonce(nonce) {
  for (let i = nonce.length - 1; i >= 0; i--) {
    nonce[i]++;
    if (nonce[i] !== 0) break;
  }
}

Инициализация:

  • первые 16 байт — случайные (salt/stream id)
  • последние 8 байт — счётчик блоков

2. Разделение nonce на prefix + counter

const baseNonce = nacl.randomBytes(16);
let counter = 0;

function makeNonce() {
  const nonce = new Uint8Array(24);
  nonce.set(baseNonce, 0);

  const view = new DataView(nonce.buffer);
  view.setBigUint64(16, BigInt(counter), true);

  counter++;
  return nonce;
}

Преимущество: отсутствие мутаций, удобство параллельной обработки.


Разбиение данных на блоки

Ключевой этап — разрезание входного буфера.

function chunkBuffer(buffer, chunkSize) {
  const chunks = [];
  for (let i = 0; i < buffer.length; i += chunkSize) {
    chunks.push(buffer.slice(i, i + chunkSize));
  }
  return chunks;
}

Типичные размеры:

  • 16 KB — минимизация задержек
  • 64 KB — баланс скорости и памяти
  • 256 KB — оптимизация CPU

Шифрование потоков данных

Полный процесс шифрования большого массива данных:

function encryptLargeData(data, key) {
  const chunkSize = 64 * 1024;
  const chunks = chunkBuffer(data, chunkSize);

  const baseNonce = nacl.randomBytes(16);
  let counter = 0;

  const encryptedChunks = [];

  for (const chunk of chunks) {
    const nonce = new Uint8Array(24);
    nonce.set(baseNonce, 0);

    const view = new DataView(nonce.buffer);
    view.setBigUint64(16, BigInt(counter), true);

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

    encryptedChunks.push(encrypted);
    counter++;
  }

  return {
    chunks: encryptedChunks,
    baseNonce
  };
}

Формат хранения результата

При разбиении важно сохранять метаданные:

  • размер блока
  • baseNonce
  • алгоритм
  • количество блоков

Типичная структура:

[header][chunk1][chunk2][chunk3]...

Где header содержит:

{
  "algorithm": "secretbox-xsalsa20-poly1305",
  "chunkSize": 65536,
  "noncePrefix": "...",
  "totalChunks": 120
}

Дешифрование блоков

Дешифрование выполняется симметрично:

function decryptLargeData(chunks, key, baseNonce) {
  const result = [];

  for (let i = 0; i < chunks.length; i++) {
    const nonce = new Uint8Array(24);
    nonce.set(baseNonce, 0);

    const view = new DataView(nonce.buffer);
    view.setBigUint64(16, BigInt(i), true);

    const decrypted = nacl.secretbox.open(chunks[i], nonce, key);

    if (!decrypted) {
      throw new Error("Ошибка аутентификации блока");
    }

    result.push(decrypted);
  }

  return concatBuffers(result);
}

Объединение блоков

function concatBuffers(buffers) {
  let totalLength = 0;

  for (const b of buffers) {
    totalLength += b.length;
  }

  const result = new Uint8Array(totalLength);

  let offset = 0;
  for (const b of buffers) {
    result.set(b, offset);
    offset += b.length;
  }

  return result;
}

Потоковая модель вместо загрузки в память

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

  • FileReader (браузер)
  • Streams API
  • Node.js fs.createReadStream

Пример концепции:

чтение блока → шифрование → запись → следующий блок

Использование Web Streams API

В браузере можно построить transform stream:

class EncryptStream extends TransformStream {
  constructor(key) {
    let counter = 0;
    const baseNonce = nacl.randomBytes(16);

    super({
      transform(chunk, controller) {
        const nonce = new Uint8Array(24);
        nonce.set(baseNonce, 0);

        const view = new DataView(nonce.buffer);
        view.setBigUint64(16, BigInt(counter++), true);

        const encrypted = nacl.secretbox(chunk, nonce, key);
        controller.enqueue(encrypted);
      }
    });
  }
}

Критические ошибки при реализации

Повтор nonce

Повтор nonce при одном ключе полностью разрушает безопасность XSalsa20.

Изменение порядка блоков

Каждый блок автономен, но порядок важен. Потеря индексации приводит к невозможности восстановления данных.

Несогласованность chunkSize

Если на этапе шифрования и дешифрования используются разные размеры блоков без явной метадаты — восстановление невозможно.


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

Хотя secretbox уже включает Poly1305-аутентификацию, на уровне всей структуры часто добавляют:

  • хэш всего файла (SHA-256)
  • или Merkle tree для частичной проверки

Пример добавления контроля:

const hash = nacl.hash(fullData);

Оптимизация производительности

1. Минимизация аллокаций

Переподсчёт nonce через мутацию быстрее, чем создание нового массива.

2. Использование Uint8Array без конверсий

Избегать TextEncoder/TextDecoder внутри циклов.

3. Batch обработка

Объединение нескольких блоков перед записью снижает I/O нагрузку.


Сценарии применения блочного шифрования

  • шифрование файлов в браузере перед загрузкой
  • защищённые чаты с вложениями
  • офлайн-хранилища (IndexedDB encrypted blobs)
  • передача видео и аудио потоков
  • защищённые backup-системы

Особенности nacl.js vs TweetNaCl.js

  • TweetNaCl.js — минималистичная реализация, меньше overhead
  • nacl.js — иногда обёртки и дополнительные утилиты

Однако модель блочного шифрования идентична: всё строится поверх secretbox.


Масштабирование подхода

При увеличении объёмов данных до гигабайтов добавляются дополнительные уровни:

  • параллельное шифрование блоков (Web Workers)
  • очереди задач
  • backpressure при потоках
  • запись напрямую в файл (stream piping)

Параллельное шифрование блоков

async function encryptParallel(chunks, key) {
  return Promise.all(
    chunks.map((chunk, i) => encryptChunk(chunk, key, i))
  );
}

Проблема: nonce должен оставаться детерминированным или централизованным.


Итоговая модель архитектуры

Система блочного шифрования на базе TweetNaCl.js обычно включает:

  • генератор nonce (prefix + counter)
  • chunker данных
  • encrypt/decrypt pipeline
  • метаданные контейнера
  • потоковую обработку

Такая модель превращает secretbox из “шифрования сообщения” в полноценный механизм для работы с произвольными объёмами данных без изменения криптографического ядра.