Передача файлов через браузерный интерфейс в типичной веб-архитектуре предполагает доверие к транспортному уровню (TLS/HTTPS) и серверной стороне. Однако этого недостаточно в сценариях, где требуется защита содержимого уже на клиенте:
В этих условиях шифрование выполняется до отправки файла из браузера, а сервер получает только криптографически защищённый набор байтов.
TweetNaCl.js — это компактная реализация криптографической библиотеки NaCl (Networking and Cryptography library), адаптированная для JavaScript. Она предоставляет набор примитивов:
secretboxboxОсновные свойства библиотеки:
Uint8ArrayВ браузерных сценариях чаще всего используются:
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;
Обычно:
secretbox требует:
Nonce должен быть уникальным для каждого шифрования с данным ключом.
const nonce = nacl.randomBytes(24);
const encrypted = nacl.secretbox(uint8, nonce, key);
Важно: secretbox возвращает только шифртекст без 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);
Передача выполняется как бинарный поток:
await fetch('/upload', {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream'
},
body: payload
});
На сервере затем необходимо:
Если требуется, чтобы сервер не мог расшифровать данные без
приватного ключа клиента или наоборот, используется
nacl.box.
Процесс включает:
const nonce = nacl.randomBytes(24);
const sharedKey = nacl.box.keyPair();
const encrypted = nacl.box(
uint8,
nonce,
serverPublicKey,
sharedKey.secretKey
);
Сервер расшифровывает своим приватным ключом.
В практических системах используется схема:
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 должен быть:
Практика:
Каждый блок необходимо сериализовать:
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);
}
Финальный пакет:
Процесс обратный:
const decrypted = nacl.secretbox.open(encrypted, nonce, key);
Если результат null, значит:
Использование nonce позволяет избежать повторного использования зашифрованных сообщений, но дополнительно применяются:
Браузер не всегда удобно работает с 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.
Альтернативный способ загрузки:
const reader = new FileReader();
reader.onl oad = () => {
const uint8 = new Uint8Array(reader.result);
};
reader.readAsArrayBuffer(file);
Для больших файлов возможно использование потокового API
(ReadableStream), но TweetNaCl требует буферизацию
блоков.
Несмотря на удобство, библиотека имеет ограничения:
Поэтому архитектура шифрования должна учитывать:
Ключи не должны храниться в открытом виде в:
Возможные подходы:
Типовой поток:
secretboxОсновные узкие места:
Оптимизации:
secretbox как потоковый шифр