Шифрование данных в браузере: работа с File API

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

Браузер предоставляет File API, позволяющий получать доступ к содержимому файлов через объекты File и Blob. Эти объекты не содержат байтов в явном виде до момента чтения, поэтому требуется асинхронное извлечение данных.

Основной инструмент для этого — FileReader.

const fileInput = document.querySelector('input[type="file"]');

fileInput.addEventListener('change', async (event) => {
  const file = event.target.files[0];
  const arrayBuffer = await file.arrayBuffer();
  const data = new Uint8Array(arrayBuffer);

  console.log(data);
});

Метод arrayBuffer() является предпочтительным, поскольку он сразу возвращает бинарное представление без промежуточных преобразований в строки.

TweetNaCl.js и модель работы с данными

TweetNaCl.js — компактная криптографическая библиотека, реализующая NaCl (Networking and Cryptography library). Она оперирует только байтовыми массивами и предоставляет два основных подхода к шифрованию:

  • симметричное шифрование (nacl.secretbox)
  • асимметричное шифрование (nacl.box)

Для работы с файлами чаще используется secretbox, так как он проще и быстрее.

Схема симметричного шифрования

ciphertext = nacl.secretbox(message, nonce, key)
plaintext = nacl.secretbox.open(ciphertext, nonce, key)
  • message — исходные данные (Uint8Array)
  • nonce — уникальное значение (24 байта)
  • key — секретный ключ (32 байта)

Генерация ключей и nonce

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

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

Ключ можно сохранять локально (например, в IndexedDB), но nonce никогда не должен повторяться для одной и той же пары ключ/сообщение.

Шифрование файлов через File API

После получения файла и преобразования его в Uint8Array, можно применить secretbox.

async function encryptFile(file, key) {
  const arrayBuffer = await file.arrayBuffer();
  const data = new Uint8Array(arrayBuffer);

  const nonce = nacl.randomBytes(24);
  const encrypted = nacl.secretbox(data, nonce, key);

  return {
    encrypted,
    nonce
  };
}

Здесь важно учитывать, что результат шифрования — это тоже Uint8Array, который не содержит метаданных. Поэтому nonce необходимо сохранять отдельно.

Объединение nonce и зашифрованных данных

В практических сценариях nonce часто конкатенируется с шифротекстом:

function combineNonceAndCipher(nonce, ciphertext) {
  const result = new Uint8Array(nonce.length + ciphertext.length);
  result.set(nonce, 0);
  result.set(ciphertext, nonce.length);
  return result;
}

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

Расшифрование файла

Процесс обратный: извлекается nonce, затем выполняется secretbox.open.

function decryptFile(combined, key) {
  const nonce = combined.slice(0, 24);
  const ciphertext = combined.slice(24);

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

  if (!decrypted) {
    throw new Error('Ошибка расшифрования');
  }

  return decrypted;
}

Если ключ или данные неверны, функция возвращает null, что служит индикатором неудачи.

Сохранение зашифрованных файлов

После шифрования данные можно сохранить как Blob:

function saveFile(data, filename) {
  const blob = new Blob([data], { type: 'application/octet-stream' });
  const url = URL.createObjectURL(blob);

  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();

  URL.revokeObjectURL(url);
}

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

Полный цикл обработки файла

Общий поток обработки включает:

  1. получение файла через File API
  2. преобразование в Uint8Array
  3. генерацию nonce
  4. шифрование через TweetNaCl.js
  5. объединение nonce и данных
  6. сохранение результата
async function processFile(file, key) {
  const buffer = await file.arrayBuffer();
  const data = new Uint8Array(buffer);

  const nonce = nacl.randomBytes(24);
  const encrypted = nacl.secretbox(data, nonce, key);

  const combined = new Uint8Array(nonce.length + encrypted.length);
  combined.set(nonce);
  combined.set(encrypted, nonce.length);

  return combined;
}

Ограничения работы в браузере

При использовании File API и TweetNaCl.js необходимо учитывать ряд ограничений:

  • память браузера ограничена, большие файлы могут вызывать нагрузку
  • шифрование выполняется в основном потоке, что может блокировать UI
  • отсутствует потоковая обработка в стандартном API TweetNaCl.js
  • большие бинарные объекты требуют осторожного обращения с копированием Uint8Array

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

Обработка больших файлов через чанки

При необходимости можно разделить файл:

const CHUNK_SIZE = 64 * 1024;

function splitIntoChunks(data) {
  const chunks = [];
  for (let i = 0; i < data.length; i += CHUNK_SIZE) {
    chunks.push(data.slice(i, i + CHUNK_SIZE));
  }
  return chunks;
}

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

Особенности безопасности nonce

Nonce в TweetNaCl.js не должен повторяться при одном и том же ключе. Повтор nonce приводит к компрометации данных из-за свойств поточного шифра.

Практическая схема:

  • nonce генерируется случайно для каждого файла
  • nonce хранится вместе с шифротекстом
  • ключ отделён от данных и не передаётся вместе с файлом

Взаимодействие с интерфейсом браузера

Интеграция File API в пользовательский интерфейс обычно строится вокруг событий:

  • change для input
  • dragover и drop для drag-and-drop
dropZone.addEventListener('drop', async (event) => {
  event.preventDefault();

  const file = event.dataTransfer.files[0];
  const encrypted = await processFile(file, key);

  saveFile(encrypted, 'encrypted.bin');
});

Drag-and-drop позволяет обрабатывать файлы без промежуточного сохранения.

Конвертация между строками и бинарными данными

Хотя TweetNaCl.js работает с байтами, иногда требуется представление в строковом виде (например, для хранения в JSON или отправки через API). Используется Base64:

function toBase64(uint8) {
  return btoa(String.fromCharCode(...uint8));
}

function fromBase64(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);

  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }

  return bytes;
}

Base64 увеличивает размер данных примерно на 33%, что следует учитывать при хранении.

Хранение ключей и интеграция с браузерным хранилищем

Для долговременного хранения ключей используется IndexedDB:

async function storeKey(key) {
  const db = indexedDB.open('crypto-db', 1);

  db.onsucc ess = () => {
    const tx = db.result.transaction('keys', 'readwrite');
    const store = tx.objectStore('keys');

    store.put(key, 'main-key');
  };
}

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

Типичные ошибки при работе с File API и TweetNaCl.js

  • использование строк вместо Uint8Array
  • повтор nonce для одного ключа
  • попытка шифровать слишком большие файлы без разбиения
  • потеря nonce при хранении шифротекста
  • смешивание кодировок при конвертации данных

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

Производительность операций

TweetNaCl.js оптимизирован для компактности, а не для максимальной скорости. При работе с File API основное узкое место — копирование массивов и операции с памятью.

Оптимизация достигается через:

  • минимизацию промежуточных копий Uint8Array
  • повторное использование буферов
  • отказ от лишних преобразований в строки

Работа с файлами в браузере через File API в связке с TweetNaCl.js формирует устойчивую модель локального шифрования, в которой данные никогда не покидают клиентскую среду в открытом виде, а все операции выполняются над низкоуровневыми бинарными структурами.