localStorage в браузере хранит данные в открытом виде, доступные любому скрипту, выполняющемуся в контексте страницы. Это создаёт прямую угрозу при хранении токенов, персональных данных или любой информации, чувствительной к утечке через XSS. Поэтому перед записью данных в localStorage используется симметричное шифрование на стороне клиента, где одним из практичных решений выступает Stanford JS Crypto Library (SJCL).
Основная идея заключается в том, что данные преобразуются в зашифрованную строку до момента сохранения, а при чтении — расшифровываются обратно в исходный объект. localStorage при этом выступает лишь как небезопасное хранилище строк, не способное интерпретировать содержимое.
Ключевой принцип:
SJCL реализует симметричное шифрование на базе AES с режимом CCM и встроенной генерацией случайных значений. Библиотека также поддерживает вывод ключей через PBKDF2.
Типовой поток выглядит следующим образом:
SJCL предоставляет простой интерфейс:
const data = {
userId: 42,
token: "abc123",
role: "admin"
};
const password = "strong-password";
// сериализация
const json = JSON.stringify(data);
// шифрование
const encrypted = sjcl.encrypt(password, json);
// запись в localStorage
localStorage.setItem("secure_data", encrypted);
Функция sjcl.encrypt возвращает JSON-строку,
содержащую:
Это позволяет восстановить данные при наличии пароля.
При чтении выполняется обратная операция:
const encrypted = localStorage.getItem("secure_data");
const decrypted = sjcl.decrypt(password, encrypted);
const data = JSON.parse(decrypted);
Если пароль неверный или данные повреждены, SJCL выбрасывает исключение. Поэтому в реальных приложениях необходимо оборачивать операцию в try/catch.
let data = null;
try {
const encrypted = localStorage.getItem("secure_data");
if (encrypted) {
const decrypted = sjcl.decrypt(password, encrypted);
data = JSON.parse(decrypted);
}
} catch (e) {
console.error("Ошибка расшифровки данных:", e);
localStorage.removeItem("secure_data");
}
Удаление повреждённых данных предотвращает повторные ошибки при последующих загрузках.
SJCL работает со строками, поэтому любые структуры должны быть сериализованы заранее. Это включает вложенные объекты, массивы и конфигурации приложения.
const sessionState = {
user: {
id: 10,
name: "Alex",
permissions: ["read", "write"]
},
settings: {
theme: "dark",
lang: "ru"
}
};
const encrypted = sjcl.encrypt(password, JSON.stringify(sessionState));
localStorage.setItem("session", encrypted);
При восстановлении важно строго соблюдать порядок операций:
const decrypted = sjcl.decrypt(password, localStorage.getItem("session"));
const sessionState = JSON.parse(decrypted);
На практике пароль не должен быть жёстко зашит в код. SJCL позволяет использовать PBKDF2 для вывода ключа из пользовательского пароля.
const salt = sjcl.random.randomWords(4);
const key = sjcl.misc.pbkdf2(password, salt, 10000, 256);
Однако чаще используется встроенная схема sjcl.encrypt,
которая автоматически:
Это снижает вероятность ошибок при ручной настройке криптографии.
SJCL позволяет управлять параметрами:
const encrypted = sjcl.encrypt(password, json, {
iter: 10000,
ks: 256,
mode: "ccm",
ts: 64
});
Здесь:
iter — число итераций PBKDF2 (влияет на устойчивость к
brute-force)ks — размер ключаmode — режим блочного шифрованияts — длина аутентификационного тегаУвеличение итераций повышает безопасность, но снижает производительность.
При проектировании клиентского хранилища с шифрованием структура обычно строится вокруг двух функций:
function saveSecure(key, value, password) {
const json = JSON.stringify(value);
const encrypted = sjcl.encrypt(password, json);
localStorage.setItem(key, encrypted);
}
function loadSecure(key, password) {
const encrypted = localStorage.getItem(key);
if (!encrypted) return null;
const decrypted = sjcl.decrypt(password, encrypted);
return JSON.parse(decrypted);
}
Такой подход унифицирует работу с любыми данными.
localStorage имеет ряд критических ограничений:
Поэтому даже при использовании SJCL остаётся риск компрометации при наличии вредоносного скрипта. Шифрование защищает данные только «в состоянии покоя», но не во время работы приложения.
Если атакующий получает возможность выполнять JavaScript в контексте страницы, он получает:
Поэтому SJCL в localStorage следует рассматривать как защиту от:
но не как защиту от XSS.
Шифрование увеличивает размер строки примерно на 30–100%. При больших объектах это важно учитывать.
Практики оптимизации:
const compact = {
u: userId,
t: token,
r: role
};
Наиболее частый кейс — временное хранение состояния авторизации:
const session = {
accessToken: "jwt-token",
expires: Date.now() + 3600 * 1000
};
saveSecure("session", session, password);
При загрузке приложения выполняется проверка срока действия после расшифровки:
const session = loadSecure("session", password);
if (session && session.expires > Date.now()) {
// активная сессия
} else {
localStorage.removeItem("session");
}
Такой подход снижает риск использования устаревших токенов даже при наличии доступа к зашифрованным данным.