В криптографических библиотеках семейства TweetNaCl.js / nacl.js ключевой материал представляет собой наиболее чувствительный тип данных, присутствующий в рантайме приложения. Основная проблема заключается в том, что JavaScript не предоставляет низкоуровневого контроля над памятью, сопоставимого с языками вроде C или Rust, что делает невозможным гарантированное физическое уничтожение данных в момент освобождения.
Ключи, однажды попавшие в память, могут сохраняться в неожиданных местах: в копиях буферов, замыканиях, оптимизированных структурах движка, временных объектах при преобразованиях типов. Это формирует класс рисков, связанных не с криптографической стойкостью алгоритмов, а с утечками через управление памятью.
TweetNaCl.js оперирует строго бинарными структурами:
Uint8Array для ключейUint8Array для nonceUint8Array для ciphertext и plaintextКлючи генерируются функциями:
const nacl = require('tweetnacl');
const keyPair = nacl.box.keyPair();
const publicKey = keyPair.publicKey;
const secretKey = keyPair.secretKey;
secretKey в данном случае представляет собой 32-байтовый
массив. Он не защищён на уровне памяти от копирования и может быть
неявно дублирован при любых операциях:
subarray)Любая операция, создающая новый Uint8Array, приводит к
дублированию данных:
const copy = new Uint8Array(secretKey);
Даже такие конструкции как slice создают новый
буфер:
const partial = secretKey.slice(0, 16);
Оригинальные данные остаются в памяти до момента сборки мусора, а копии увеличивают количество активных экземпляров ключа.
Функции в JavaScript удерживают доступ к внешним переменным через замыкания:
function createEncryptor(key) {
return function(message) {
return nacl.box(message, nonce, key, publicKey);
};
}
В таком случае key фиксируется в лексическом окружении и
не освобождается, пока существует ссылка на функцию. Это увеличивает
время жизни ключевого материала, часто непредсказуемо.
Типичная ошибка связана с преобразованием данных:
JSON.stringify(secretKey);
Такое преобразование приводит к созданию строкового представления массива. Даже если ключ далее удаляется, строка остаётся в памяти до её освобождения сборщиком мусора, а также может попадать в логи или кэши.
В среде Node.js и Chromium (V8 engine) возможны следующие эффекты:
Это делает невозможным точное управление моментом удаления данных.
Ключевой материал должен существовать в максимально узком контексте выполнения:
function encryptMessage(message) {
const key = nacl.box.keyPair().secretKey;
const encrypted = nacl.box(message, nonce, key, publicKey);
return encrypted;
}
После выхода из функции ссылка становится недоступной, ускоряя потенциальную очистку памяти.
JavaScript не предоставляет встроенного механизма secure wipe, однако возможно ручное перезаписывание:
function wipe(buffer) {
buffer.fill(0);
}
Применение:
const key = nacl.box.keyPair().secretKey;
try {
const encrypted = nacl.box(message, nonce, key, publicKey);
} finally {
wipe(key);
}
Недостаток подхода заключается в невозможности гарантировать отсутствие копий буфера.
Избегается передача ключей через множество функций. Передача осуществляется напрямую в криптографический вызов:
nacl.box(message, nonce, secretKey, publicKey);
Любое промежуточное хранение увеличивает поверхность утечки.
Категорически исключается преобразование ключей в:
Buffer.toString('base64')Любая строка становится независимой копией, не поддающейся очистке
через fill(0).
Сборщик мусора в JavaScript не гарантирует:
Даже после удаления ссылок:
let key = nacl.box.keyPair().secretKey;
key = null;
буфер может сохраняться в памяти до следующего цикла GC.
Асинхронные цепочки увеличивают риск удержания ключей:
async function process() {
const key = nacl.box.keyPair().secretKey;
const encrypted = await encryptAsync(key);
return encrypted;
}
Промисы и очереди событий могут удерживать ссылки значительно дольше, чем предполагается логикой функции.
Дополнительный риск создают:
Любое помещение ключа в такие структуры приводит к его долговременному присутствию в памяти, часто вне контроля кода.
В браузере ключевой материал может попадать:
Даже удалённые переменные могут оставаться в дампах памяти, доступных через инструменты отладки.
TweetNaCl.js проектировался как компактная реализация NaCl, а не как система с защищённой памятью. В его архитектуре отсутствуют:
Это означает, что защита ключей целиком возлагается на дисциплину обращения с объектами JavaScript.
Поведенческие модели обращения с ключами в JavaScript сводятся к следующим принципам:
Любое отклонение увеличивает вероятность появления долговременных копий в heap, что критично для сценариев, где ключевой материал является единственной точкой компрометации системы.