Шифрование загружаемых файлов в браузере до передачи

Передача файлов через браузерный интерфейс в типичной веб-архитектуре предполагает доверие к транспортному уровню (TLS/HTTPS) и серверной стороне. Однако этого недостаточно в сценариях, где требуется защита содержимого уже на клиенте:

  • данные не должны быть доступны серверу в открытом виде
  • необходимо исключить возможность компрометации на уровне прокси, логов или промежуточных сервисов
  • требуется защита при хранении на сервере (zero-knowledge подход)

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

Криптографическая основа TweetNaCl.js

TweetNaCl.js — это компактная реализация криптографической библиотеки NaCl (Networking and Cryptography library), адаптированная для JavaScript. Она предоставляет набор примитивов:

  • симметричное шифрование: secretbox
  • асимметричное шифрование: box
  • генерация ключей
  • криптографически стойкие случайные значения

Основные свойства библиотеки:

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

В браузерных сценариях чаще всего используются:

  • nacl.secretbox — шифрование файла с заранее согласованным ключом
  • nacl.box — шифрование с использованием пары ключей (публичный/приватный)

Представление файла в памяти браузера

Файл, полученный через <input type="file">, представляет собой объект File, который можно преобразовать в бинарное представление:

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

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

На этом этапе данные уже готовы к криптографической обработке.


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

Симметричный ключ

Для secretbox требуется 32-байтовый ключ:

const key = nacl.randomBytes(32);

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


Публично-ключевая схема

Для box используются две пары ключей:

const keyPair = nacl.box.keyPair();

const publicKey = keyPair.publicKey;
const secretKey = keyPair.secretKey;

Обычно:

  • публичный ключ сервера известен клиенту заранее
  • клиент генерирует ephemeral ключи или использует постоянную пару

Шифрование файла с помощью secretbox

secretbox требует:

  • nonce (24 байта)
  • ключ (32 байта)
  • сообщение (Uint8Array)

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

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

Важно: secretbox возвращает только шифртекст без nonce, его нужно передавать отдельно.


Формирование пакета для отправки

При отправке на сервер необходимо объединить:

  • nonce
  • зашифрованные данные
  • метаданные (имя файла, тип, размер)

Часто используется формат:

function concatBuffers(...buffers) {
    let totalLength = 0;
    buffers.forEach(b => totalLength += b.length);

    const result = new Uint8Array(totalLength);
    let offset = 0;

    buffers.forEach(b => {
        result.set(b, offset);
        offset += b.length;
    });

    return result;
}

const payload = concatBuffers(nonce, encrypted);

Отправка через fetch

Передача выполняется как бинарный поток:

await fetch('/upload', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/octet-stream'
    },
    body: payload
});

На сервере затем необходимо:

  • отделить nonce
  • расшифровать данные
  • восстановить файл

Использование асимметричного шифрования (box)

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

Процесс включает:

  • публичный ключ получателя
  • приватный ключ отправителя
  • nonce
const nonce = nacl.randomBytes(24);

const sharedKey = nacl.box.keyPair();

const encrypted = nacl.box(
    uint8,
    nonce,
    serverPublicKey,
    sharedKey.secretKey
);

Сервер расшифровывает своим приватным ключом.


Обмен ключами и ECDH модель

В практических системах используется схема:

  • сервер публикует публичный ключ
  • клиент генерирует ephemeral ключ
  • вычисляется общий секрет
  • данные шифруются через secretbox

Это снижает нагрузку и упрощает криптографический слой.


Работа с большими файлами

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

Разбиение на блоки

const chunkSize = 64 * 1024;
const chunks = [];

for (let i = 0; i < uint8.length; i += chunkSize) {
    chunks.push(uint8.slice(i, i + chunkSize));
}

Каждый блок шифруется отдельно:

const encryptedChunks = chunks.map(chunk => {
    const nonce = nacl.randomBytes(24);
    const encrypted = nacl.secretbox(chunk, nonce, key);
    return { nonce, encrypted };
});

Проблема nonce-менеджмента

Nonce должен быть:

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

Практика:

  • использовать счетчик + случайный префикс
  • либо полностью случайные nonce с проверкой коллизий (редко на практике)

Упаковка многоблочного файла

Каждый блок необходимо сериализовать:

function serializeChunk(nonce, encrypted) {
    const header = new Uint8Array(24 + 4);

    header.set(nonce, 0);

    const length = new Uint32Array([encrypted.length]);
    header.set(new Uint8Array(length.buffer), 24);

    return concatBuffers(header, encrypted);
}

Финальный пакет:

  • последовательность блоков
  • либо индексированная структура

Декодирование на сервере

Процесс обратный:

  1. извлечь nonce
  2. прочитать длину
  3. расшифровать блок
const decrypted = nacl.secretbox.open(encrypted, nonce, key);

Если результат null, значит:

  • повреждены данные
  • неверный ключ
  • неправильный nonce

Защита от повторной отправки и атак воспроизведения

Использование nonce позволяет избежать повторного использования зашифрованных сообщений, но дополнительно применяются:

  • временные метки
  • подписи (HMAC поверх payload)
  • серверные session keys

Сериализация бинарных данных

Браузер не всегда удобно работает с Uint8Array напрямую, поэтому часто применяется Base64:

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

function fromBase64(str) {
    return new Uint8Array(atob(str).split('').map(c => c.charCodeAt(0)));
}

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


Интеграция с FileReader и потоковой обработкой

Альтернативный способ загрузки:

const reader = new FileReader();

reader.onl oad = () => {
    const uint8 = new Uint8Array(reader.result);
};

reader.readAsArrayBuffer(file);

Для больших файлов возможно использование потокового API (ReadableStream), но TweetNaCl требует буферизацию блоков.


Ограничения TweetNaCl.js в браузере

Несмотря на удобство, библиотека имеет ограничения:

  • нет встроенного streaming encryption
  • нет поддержки AEAD режимов уровня AES-GCM
  • фиксированные размеры nonce и ключей
  • необходимость ручного управления пакетами

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

  • предварительную буферизацию
  • контроль целостности на уровне приложения
  • аккуратное управление памятью

Безопасное хранение ключей на клиенте

Ключи не должны храниться в открытом виде в:

  • localStorage без защиты
  • URL-параметрах
  • глобальных переменных без изоляции

Возможные подходы:

  • WebCrypto API для хранения ключей
  • session-based ephemeral keys
  • derivation через PBKDF2 / Argon2 (через WASM)

Практическая схема end-to-end шифрования файла

Типовой поток:

  1. пользователь выбирает файл
  2. файл преобразуется в Uint8Array
  3. генерируется nonce
  4. выполняется secretbox
  5. формируется пакет (nonce + ciphertext)
  6. отправка через fetch
  7. сервер хранит только шифртекст
  8. расшифровка выполняется только при наличии ключа

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

Основные узкие места:

  • копирование больших Uint8Array
  • частые аллокации при chunking
  • сериализация в Base64

Оптимизации:

  • переиспользование буферов
  • минимизация промежуточных преобразований
  • работа напрямую с ArrayBuffer
  • обработка через Web Workers для UI-неблокирующего шифрования

Типовые ошибки при использовании

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

Архитектурные варианты применения

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