Библиотека TweetNaCl.js реализует набор криптографических примитивов
NaCl (Networking and Cryptography library), ориентированных на простоту
и безопасность. В контексте работы с файлами и потоками в Node.js
ключевым инструментом становится nacl.secretbox,
обеспечивающий симметричное шифрование с аутентификацией.
Главная особенность NaCl-подхода — отказ от «магических режимов» и акцент на явное управление nonce, ключами и целостностью данных.
Для потокового шифрования файлов важно учитывать ограничение:
secretbox работает с фиксированным сообщением, а не с
потоками. Поэтому потоковая модель строится поверх разбиения данных на
блоки.
Основной механизм симметричного шифрования:
nacl.secretbox(message, nonce, key)
Расшифровка:
message — Uint8Array с даннымиnonce — 24-байтовое уникальное значение для каждого
сообщенияkey — 32-байтовый секретный ключДешифрование:
nacl.secretbox.open(box, nonce, key)
Если nonce повторяется с тем же ключом, безопасность полностью нарушается. В потоковом режиме это критически важно.
Для файлового шифрования используются случайные ключи и начальный nonce:
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
const key = nacl.randomBytes(32);
const nonce = nacl.randomBytes(24);
Nonce не должен быть случайным для каждого блока при потоковой модели. Он должен изменяться предсказуемо (например, инкрементироваться).
Так как secretbox не поддерживает поток напрямую,
применяется блочная схема:
Nonce — 24 байта. Его можно трактовать как число и увеличивать:
function incrementNonce(nonce) {
const result = new Uint8Array(nonce);
for (let i = 0; i < result.length; i++) {
result[i]++;
if (result[i] !== 0) break;
}
return result;
}
Такой подход обеспечивает уникальность nonce для каждого блока.
Стандартный поток чтения файла:
const fs = require('fs');
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
const key = nacl.randomBytes(32);
let nonce = nacl.randomBytes(24);
const input = fs.createReadStream('input.txt', {
highWaterMark: 16 * 1024
});
const output = fs.createWriteStream('output.enc');
Основная логика обработки:
input.on('data', (chunk) => {
const uint8Chunk = new Uint8Array(chunk);
const encrypted = nacl.secretbox(uint8Chunk, nonce, key);
output.write(Buffer.from(encrypted));
nonce = incrementNonce(nonce);
});
Процесс обратный:
const input = fs.createReadStream('output.enc', {
highWaterMark: 16 * 1024 + 16
});
const output = fs.createWriteStream('decrypted.txt');
let nonce = initialNonce;
input.on('data', (chunk) => {
const decrypted = nacl.secretbox.open(
new Uint8Array(chunk),
nonce,
key
);
if (!decrypted) {
throw new Error('Ошибка дешифрования');
}
output.write(Buffer.from(decrypted));
nonce = incrementNonce(nonce);
});
secretbox добавляет overhead в 16 байт (MAC).
Поэтому:
При чтении важно учитывать это расхождение, иначе поток разъедется.
Практическая структура:
[nonce (24 bytes)][cipher chunk 1][cipher chunk 2]...
Однако при потоковом подходе nonce обычно хранится отдельно (например, в заголовке файла или метаданных).
Пример записи заголовка:
output.write(Buffer.from(nonce));
Более правильная архитектура в Node.js — использование
Transform:
const { Transform } = require('stream');
class EncryptStream extends Transform {
constructor(key, nonce) {
super();
this.key = key;
this.nonce = nonce;
}
_transform(chunk, encoding, callback) {
const encrypted = nacl.secretbox(
new Uint8Array(chunk),
this.nonce,
this.key
);
this.push(Buffer.from(encrypted));
this.nonce = incrementNonce(this.nonce);
callback();
}
}
Использование:
fs.createReadStream('input.txt')
.pipe(new EncryptStream(key, nonce))
.pipe(fs.createWriteStream('output.enc'));
class DecryptStream extends Transform {
constructor(key, nonce) {
super();
this.key = key;
this.nonce = nonce;
}
_transform(chunk, encoding, callback) {
const decrypted = nacl.secretbox.open(
new Uint8Array(chunk),
this.nonce,
this.key
);
if (!decrypted) {
return callback(new Error('Decryption failed'));
}
this.push(Buffer.from(decrypted));
this.nonce = incrementNonce(this.nonce);
callback();
}
}
При работе с файлами большого размера критично учитывать:
Transform автоматически управляет backpressure, что
делает его предпочтительным вариантом.
Наиболее частые проблемы:
Любое нарушение последовательности приводит к невозможности расшифровки последующих блоков.
TweetNaCl.js не предоставляет полноценного streaming API. Поэтому для более сложных задач часто применяется:
Node.js Buffer требует явного преобразования:
const uint8 = new Uint8Array(buffer);
const back = Buffer.from(uint8);
Ошибка в этом месте часто приводит к повреждению данных при дешифровке.
При необходимости хранения в JSON:
const encoded = nacl.util.encodeBase64(encrypted);
const decoded = nacl.util.decodeBase64(encoded);
Base64 увеличивает размер примерно на 33%, но упрощает транспортировку.
secretbox уже включает Poly1305 MAC, что
обеспечивает:
При ошибке secretbox.open возвращает
null.
Основные ограничения:
secretboxОптимизация достигается через:
Практически используемая схема:
Такая структура обеспечивает детерминированное восстановление данных при наличии ключа и начального nonce.
Эти ошибки приводят к полной невозможности восстановления данных без явных сообщений об ошибках.
Потоковое шифрование на основе TweetNaCl.js применяется в:
При этом архитектура остаётся простой: поток → шифратор → файл, без промежуточных буферов и сложных криптографических режимов.