Хранение сессионных данных в браузере всегда начинается с определения границ доверия. Основная проблема заключается в том, что клиентская среда считается недоверенной: любой скрипт, попавший в контекст страницы, получает доступ к памяти приложения, DOM и большинству API хранения.
Ключевые угрозы:
localStorage,
sessionStorage, перехватывать переменные в памятиЛюбое решение на базе TweetNaCl.js / nacl.js должно проектироваться с учётом того, что шифрование не защищает от XSS, но может ограничить ущерб при утечке хранилища.
Самый безопасный с точки зрения персистентности вариант.
Особенности:
Используется для:
let accessToken = null;
let sessionKey = null;
Минус — необходимость повторной аутентификации или refresh-механизма.
Изолировано на уровень вкладки.
Характеристики:
sessionStorage.setItem("access_token", token);
const token = sessionStorage.getItem("access_token");
Используется для менее критичных сценариев, чем persistent storage.
Наиболее опасный вариант хранения токенов.
Проблемы:
Использование допустимо только для:
Более гибкое хранилище с асинхронным API.
Преимущества:
Библиотека TweetNaCl.js реализует криптографические primitives:
nacl.secretbox — симметричное шифрованиеnacl.box — публично-ключевая криптографияnacl.sign — цифровые подписиnacl.randomBytes — генерация nonce и ключейОсновная идея — хранить не токен, а его зашифрованную форму.
Ключ должен быть непредсказуемым и не храниться в открытом виде.
import nacl from "tweetnacl";
import util from "tweetnacl-util";
const key = nacl.randomBytes(32);
В реальных системах ключ чаще выводится из:
secretbox использует:
Критически важно:
nonce нельзя повторно использовать с тем же ключом
function encryptToken(token, key) {
const nonce = nacl.randomBytes(24);
const message = util.decodeUTF8(token);
const box = nacl.secretbox(message, nonce, key);
return {
nonce: util.encodeBase64(nonce),
box: util.encodeBase64(box)
};
}
function decryptToken(encrypted, key) {
const nonce = util.decodeBase64(encrypted.nonce);
const box = util.decodeBase64(encrypted.box);
const message = nacl.secretbox.open(box, nonce, key);
if (!message) {
throw new Error("Decryption failed");
}
return util.encodeUTF8(message);
}
const encrypted = encryptToken(accessToken, key);
localStorage.setItem("auth_token", JSON.stringify(encrypted));
const stored = JSON.parse(localStorage.getItem("auth_token"));
const token = decryptToken(stored, key);
Основная слабость всей схемы заключается в том, что:
если ключ хранится рядом с данными — защита теряет смысл
Типовые подходы:
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const baseKey = await crypto.subtle.importKey(
"raw",
enc.encode(password),
"PBKDF2",
false,
["deriveBits"]
);
const derived = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt,
iterations: 100000,
hash: "SHA-256"
},
baseKey,
256
);
return new Uint8Array(derived);
}
Далее ключ можно использовать в TweetNaCl.js после преобразования.
На практике применяется гибрид:
nonce должен быть:
const nonce = nacl.randomBytes(24);
Ошибки:
Последствия:
Дополнительный уровень — nacl.sign.
Используется для:
const keyPair = nacl.sign.keyPair();
const message = util.decodeUTF8("session-data");
const signed = nacl.sign(message, keyPair.secretKey);
Проверка:
const verified = nacl.sign.open(signed, keyPair.publicKey);
if (!verified) {
throw new Error("Invalid signature");
}
Даже при использовании TweetNaCl.js шифрование не компенсирует XSS.
Если атакующий выполняет JavaScript в контексте страницы:
Следовательно, криптография решает только:
Практика снижения ущерба:
{
accessToken: null,
encryptedRefreshToken: {
nonce: "...",
box: "..."
},
keyId: "device-derived-key",
issuedAt: 1710000000
}
Наиболее частые проблемы:
Комбинация уровней:
В современных приложениях предпочтение отдаётся TweetNaCl.js как reference implementation.
const key = nacl.randomBytes(32);
function storeSession(token) {
const encrypted = encryptToken(token, key);
sessionStorage.setItem("session", JSON.stringify(encrypted));
}
function loadSession() {
const raw = sessionStorage.getItem("session");
if (!raw) return null;
const encrypted = JSON.parse(raw);
return decryptToken(encrypted, key);
}
Любая реализация в JavaScript сталкивается с фундаментальными ограничениями:
Криптографические примитивы TweetNaCl.js повышают устойчивость к утечкам хранилища, но не заменяют архитектурные меры защиты сессий.